iOS之block,一點小心得


  作為一個iOS開發程序員,沒用過block是不可能的。這次我探討的是block原理,但是有些更深層次的東西,我也不是很清楚,以后隨着更加了解block將會慢慢完善。

  第一個問題,什么是block?

  我們都會用block,但是block是什么呢,這是首先要弄清楚的概念。雖然,是什么並不影響我們用它,但是搞清楚原理我們才能更好的去使用它,我覺得作為一個程序員,需要時刻保持對事物原理追究的心態?

  block的是本質是對象。但是你也可以說它是代碼塊、閉包、內聯函數、函數指針...還有很多叫法,也可能這里的叫法都是錯誤的,或是不准確的,但是我個人覺得從功能上講,可以這么理解。不過為了對oc的尊敬,還是叫它block吧,block就是block。在其他語言中也有類似block的語法,像javascript的閉包,函數里面的函數,java中的代碼塊,c中的函數指針等。就好像,事先放一段代碼在這里,然后需要的時候回過頭來調用。我們知道,代碼執行是按順序調用的,也就是我們常說的面向過程。但是block可以反向調用。只不過block可以寫在函數里面,也可以說它是函數中的函數,它是比較特殊的函數。我們看下面的一個例子。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int i = 0;
        void (^block)() = ^(){
            NSLog(@"%i", i);
        };
        block();
    }
    return 0;
}

  block先放在i=0;的后面,但是它沒有立即執行,在使用block();后它才執行,這就是block。至於block本質,到后面會講到。

  第二個問題,為什么要用block?

  雖然知道了什么是block,但是我們為什么要用呢?什么情況下用呢?這個問題其實也很重要,要掌握他的應用場景,我們才能用好它。

  最常用的情況,網絡請求。當調用網絡的的API請求服務器的數據的時候,我們不知道什么時候才會完成,這個時候程序也不可能停下來等待請求完成再繼續運行,這樣肯定是不行的。在請求成功的時候,去執行一些操作,這個時候我們需要做到一件事,就是執行事先已經准備好的一段代碼。這種情況不正好與block吻合了嗎,block不就是來干這件事的嗎?有人會說,用代理也可以做到啊,沒錯,代理也可以實現這種場景需要的操作。但是block它的好處在於簡潔,代理以后再談。

  再舉一個例子,你寫了一個頁面,頁面上有按鈕可以點擊,但是點擊事件是用戶點了之后才觸法的,而你的響應事件想放在控制器中處理(遵循一下MVC設計模式)。這個時候,也可以用block。

  總的來說,當你的程序中需要用到回調的時候,用block會讓你思路清晰,代碼簡潔。

  第三個問題,如何用block?

  1.block的定義:void(^a)()=^(){};

=前面:
    void:返回值類型;
    ()():語法結構,第一個括號里面是block名字,第二個括號里面是參數列表,和c或java中參數列表寫法一樣;例如:int i,char c...
    ^:脫字符,block標識
    a:block名稱,就像函數名,對象實例化名稱,說白了就是一個名字;
=號后面:
    ^是block標識;
    ()參數列表,當沒有參數時,可以省略,但是“=”前面的()參數列表不能省略;
    {}要執行的代碼放在這里面,最后加“;”

  最后調用,a();跟方法的調用何其相似。但是它和方法有一個非常明顯的區別,方法可以在當前類中(或是在main函數中)都可以調用,但是block不同,它出了它所在的作用域就不能被調用了。就像變量一樣,說到這里,你是不是想到了獎block設為全局。沒錯,下面我們使用一下全局block。

  2.全局block

void(^globeBlock)(int, int);
void max(int, int);
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        globeBlock = ^(int i, int j){
            printf("%d\n", i>j?i:j);
        };
        max(1, 2);
    }
    return 0;
}
void max(int i, int j){
    globeBlock(i,j);
}

  上面的代碼中,我們可以看到,聲明了一個全局的block,在main函數中實現,在max方法中調用。這樣就可以讓block在全局中都能調用,但是一定要實現block,否則會報錯。全局block原理和變量一樣,這里不多說了。有全局block,那么有沒有靜態block呢(別打我,我也是推測),試了一下,好像沒有,也可能我寫法不對。

  3.將block用作為對象的屬性。

  創建一個類Block,.h文件中:

