神奇的Block

本文不做Block的基本介紹和底層實現原理,有興趣的同學直接戳這篇文章,寫得灰常好,本文只在應用層面上帶領讀者進行思考,並整理出一些結論.這些結論是我從書上和上網資料收集所得,並通過實踐進行驗證而來,希望能和高手們共同探討 :)

在看例子之前,至少要知道block有幾個類型.

  • _NSConcreteGlobalBlock(全局塊)
  • _NSConcreteStackBlock(棧塊)
  • _NSConcreteMallocBlock(堆塊)

廢話不說,直接看例子.測試環境爲ARC,就不做MRC的測試了.

精神病入門

例子一:

typedef void (^blk_t) ();

int main(int argc, const char * argv[]) {

    blk_t block = ^{
        printf("I'm just a block\n");
    };
    block();

    return 0;
}

很簡單的一段代碼,執行block之後結果是I'm just a block.但如果問你,這個block是什麼類型的block,你會怎麼回答?

在代碼中打一個斷點,通過打印blockisa,可以知道該block是什麼類型的.

第一步:打個斷點


第二步:打印isa


然後就能看到結果了:


看到結果,尼瑪居然是個全局塊,可是我明明是在棧上創建的一個block呀!

再來看一個例子,這時定義了一個局部變量,並在block中使用了這個局部變量.

例子二:

typedef void (^blk_t) ();

int main(int argc, const char * argv[]) {
    int i = 1;
    blk_t block = ^{
        printf("%d\n",i);
    };
    block();

    return 0;
}

按照以上步驟再看看blockisa.


……我去,怎麼成堆塊了?

別急,再舉個�.

例子三:

typedef void (^blk_t) ();

int main(int argc, const char * argv[]) {
    int i = 1;
    __weak blk_t block = ^{
        printf("%d\n",i);
    };
    block();

    return 0;
}

雖然編譯器在__weak blk_t block = ^{這行爆出了警告,但是程序還是能夠正常運行.block並未因爲一個弱引用立即釋放.然後看看結果:



終於看到棧塊了,全家福終於齊人了.

下面開始總結了.

在哪些情況下,Block_NSConcreteGlobalBlock類對象?

  • 記述全局變量的地方創建的Block,比如下面的例子.

    blk_t block = ^{
        printf("I'm just a block");
    };
    int main(int argc, const char * argv[]) {
        block();
        return 0;
    }
  • 不截獲自動變量的時候.

    例子一這種情況下.雖然是在棧上創建的一個block,但由於閉包內不截獲外部的自動變量(局部變量),將會被編譯器編譯爲_NSConcreteGlobalBlock.

再來總結一下第二個例子.之所以是一個堆塊,是因爲編譯器爲塊進行了copy操作(實質上是調用_Block_copy函數).以下方式會讓塊從棧複製到堆上.

  • 調用Blockcopy實例方法.

    [^{
            printf("a heap block");
        } copy]; 
    // 對block調用copy,會把棧上的block複製到堆上.
  • Block賦值給附有__strong修飾符id類型的類或Block類型成員變量時.

    也就是說,有個__strong修飾的變量指向這個block就會讓編譯器爲block調用copy方法.

    例子二就是將Block賦值給了一個__strong(默認都是strong)修飾的Block類型成員變量——blk_t block.

    因此,在例子三中,我將強引用變成弱引用,創建了一個棧上的block.雖然編譯器會有警告,因爲編譯器在這裏可能還不知道那個塊也是棧上的,而這個棧上的塊,顯然不會立即釋放.

  • Block作爲函數返回值時

    例如:

    blk_t return_A_Block(){
        int val = 10;
        return ^{NSLog(@"%d",val);};
    }
    
    int main(int argc, const char * argv[]) {
        NSLog(@"%@",return_A_Block());
        return 0;
    }

    打印所得是一個堆塊.

Block的副本
Block的類 副本源的配置存儲域 複製效果
_NSConcreteStackBlock 從棧複製到堆
_NSConcreteGlobalBlock 程序的數據區域 什麼也不做
_NSConcreteMallocBlock 引用計數增加

精神病進階

例子四:

typedef void (^blk_t) (id obj);

int main(int argc, const char * argv[]) {
    blk_t blk;
    {
        id array = [[NSMutableArray alloc] init];
        blk = ^(id obj){
            [array addObject:obj];
            NSLog(@"%ld",[array count]);
        };
    }
   // array超出了作用域,在括號外已經不能被使用了  
    blk([NSObject new]);
    blk([NSObject new]);
    blk([NSObject new]);

    return 0;
}

打印臺輸出結果:


可以看到,在超出了作用域後,array依舊能夠被訪問到.

例子五:

int main(int argc, const char * argv[]) {
    blk_t blk;
    {
        id array = [[NSMutableArray alloc] init];
        id __weak array2 = array;
        blk = ^(id obj){
            [array2 addObject:obj];
            NSLog(@"%ld",[array2 count]);
        };
    }
    blk([NSObject new]);
    blk([NSObject new]);
    blk([NSObject new]);

    return 0;
}

打印臺結果:



大相徑庭的結果.

在例子四中,Block中截獲了外部的自動變量,並且根據上面說過的結論,編譯器爲我們調用了copy方法,這個Block是個堆塊.

我們將例子四稍加改寫,將塊改爲棧塊(即不讓編譯器爲我們調用copy方法):

int main(int argc, const char * argv[]) {
    __weak blk_t blk;
    {
        id array = [[NSMutableArray alloc] init];
        blk = ^(id obj){
            [array addObject:obj];
            NSLog(@"%ld",[array count]);
        };
    }
    blk([NSObject new]);
    blk([NSObject new]);
    blk([NSObject new]);

    return 0;
}

打印出來的結果和例子五一致.

從該例子得出的結論是:

  • 只有調用了Blockcopy方法,才能持有截獲的附有__strong修飾符的對象類型的自動變量值.

基於這個結論,我們還可以得出,在ARC環境下,定義block類型的屬性時,可以用strong,並不是非得用copy纔是正確的.

// 兩者效果一樣
@property (strong, nonatomic)     blk_t     *block;
@property (copy, nonatomic)     blk_t     *block;

在例子五中,雖然截獲的自動變量是__weak修飾符修飾的對象類型.但是作用域過後array被釋放,nil被賦值給了array2,並不能持有對象.這讓我們想起了平時爲了防止循環引用,我們會用一個弱指針指向self,並讓block捕獲弱指針而不是讓block持有self.

注意,即使你不使用self.object訪問實例變量,而是通過_object訪問,也同樣會造成循環引用.因爲無論用什麼形式訪問實例變量,經過編譯後,最終都會轉換成self+變量內存偏移的形式來進行訪問,還是會造成循環引用.

那麼,截獲和__block修飾有何不同呢?

block本質也是一個結構體,截獲的對象會成爲結構體成員的一部分.

例如:

int main(int argc, const char * argv[]) {
    id obj;
    ^{
        obj;
    };

    return 0;
}

其中生成的block會是這樣子的:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __Person__test_block_desc_0* Desc;
  id __strong obj;
};

