神奇的Block

神奇的Block

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

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

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

廢話不說,直接看例子.測試環(huán)境為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;
}

很簡單的一段代碼,執(zhí)行block之后結(jié)果是I'm just a block.但如果問你,這個block是什么類型的block,你會怎么回答?

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

第一步:打個斷點

第二步:打印isa

然后就能看到結(jié)果了:

看到結(jié)果,尼瑪居然是個全局塊,可是我明明是在棧上創(chuàng)建的一個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并未因為一個弱引用立即釋放.然后看看結(jié)果:


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

下面開始總結(jié)了.

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

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

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

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

再來總結(jié)一下第二個例子.之所以是一個堆塊,是因為編譯器為塊進行了copy操作(實質(zhì)上是調(diào)用_Block_copy函數(shù)).以下方式會讓塊從棧復(fù)制到堆上.

  • 調(diào)用Blockcopy實例方法.

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

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

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

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

  • Block作為函數(shù)返回值時

    例如:

    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的類 副本源的配置存儲域 復(fù)制效果
_NSConcreteStackBlock 從棧復(fù)制到堆
_NSConcreteGlobalBlock 程序的數(shù)據(jù)區(qū)域 什么也不做
_NSConcreteMallocBlock 引用計數(shù)增加

精神病進階

例子四:

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超出了作用域,在括號外已經(jīng)不能被使用了  
    blk([NSObject new]);
    blk([NSObject new]);
    blk([NSObject new]);
    
    return 0;
}

打印臺輸出結(jié)果:

可以看到,在超出了作用域后,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;
}

打印臺結(jié)果:


大相徑庭的結(jié)果.

在例子四中,Block中截獲了外部的自動變量,并且根據(jù)上面說過的結(jié)論,編譯器為我們調(diào)用了copy方法,這個Block是個堆塊.

我們將例子四稍加改寫,將塊改為棧塊(即不讓編譯器為我們調(diào)用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;
}

打印出來的結(jié)果和例子五一致.

從該例子得出的結(jié)論是:

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

基于這個結(jié)論,我們還可以得出,在ARC環(huán)境下,定義block類型的屬性時,可以用strong,并不是非得用copy才是正確的.

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

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

注意,即使你不使用self.object訪問實例變量,而是通過_object訪問,也同樣會造成循環(huán)引用.因為無論用什么形式訪問實例變量,經(jīng)過編譯后,最終都會轉(zhuǎn)換成self+變量內(nèi)存偏移的形式來進行訪問,還是會造成循環(huán)引用.

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

block本質(zhì)也是一個結(jié)構(gòu)體,截獲的對象會成為結(jié)構(gòu)體成員的一部分.

例如:

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語言中,結(jié)構(gòu)體不能含有附有__strong修飾的變量.因為編譯器不知道應(yīng)何時進行C語言結(jié)構(gòu)體的初始化和廢棄操作,不能很好的管理內(nèi)存.

而OC卻可以,它能夠準確的把握block從棧復(fù)制到堆以及堆上的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修飾的變量變成了一個結(jié)構(gòu)體(__Block_byref_a_0類型).至于這個結(jié)構(gòu)體又長什么樣,就不貼代碼了,只需知道a被包裝到了這個結(jié)構(gòu)體中,成為其中一個成員變量,其他成員變量描述了該結(jié)構(gòu)體的一些信息.

  • Block被拷貝到堆上的時候,附有__strong修飾的變量因為Block結(jié)構(gòu)體內(nèi)有強指針持有,使得該指針所指向的對象在作用域外還有引用計數(shù),因此存活著.
  • Block被拷貝到堆上的時候,被__block修飾的變量被包裝到了一個新的結(jié)構(gòu)體中,被block結(jié)構(gòu)體持有,該結(jié)構(gòu)體跟隨Block也被拷貝到堆上了.
  • 截獲的方式并不能修改截獲的變量本身,而__block修飾的方式卻可以,因為它本質(zhì)是復(fù)制了一份該變量.

根據(jù)結(jié)論,可以知道用__block修飾的方式也能夠避免循環(huán)引用.只要在塊中將需要避免循環(huán)引用的?變量置為nil.

如:

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

如果最后不置為nil,那么self持有block結(jié)構(gòu)體,block結(jié)構(gòu)體持有__block變量結(jié)構(gòu)體,__block變量結(jié)構(gòu)體持有self,只有在最后將_block變量結(jié)構(gòu)體中的self置空,才能手動破除循環(huán).這個方式比weak方式優(yōu)點的地方在于可控制對象的持有期間.

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,791評論 6 545
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,795評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,943評論 0 384
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 64,057評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,773評論 6 414
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 56,106評論 1 330
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,082評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,282評論 0 291
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,793評論 1 338
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,507評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,741評論 1 375
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,220評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,929評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,325評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,661評論 1 296
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,482評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,702評論 2 380

推薦閱讀更多精彩內(nèi)容