typedef void (^block)();
@interface Block : NSObject
@property (nonatomic, assign) block myBlock;
- (void)start;

  .m文件中

- (void)start{
    if (self.myBlock) {
        self.myBlock();
    }
}

  在main函數中:

__block int i = 0;
Block *block = [Block new];
block.myBlock = ^{
    i++;
};
[block start];

  這種寫法是比較常用的,但是也會造成一些問題,例如:retain cycle,這個放在后面說。

  4.將block作為變量傳遞。

  還是用上面的類舉例。.h文件中:

- (void)show:(void (^)(NSInteger index))paramBlock;

  .m文件中

- (void)show:(void (^)(NSInteger index))paramBlock{
    paramBlock(1);
}

  main函數中調用:

[block show:^(NSInteger index) {
  NSLog(@"%li", index);
}];

  這樣就將block當作變量傳遞了,一些網絡請求庫里面會經常用這種方式。

  上面講到的有關block的概念都是一些比較基礎的東西,下面會講block更深層的理論。

  1.retain cycle,循環引用一直是一個老生常談的問題,我相信99.9的oc程序員都遇到過,並都能很好的解決,所以我這里也不贅述,簡單說一下。

  retain cycle在block中是怎么產生的。自從iOS引進了ARC之后,內存管理變得方便,但同時有些情況讓人頭疼。當對象A持有對象B的時候,A釋放,B會隨着A的釋放而釋放。那么問題來了,如果B也持有對象A,那么A會隨着B的釋放而釋放。很好,現在A、B都在等對方釋放,互相傷害啊,結果大家都不釋放,這樣便造成了循環引用。用weak關鍵字修飾,或是用其他的方法,都可以解決,這里不多說,沒多大意思了。

  2.不會形成retain cycle的block。像UIKit中UIView的block動畫它不會形成retain cycle,還有網絡庫AF中的回調也不會形成retain cycle。沒有形成retain cycle說明沒有互相強引用,UIView調用的是類方法,當前控制器不可能強引用一個類,所以並沒有造成retain cycle;至於AF里面怎么做到的,說實話我也不是很清楚,哈哈,還要研究一下。既然如此,那我們來寫寫不會造成循環引用的block。

  2.1block里面沒有引用當前控制器

  視圖控制器是TestViewController,有一個屬性testView,類TestView代碼如下:

.h
@interface TestView : UIView
{
    NSString *str;
}
@property (nonatomic, copy) void(^block)();
@end

.m
#import "TestView.h"

@implementation TestView
- (void)dealloc{
    NSLog(@"TestView 被釋放");
}
- (instancetype)initWithFrame:(CGRect)frame{
    if (self = [super initWithFrame:frame]) {
        self.backgroundColor = [UIColor blackColor];
        UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(click)];
        [self addGestureRecognizer:tap];
    }
    return self;
}

- (void)click{
    if (self.block) {
        self.block();
    }
}

@end

  TestViewController代碼如下:

- (void)dealloc{
    NSLog(@"TestViewController 被釋放");
}

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.view.backgroundColor = [UIColor whiteColor];
    self.testView = [[TestView alloc] initWithFrame:CGRectMake(100, 200, 50, 50)];
    self.testView.block = ^{
        NSLog(@"沒有引用self");
    };
    [self.view addSubview:self.testView];
    
}

  在block中,沒有引用self,所以當前控制器didDisAppear之后就銷毀了。

2017-07-07 11:16:33.556 noRetainBlock[1319:39876] TestViewController 被釋放
2017-07-07 11:16:33.557 noRetainBlock[1319:39876] TestView 被釋放

  會造成循環引用的情況:

  然后TestViewController和TestView都沒有被釋放,成功造成retain cycle。用weak修飾一下self就可以了,這里不說了。

  2.2類方法調用,block作為變量,TestView代碼如下:

.h
@interface TestView : UIView
@property (nonatomic, copy) void(^block)();
+ (void)show:(void(^)())myBlock;
@end

.m
#import "TestView.h"

