iOS block 機制

本文要將block的以下機制关顷,并配合具體代碼詳細描述:

  • block 與 外部變量
  • block 的存儲域:棧塊议双、堆塊、全局塊

定義

塊與函數(shù)類似汞舱,只不過是直接定義在另一個函數(shù)里宗雇,和定義它的那個函數(shù)共享同一個范圍內(nèi)的東西。

訪問外部變量

堆塊內(nèi)部泌神,棧是紅燈區(qū)舞虱,堆是綠燈區(qū)矾兜。

根據(jù)塊的存儲位置,可將塊分為全局塊浑槽、棧塊配并、堆塊。這里先主要針對堆塊講解畸冲。

Block不允許修改外部變量的值。Apple這樣設計算行,應該是考慮到了block的特殊性苫耸,block也屬于“函數(shù)”的范疇褪子,變量進入block,實際就是已經(jīng)改變了作用域呀枢。在幾個作用域之間進行切換時笼痛,如果不加上這樣的限制缨伊,變量的可維護性將大大降低。又比如我想在block內(nèi)聲明了一個與外部同名的變量枷恕,此時是允許呢還是不允許呢紧唱?只有加上了這樣的限制漏益,這樣的情景才能實現(xiàn)。于是棧區(qū)變成了紅燈區(qū)铜犬,堆區(qū)變成了綠燈區(qū)轻庆。

幾種演算

  • block調(diào)用 基本數(shù)據(jù)類型
    {
        NSLog(@"\n--------------------block調(diào)用 基本數(shù)據(jù)類型---------------------\n");

        int a = 10;
        NSLog(@"block定義前a地址=%p", &a);
        void (^aBlock)() = ^(){
            NSLog(@"block定義內(nèi)部a地址=%p", &a);
        };
        NSLog(@"block定義后a地址=%p", &a);
        aBlock();
    }
    
    /*
     結果:
     block定義前a地址=0x7fff5bdcea8c
     block定義后a地址=0x7fff5bdcea8c
     block定義內(nèi)部a地址=0x7fa87150b850
     */
    
    /*
     流程:
     1. block定義前:a在棧區(qū)
     2. block定義內(nèi)部:里面的a是根據(jù)外面的a拷貝到堆中的余爆,不是一個a
     3. block定義后:a在棧區(qū)
     */
    
    {
        NSLog(@"\n--------------------block調(diào)用 __block修飾的基本數(shù)據(jù)類型---------------------\n");
        
        __block int b = 10;
        NSLog(@"block定義前b地址=%p", &b);
        void (^bBlock)() = ^(){
            b = 20;
            NSLog(@"block定義內(nèi)部b地址=%p", &b);
        };
        NSLog(@"block定義后b地址=%p", &b);
        NSLog(@"調(diào)用block前 b=%d", b);
        bBlock();
        NSLog(@"調(diào)用block后 b=%d", b);
    }
    
    /*
     結果:
     block定義前b地址=0x7fff5bdcea50
     block定義后b地址=0x7fa873b016d8
     調(diào)用block前 b=10
     block定義內(nèi)部b地址=0x7fa873b016d8
     調(diào)用block后 b=20
     */
    
    /*
     流程:
     1. 聲明 b 為 __block (__block 所起到的作用就是只要觀察到該變量被 block 所持有蛾方,就將“外部變量”在棧中的內(nèi)存地址放到了堆中上陕。)
     2. block定義前:b在棧中释簿。
     3. block定義內(nèi)部: 將外面的b拷貝到堆中硼莽,并且使外面的b和里面的b是一個。
     4. block定義后:外面的b和里面的b是一個偏螺。
     5. block調(diào)用前:b的值還未被修改矾瑰。
     6. block調(diào)用后:b的值在block內(nèi)部被修改殴穴。
     */
    
    {
        NSLog(@"\n--------------------block調(diào)用 指針---------------------\n");
        
        NSString *c = @"ccc";
        NSLog(@"block定義前:c=%@, c指向的地址=%p, c本身的地址=%p", c, c, &c);
        void (^cBlock)() = ^{
            NSLog(@"block定義內(nèi)部:c=%@, c指向的地址=%p, c本身的地址=%p", c, c, &c);
        };
        NSLog(@"block定義后:c=%@, c指向的地址=%p, c本身的地址=%p", c, c, &c);
        cBlock();
        NSLog(@"block調(diào)用后:c=%@, c指向的地址=%p, c本身的地址=%p", c, c, &c);
    }
    
    /*
     c指針本身在block定義中和外面不是一個采幌,但是c指向的地址一直保持不變震桶。
     1. block定義前:c指向的地址在堆中, c指針本身的地址在棧中磨取。
     2. block定義內(nèi)部:c指向的地址在堆中柴墩, c指針本身的地址在堆中(c指針本身和外面的不是一個江咳,但是指向的地址和外面指向的地址是一樣的)。
     3. block定義后:c不變爹土,c指向的地址在堆中胀茵, c指針本身的地址在棧中挟阻。
     4. block調(diào)用后:c不變呵哨,c指向的地址在堆中孟害, c指針本身的地址在棧中挪拟。
     */

    {
        NSLog(@"\n--------------------block調(diào)用 指針并修改值---------------------\n");
        
        NSMutableString *d = [NSMutableString stringWithFormat:@"ddd"];
        NSLog(@"block定義前:d=%@, d指向的地址=%p, d本身的地址=%p", d, d, &d);
        void (^dBlock)() = ^{
            NSLog(@"block定義內(nèi)部:d=%@, d指向的地址=%p, d本身的地址=%p", d, d, &d);
            d.string = @"dddddd";
        };
        NSLog(@"block定義后:d=%@, d指向的地址=%p, d本身的地址=%p", d, d, &d);
        dBlock();
        NSLog(@"block調(diào)用后:d=%@, d指向的地址=%p, d本身的地址=%p", d, d, &d);
    }
    
    /*
     d指針本身在block定義中和外面不是一個玉组,但是d指向的地址一直保持不變。
     在block調(diào)用后朝巫,d指向的堆中存儲的值發(fā)生了變化石景。
     */
    
    {
        NSLog(@"\n--------------------block調(diào)用 __block修飾的指針---------------------\n");
        
        __block NSMutableString *e = [NSMutableString stringWithFormat:@"eee"];
        NSLog(@"block定義前:e=%@, e指向的地址=%p, e本身的地址=%p", e, e, &e);
        void (^eBlock)() = ^{
            NSLog(@"block定義內(nèi)部:e=%@, e指向的地址=%p, e本身的地址=%p", e, e, &e);
            e = [NSMutableString stringWithFormat:@"new-eeeeee"];
        };
        NSLog(@"block定義后:e=%@, e指向的地址=%p, e本身的地址=%p", e, e, &e);
        eBlock();
        NSLog(@"block調(diào)用后:e=%@, e指向的地址=%p, e本身的地址=%p", e, e, &e);
    }
    
    /*
     從block定義內(nèi)部使用__block修飾的e指針開始潮孽,e指針本身的地址由棧中改變到堆中往史,即使出了block,也在堆中挨决。
     在block調(diào)用后订歪,e在block內(nèi)部重新指向一個新對象,e指向的堆中的地址發(fā)生了變化陌粹。
     */
    
    {
        NSLog(@"\n--------------------block調(diào)用 retain cycle---------------------\n");
        
        View *v = [[View alloc] init];
        v.tag = 1;
        v.frame = CGRectMake(100, 100, 100, 100);
        [self.view addSubview:v];      //self->view->v
        void (^block)() = ^{
            v.backgroundColor = [UIColor orangeColor]; //定義內(nèi)部:block->v
        };
        v.block = block;    //v->block
        block();   
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            //預計3秒后釋放v對象。
            [v removeFromSuperview];
        });
    }
    
    /*
     結果:
     不會輸出 dealloc.
     */
    
    /*
     流程:
     1. self->view->v
     2. block定義內(nèi)部:block->v 因為block定義里面調(diào)用了v
     3. v->block
     
     結論:
     引起循環(huán)引用的是block->v->block或舞,切斷其中一個線即可解決循環(huán)引用映凳,跟self->view->v這根線無關
     */
    
    {
        NSLog(@"\n--------------------block調(diào)用self---------------------\n");
        
        View *v = [[View alloc] init];
        v.tag = 2;
        v.frame = CGRectMake(100, 220, 100, 100);
        [self.view addSubview:v];      //self->view->v
        void (^block)() = ^{
            self.view.backgroundColor = [UIColor redColor]; //定義內(nèi)部:block->self
            _count ++;   //調(diào)用self的實例變量诈豌,也會讓block強引用self。
            
        };
        v.block = block;    //v->block
        block();
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            //預計3秒后釋放self這個對象彤蔽。
            AppDelegate *appDelegate = [UIApplication sharedApplication].delegate;
            appDelegate.window.rootViewController = nil;
        });
    }

    /*
     結果:
     不會輸出 dealloc.
     */
    
    /*
     流程:
     1. self->view->v
     2. v->block
     3. block->self 因為block定義里面調(diào)用了self
     
     結論:
     在block內(nèi)引用實例變量庙洼,該實例變量會被block強引用油够。
     引起循環(huán)引用的是self->view->v->block->self,切斷一個線即可解決循環(huán)引用揩悄。
     */

棧塊鬼悠、堆塊厦章、全局塊

塊本身也是對象,由isa指針、塊對象正常運轉所需的信息群发、捕獲到的變量組成发乔。
根據(jù)Block創(chuàng)建的位置不同,Block有三種類型起愈,創(chuàng)建的Block對象分別會存儲到棧抬虽、堆纵菌、全局數(shù)據(jù)區(qū)域咱圆。

block_storage.png

上面講了塊會把它所捕獲的所有變量都拷貝一份功氨,這些拷貝放在 descriptor 變量后面捷凄,捕獲了多少個變量围来,就要占據(jù)多少內(nèi)存空間。請注意钦铁,拷貝的并不是對象本身才漆,而是指向這些對象的指針變量醇滥。

1. 在全局數(shù)據(jù)區(qū)的Block對象

    {
        NSLog(@"\n--------------------block的存儲域 全局塊---------------------\n");
        
        void (^blk)(void) = ^{
            NSLog(@"Global Block");
        };
        blk();
        NSLog(@"%@", [blk class]);
    }
    /*
     結果:輸出 __NSGlobalBlock__
     */
    
    /*
     結論:
     全局塊:這種塊不會捕捉任何狀態(tài)(外部的變量)鸳玩,運行時也無須有狀態(tài)來參與。塊所使用的整個內(nèi)存區(qū)域颓帝,在編譯期就已經(jīng)確定窝革。
     全局塊一般聲明在全局作用域中虐译。但注意有種特殊情況,在函數(shù)棧上創(chuàng)建的block侮攀,如果沒有捕捉外部變量兰英,block的實例還是會被設置在程序的全局數(shù)據(jù)區(qū)蚪腐,而非棧上。
     */

2. 在堆上創(chuàng)建的Block對象

    {
        NSLog(@"\n--------------------block的存儲域 堆塊---------------------\n");
        
        int i = 1;
        void (^blk)(void) = ^{
            NSLog(@"Malloc Block, %d", i);
        };
        blk();
        NSLog(@"%@", [blk class]);
    }
    /*
     結果:輸出 __NSMallocBlock__
     */
    
    /*
     結論:
     堆塊:解決塊在棧上會被覆寫的問題家制,可以給塊對象發(fā)送copy消息將它拷貝到堆上颤殴。復制到堆上后,塊就成了帶引用計數(shù)的對象了杈绸。
     
     在ARC中瞳脓,以下幾種情況棧上的Block會自動復制到堆上:
     - 調(diào)用Block的copy方法
     - 將Block作為函數(shù)返回值時(MRC時此條無效澈侠,需手動調(diào)用copy)
     - 將Block賦值給__strong修飾的變量時(MRC時此條無效)
     - 向Cocoa框架含有usingBlock的方法或者GCD的API傳遞Block參數(shù)時
     
     上述代碼就是在ARC中哨啃,block賦值給__strong修飾的變量,并且捕獲了外部變量审姓,block就會自動復制到堆上祝峻。
     */

3. 在棧上創(chuàng)建的Block對象

    {
        NSLog(@"\n--------------------block的存儲域 棧塊---------------------\n");
        int i = 1;
        __weak void (^blk)(void) = ^{
            NSLog(@"Stack Block, %d", i);
        };
        blk();
        NSLog(@"%@", [blk class]);
    }
    /*
     結果:輸出 __NSStackBlock__
     */
    
    /*
     結論:
     棧塊:塊所占內(nèi)存區(qū)域分配在棧中莱找,編譯器有可能把分配給塊的內(nèi)存覆寫掉宋距。
     在ARC中症脂,除了上面四種情況诱篷,并且不在global上,block是在棧中闸盔。
     */

內(nèi)存泄漏

堆塊訪問外部變量時會拷貝一份指針到堆中琳省,相當于強引用了指針所指的值。如果該對象又直接或間接引用了塊击费,就出現(xiàn)了循環(huán)引用蔫巩。
解決方法:要么在捕獲時使用__weak解除引用,要么在執(zhí)行完后置nil解除引用(使用后置nil的方式垃瞧,如果未執(zhí)行个从,則仍會內(nèi)存泄漏)截粗。

  • 注意:使用__block并不能解決循環(huán)引用問題绸罗。

優(yōu)缺點

優(yōu)點:

  • 捕獲外部變量
  • 降低代碼分散程度

缺點:

  • 循環(huán)引用引起內(nèi)存泄露

總結

  • 在block內(nèi)部意推,棧是紅燈區(qū),堆是綠燈區(qū)珊蟀。

  • 在block內(nèi)部使用的是將外部變量的拷貝到堆中的(基本數(shù)據(jù)類型直接拷貝一份到堆中菊值,對象類型只將在棧中的指針拷貝到堆中并且指針所指向的地址不變。)

  • __block修飾符的作用:是將block中用到的變量育灸,拷貝到堆中腻窒,并且外部的變量本身地址也改變到堆中。

  • 循環(huán)引用:分析實際的引用關系磅崭,block中直接引用self也不一定會造成循環(huán)引用儿子。

  • __block不能解決循環(huán)引用,需要在block執(zhí)行尾部將變量設置成nil(但問題很多砸喻,比如block永遠不執(zhí)行,外面變量變了里面也變割岛,里面變了外面也變等問題)

  • __weak可以解決循環(huán)引用愉适,block在捕獲weakObj時,會對weakObj指向的對象進行弱引用癣漆。

  • 使用__weak時维咸,可在block開始用局部__strong變量持有,以免block執(zhí)行期間對象被釋放。

  • 塊的存儲域:全局塊癌蓖、棧塊瞬哼、堆塊

  • 全局塊不引用外部變量,所以不用考慮费坊。

  • 堆塊引用的外部變量倒槐,不是原始的外部變量,是拷貝到堆中的副本附井。

  • 棧塊本身就在棧中讨越,引用外部變量不會拷貝到堆中。

參考

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末永毅,一起剝皮案震驚了整個濱河市把跨,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌沼死,老刑警劉巖着逐,帶你破解...
    沈念sama閱讀 216,591評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異意蛀,居然都是意外死亡耸别,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評論 3 392
  • 文/潘曉璐 我一進店門县钥,熙熙樓的掌柜王于貴愁眉苦臉地迎上來秀姐,“玉大人,你說我怎么就攤上這事若贮∈∮校” “怎么了?”我有些...
    開封第一講書人閱讀 162,823評論 0 353
  • 文/不壞的土叔 我叫張陵谴麦,是天一觀的道長蠢沿。 經(jīng)常有香客問我,道長匾效,這世上最難降的妖魔是什么舷蟀? 我笑而不...
    開封第一講書人閱讀 58,204評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮面哼,結果婚禮上雪侥,老公的妹妹穿的比我還像新娘。我一直安慰自己精绎,他們只是感情好,可當我...
    茶點故事閱讀 67,228評論 6 388
  • 文/花漫 我一把揭開白布锌妻。 她就那樣靜靜地躺著代乃,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上搁吓,一...
    開封第一講書人閱讀 51,190評論 1 299
  • 那天原茅,我揣著相機與錄音,去河邊找鬼堕仔。 笑死擂橘,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的摩骨。 我是一名探鬼主播通贞,決...
    沈念sama閱讀 40,078評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼恼五!你這毒婦竟也來了昌罩?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 38,923評論 0 274
  • 序言:老撾萬榮一對情侶失蹤灾馒,失蹤者是張志新(化名)和其女友劉穎茎用,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體睬罗,經(jīng)...
    沈念sama閱讀 45,334評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡轨功,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,550評論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了容达。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片古涧。...
    茶點故事閱讀 39,727評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖董饰,靈堂內(nèi)的尸體忽然破棺而出蒿褂,到底是詐尸還是另有隱情,我是刑警寧澤卒暂,帶...
    沈念sama閱讀 35,428評論 5 343
  • 正文 年R本政府宣布丈甸,位于F島的核電站钢悲,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜愁憔,卻給世界環(huán)境...
    茶點故事閱讀 41,022評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望算谈。 院中可真熱鬧糠雨,春花似錦、人聲如沸奖亚。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽昔字。三九已至爆袍,卻和暖如春首繁,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背陨囊。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評論 1 269
  • 我被黑心中介騙來泰國打工弦疮, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人蜘醋。 一個月前我還...
    沈念sama閱讀 47,734評論 2 368
  • 正文 我出身青樓胁塞,卻偏偏與公主長得像,于是被迫代替她去往敵國和親压语。 傳聞我的和親對象是個殘疾皇子啸罢,可洞房花燭夜當晚...
    茶點故事閱讀 44,619評論 2 354

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

  • 前言 Blocks是C語言的擴充功能,而Apple 在OS X Snow Leopard 和 iOS 4中引入了這...
    小人不才閱讀 3,768評論 0 23
  • 《Objective-C高級編程》這本書就講了三個東西:自動引用計數(shù)无蜂、block伺糠、GCD,偏向于從原理上對這些內(nèi)容...
    WeiHing閱讀 9,810評論 10 69
  • 1: 什么是block斥季?1.0: Block的語法1.1: block編譯轉換結構1.2: block實際結構 2...
    iYeso閱讀 838評論 0 5
  • 目錄 Block底層解析什么是block训桶?block編譯轉換結構block實際結構block的類型NSConcre...
    tripleCC閱讀 33,180評論 32 388
  • Block是iOS開發(fā)中一種比較特殊的數(shù)據(jù)結構,它可以保存一段代碼酣倾,在合適的地方再調(diào)用舵揭,具有語法簡介、回調(diào)方便躁锡、編...
    飛魚灣閱讀 4,117評論 0 7