注:在C語言中,結構體不能含有附有__strong修飾的變量.因爲編譯器不知道應何時進行C語言結構體的初始化和廢棄操作,不能很好的管理內存.

而OC卻可以,它能夠準確的把握block從棧複製到堆以及堆上的block被廢棄的時機.

如果是通過__block修飾的一個變量呢?

int main(int argc, const char * argv[]) {
    __block int a;
    ^{
        a = 10;
    };

    return 0;
}

其中生成的block會是這樣子的:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __Person__test_block_desc_0* Desc;
  __Block_byref_a_0 *a; // by ref
};

此時,被__block修飾的變量變成了一個結構體(__Block_byref_a_0類型).至於這個結構體又長什麼樣,就不貼代碼了,只需知道a被包裝到了這個結構體中,成爲其中一個成員變量,其他成員變量描述了該結構體的一些信息.

  • Block被拷貝到堆上的時候,附有__strong修飾的變量因爲Block結構體內有強指針持有,使得該指針所指向的對象在作用域外還有引用計數,因此存活着.
  • Block被拷貝到堆上的時候,被__block修飾的變量被包裝到了一個新的結構體中,被block結構體持有,該結構體跟隨Block也被拷貝到堆上了.
  • 截獲的方式並不能修改截獲的變量本身,而__block修飾的方式卻可以,因爲它本質是複製了一份該變量.

根據結論,可以知道用__block修飾的方式也能夠避免循環引用.只要在塊中將需要避免循環引用的變量置爲nil.

如:

- (id)init{
  self = [super init];
  __block id tmp = self;
  blk_t blk = ^{
    tmp.name = @"ye";
    tmp = nil;
  }
  return self;
}

如果最後不置爲nil,那麼self持有block結構體,block結構體持有__block變量結構體,__block變量結構體持有self,只有在最後將_block變量結構體中的self置空,才能手動破除循環.這個方式比weak方式優點的地方在於可控制對象的持有期間.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章