@implementation TestView
- (void)dealloc{
    NSLog(@"TestView 被釋放");
}
- (instancetype)initWithFrame:(CGRect)frame{
    if (self = [super initWithFrame:frame]) {
        self.backgroundColor = [UIColor blackColor];
        UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(click)];
        [self addGestureRecognizer:tap];
    }
    return self;
}
- (void)click{
    if (self.block) {
        self.block();
    }
}
+ (void)show:(void (^)())myBlock{
    myBlock();
}
@end

  控制器中調用:

- (void)dealloc{
    NSLog(@"TestViewController 被釋放");
}

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.view.backgroundColor = [UIColor whiteColor];
    [TestView show:^{
        [self show];
    }];
}
- (void)show{
    NSLog(@"show");
}

  最后,控制器消失之后被釋放

2017-07-07 11:40:15.482 noRetainBlock[1608:52010] show
2017-07-07 11:40:17.784 noRetainBlock[1608:52010] TestViewController 被釋放

  不要問我,為什么testView沒有被釋放,因為根本就沒有創建這個對象,哈哈,被show了吧。還有一種寫法我試過了,也會造成retain cycle,將testView不設為控制器的屬性,這樣不會有警告,但是最終還是會造成循環引用,因為析構函數沒有被調用。

  以上兩種均可以避開循環引用,不過貌似第一種沒啥用。因為不在里面操作self的一些方法貌似這個block沒啥意義,說到底也只寫了一種。至於還有沒有其他的寫法,我還沒有研究。以后要是發現了會補上的。接下來,我們將看看block到底是什么東西。

  在看block源碼之前,我們先回到下面這段代碼,仔細看看會發現,i被__block修飾了。

__block int i = 0;
Block *block = [Block new];
block.myBlock = ^{
    i++;
};
[block start];

  那么不用__block修飾會是什么樣子呢?答案是:如果在block中修改局部變量i的值,那么編譯器會報錯,根本不能通過編譯,不過你要打印或是給其他變量賦值還是可以的。既然這樣,我們來看看上述寫法和下面寫法的兩種源碼,在終端cd到main函數所在文件夾,使用命令 clang -rewrite-objc main.m會得到main.cpp的文件。可以查看源碼。

  第一種,不用__block修飾,打印變量i:

int i = 0;
void(^block)() = ^{
    NSLog(@"%i", i);
};
block();

cpp文件中,末尾,因為大概有95000行代碼,只取最后部分代碼
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int i;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _i, int flags=0) : i(_i) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int i = __cself->i; // bound by copy
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_s1_pqp17thd18bdxjswsty3xkmh0000gn_T_main_e26a0c_mi_0, i);
   }
static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        int i = 0;
        void(*block)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, i));
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    return 0;
}

  源碼中__main_block_impl_0和__main_block_desc_0中的信息告訴我們block的本質了,它有一個isa指針,可以將block看成一個對象,但是不能說它就是一個對象。在上述源碼中可以看到,block中將變量i拷貝了一份,也就是說,block外面的i和里面的i是兩個變量。就好像實參和行參的關系。行參將實參拷貝了一份,放在內存中。oc做了優化,不能直接改變局部變量i的值。但是用__block修飾變量i之后就可以改變了。這又是為啥呢,看下面的源碼(只看main中的):

__block int i = 0;
void(^block)() = ^{
    i++;
    NSLog(@"%i", i);
};
block();

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 
        __attribute__((__blocks__(byref))) __Block_byref_i_0 i = {(void*)0,(__Block_byref_i_0 *)&i, 0, sizeof(__Block_byref_i_0), 0};
        void(*block)() = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_i_0 *)&i, 570425344));
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    return 0;
}

  我們可以看到__attribute__((__blocks__(byref))) __Block_byref_i_0 i = {(void*)0,(__Block_byref_i_0 *)&i, 0, sizeof(__Block_byref_i_0), 0};被__block修飾過的變量i變成這樣了,然后在block中的i是(__Block_byref_i_0 *)&i,一個地址。原來,用__block修飾過的i傳入block里面時,是地址,所以可以改變i的值。

  其實還有一個變量的值是可以在block中修改的,全局變量,靜態變量。但是要注意,在改變變量的值的時候要注意循環引用的問題。

  總結:block是一個很有趣的東西,掌握它,弄清楚原理,才能更好的使用它。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM