oc篇- Block底層原理(二)

上篇講到了Block其實就是oc對象

[toc]

本文主要講解Block值捕獲以及如何修改block捕獲的變量,說明下本文的目錄結(jié)構(gòu)(簡書竟然沒做目錄功能,有點失望菲嘴,給個簡書生成目錄的鏈接吧)

注意把示例中的這行代碼改成下面的形式饿自,否則無效

// @match        http://www.reibang.com/p/*

一、Block是如何捕獲變量的

1.1 局部變量

1.1.1 auto類型變量

1.1.1.1 基本數(shù)據(jù)類型

修改下main.m的代碼

#import <Foundation/Foundation.h>

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

發(fā)現(xiàn)打印的是

10

轉(zhuǎn)成C++代碼發(fā)現(xiàn)有些變化了

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int a; //變化1
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) { // 這也是c++的語法表示 將 參數(shù)`_a` 賦值給 變量`a`
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int a = __cself->a; // 變化3

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_s2_zmz_wcdj2px2kh6hm1zkcd780000gp_T_main_4fb442_mi_0,a);
        }


int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        int a = 10;
        void (*block) (void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));// 變化2
        a = 20;
        ((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
    }
    return 0;
}

變化1:發(fā)現(xiàn)block多了個int類型的變量a;
變化2:調(diào)用block的時候除了傳遞block本身外龄坪,把a的值也就是10傳遞給了里面的變量a
變化3:最后方法執(zhí)行的地方__main_block_func_0也是把block的變量a取出

1.1.1.2 對象數(shù)據(jù)類型

原來block的結(jié)構(gòu)還不是定的昭雌,會隨著擁有的變量改變內(nèi)存結(jié)構(gòu)

再舉個例子來進(jìn)一步理解下值捕獲,改造下代碼

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
    
       NSString *name  = @"jack";
        NSLog(@"進(jìn)入block前的地址----%p",name);
        void (^block) (void) = ^{
            NSLog(@"進(jìn)入block的地址----%@--%p",name,name);
        };
        name = @"rose";
        NSLog(@"修改name之后的地址----%p",name);
        block();
    }
    return 0;
}

輸出

進(jìn)入block前的地址----0x100001078
修改name之后的地址----0x1000010d8
進(jìn)入block的地址----jack--0x100001078

c++代碼健田,這次簡單寫下就是多了個*name屬性

struct __main_block_impl_0 {
 ...
  NSString *name;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSString *_name, int flags=0) : name(_name) {
...
  }
};

解釋下為什么是輸出jack

在進(jìn)入block前name地址為0x100001078烛卧,當(dāng)?shù)搅?code>__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, name))的時候相當(dāng)于把name的值也就是0x100001078給了__main_block_impl_0里面的name變量,當(dāng)再次name = @"rose";的時候之前的name的值已經(jīng)不是0x100001078,而是0x1000010d8了总放,所以后來當(dāng)調(diào)用的時候訪問的是__main_block_impl_0name變量的值0x100001078所存的值是jack

弄了一張圖幫助理解下

15341544298315.jpg

1.1.2 static類型變量

1.1.2.1 對象類型

接著我們再次修改下代碼

int main(int argc, const char * argv[]) {
    @autoreleasepool {
    
       static NSString *name  = @"jack";
        void (^block) (void) = ^{
            NSLog(@"----%@--",name);
        };
        name = @"rose";
        block();
    }
    return 0;
}

就是加了一個static

輸出

----rose--

這次的結(jié)果完全不一樣了呈宇,這又是為什么呢,繼續(xù)看c++的實現(xiàn)

struct __main_block_impl_0 {
    ...
    NSString **name;  // 1
    ...
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  NSString **name = __cself->name; // 3

NSLog((NSString *)&__NSConstantStringImpl__var_folders_s2_zmz_wcdj2px2kh6hm1zkcd780000gp_T_main_9d6d5e_mi_1,(*name));//4
        }

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

       static NSString *name = (NSString *)&__NSConstantStringImpl__var_folders_s2_zmz_wcdj2px2kh6hm1zkcd780000gp_T_main_9d6d5e_mi_0;

        void (*block) (void) = &__main_block_impl_0(__main_block_func_0, 
                                                &__main_block_desc_0_DATA,
                                                 &name, //2
                                                 570425344));
        name = (NSString *)&__NSConstantStringImpl__var_folders_s2_zmz_wcdj2px2kh6hm1zkcd780000gp_T_main_9d6d5e_mi_2;

    block->FuncPtr(block);
    }
    return 0;
}

看到代碼1處已經(jīng)是二重指針局雄,2處這個時候給的不是name指向的內(nèi)存地址甥啄,而是name變量的地址,3處在調(diào)用的時候取出的是指向name變量地址的內(nèi)存炬搭,而不是name所指向的內(nèi)存蜈漓,所以要想獲取name所指向的內(nèi)存,4處通過*name取值進(jìn)行傳參

也就是紅色箭頭的變化


15341552229497.jpg

1.2 全局變量

1.2.1 非static修飾的全局變量

1.2.1.1 對象類型

當(dāng)變量是全局的變量時


NSString *name  = @"jack";

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

發(fā)現(xiàn)也是輸出

---rose--

額宫盔,這又是怎么回事呢融虽,看源碼吧

NSString *name = (NSString *)&__NSConstantStringImpl__var_folders_s2_zmz_wcdj2px2kh6hm1zkcd780000gp_T_main_3b419d_mi_0;


struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {


            NSLog((NSString *)&__NSConstantStringImpl__var_folders_s2_zmz_wcdj2px2kh6hm1zkcd780000gp_T_main_3b419d_mi_2,name);
        }

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; 

        void (*block) (void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
        
        name = (NSString *)&__NSConstantStringImpl__var_folders_s2_zmz_wcdj2px2kh6hm1zkcd780000gp_T_main_3b419d_mi_3;

        block)->FuncPtr(block);
    }
    return 0;
}

我們可以發(fā)現(xiàn)這個時候block內(nèi)部并沒有像之前那樣生成一個同名的變量,也就是對于全局變量block是不會捕獲的,當(dāng)變量是static全局變量時也和全局變量一樣灼芭,留給讀者自行測試了

1.2.1.2 基本類型

略...

1.2.2 static修飾的全局變量

略... 讀者自行寫demo

說了這么多我們來梳理下有额,我們思考一個問題,block為什么要捕獲變量姿鸿,是因為里面有個方法谆吴,方法需要使用變量,

  1. 如果是局部變量的話苛预,如果不持有他的話是不是過了作用域就釋放了句狼,那就不能完成方法的正常調(diào)用,所以對于局部變量热某,一定會捕獲的腻菇;
  2. 對于全局變量,剛才說了block捕獲變量的原因要使用變量昔馋,既然是全局變量筹吐,那在哪都可以訪問,所以不需要捕獲秘遏;
  3. 那為什么局部變量有的是傳地址有的是傳值呢丘薛,對于非static修飾的局部變量其實是auto的,這種變量是放在棧區(qū)的邦危,過了作用域就會被系統(tǒng)回收洋侨,如果block捕獲變量的地址的話,那可能捕獲的地址已經(jīng)被系統(tǒng)回收倦蚪,或者已經(jīng)被其他的對象占用了希坚,這個時候程序會出現(xiàn)無法預(yù)料的異常,但是如果是static修飾的陵且,是放在數(shù)據(jù)區(qū)的裁僧,不會隨著作用域的而銷毀,從而放地址是安全的

總結(jié)就是下面這張圖


15341565010715.jpg

二、 Block捕獲對對象的引用計數(shù)的影響

2.1 __NSMallocBlock__對對象的引用計數(shù)的影響 ARC環(huán)境

我們知道基本數(shù)據(jù)類型是放在棧中的聊疲,回收是由系統(tǒng)自動回收的無需考慮茬底,所以我們這里只考慮auto類型的對象類型的引用計數(shù)

需要新增一個MyPerson

//MyPerson.h
#import <Foundation/Foundation.h>

@interface MyPerson : NSObject

@end

//MyPerson.m

#import "MyPerson.h"

@implementation MyPerson

- (void)dealloc {
    NSLog(@"-%s",__func__);
}
@end

main.m代碼如下

int main(int argc, const char * argv[]) {
    @autoreleasepool {
  
        void (^myBlock) (void);
        {
            MyPerson *p = [[MyPerson alloc] init];
           myBlock = ^ {
                NSLog(@"----%@",p);
            };
            
            NSLog(@"block類型---%@",[myBlock class]);
        }
     
        NSLog(@"----");

    }
    return 0;
}


15351680937915.jpg

輸出

__NSMallocBlock__

這里p并沒有釋放掉,按道理過了120行應(yīng)該就釋放的获洲,其實此時MyPerson *p = [[MyPerson alloc] init];__strong MyPerson *p = [[MyPerson alloc] init];是等價的

說明__NSMallocBlock__類型的myBlock會對__strong修飾的p對象的引用計數(shù)產(chǎn)生影響

再修改main.m函數(shù)

15351682736470.jpg

此時輸出

__NSMallocBlock__
--[MyPerson dealloc]

發(fā)現(xiàn)p正常釋放了

說明__NSMallocBlock__類型的myBlock不會對__weak修飾的 p對象的引用計數(shù)產(chǎn)生影響


2.2 __NSStackBlock__對對象的引用計數(shù)的影響 MRC環(huán)境

把項目改成MRC

15351618498306.jpg

MyPerson

#import "MyPerson.h"

@implementation MyPerson

- (void)dealloc {
    [super dealloc];
    NSLog(@"-%s",__func__);
}
@end

mian.m如圖

15351615827663.jpg

此時打印

block類型---__NSStackBlock__
--[MyPerson dealloc]

發(fā)現(xiàn)p正常釋放了桩警,此時的myBlock的類型為__NSStackBlock__類型的,

說明__NSStackBlock__類型的myBlock不會對__strong修飾的 p對象的引用計數(shù)產(chǎn)生影響

再修改一下main.m的代碼如圖,新增的代碼看紅色處

會發(fā)現(xiàn)此時只會輸出

block類型---__NSMallocBlock__

但是p并沒有釋放

說明__NSMallocBlock__類型的myBlock會對__strong修飾的p對象的引用計數(shù)產(chǎn)生影響

2.3 結(jié)論

得出以下結(jié)論:

  1. 當(dāng)block內(nèi)部訪問了對象類型的auto變量時
    如果block是在棧上昌妹,將不會對auto變量產(chǎn)生強(qiáng)引用

  2. 如果block被拷貝到堆上
    會調(diào)用block內(nèi)部的copy函數(shù);
    copy函數(shù)內(nèi)部會調(diào)用_Block_object_assign函數(shù)_Block_object_assign函數(shù)會根據(jù)auto變量的修飾符(__strong、__weak握截、__unsafe_unretained)做出相應(yīng)的操作飞崖,形成強(qiáng)引用(retain)或者弱引用

  3. 如果block從堆上移除
    會調(diào)用block內(nèi)部的dispose函數(shù);
    dispose函數(shù)內(nèi)部會調(diào)用_Block_object_dispose函數(shù)_Block_object_dispose函數(shù)會自動釋放引用的auto變量(release)

三、__block如何做到可以修改變量的

由前面的了解我們知道block要想可以修改變量谨胞,那么就不能值捕獲固歪,也就是不能放在棧內(nèi)存中,因為棧內(nèi)存是的釋放無法控制胯努,所以要買放在全局區(qū)牢裳,要么放在堆區(qū)篷店,來看下蘋果是放在哪里的表蝙。

3.1 __block變量的內(nèi)存結(jié)構(gòu)

int main(int argc, const char * argv[]) {
    @autoreleasepool {
    
        MyPerson *p = [[MyPerson alloc] init];
        p.name = @"jack";
        
        void (^myBlock) (void) = ^ {
            p = [[MyPerson alloc] init];
            NSLog(@"---%@",p.name);
        };
        
        myBlock();
    }
    return 0;
}

像這樣在block的內(nèi)部直接修改變量是會報錯的, 要想修改需要借助__block修飾符

int main(int argc, const char * argv[]) {
    @autoreleasepool {
    
      __block  MyPerson *p = [[MyPerson alloc] init];
        p.name = @"jack";
        
        void (^myBlock) (void) = ^ {
            p = [[MyPerson alloc] init];
            NSLog(@"---%@",p.name);
        };
        
        myBlock();
    }
    return 0;
}

我們看看轉(zhuǎn)成的c++的代碼

15351801025148.jpg

可以看到和之前沒有__block修飾的不同的是這次的p變成了類型為__Block_byref_p_0 *p
再看初始化的地方

15351802434170.jpg

一行__block MyPerson *p = [[MyPerson alloc] init];就變成了初識化一個__Block_byref_p_0類型的結(jié)構(gòu)體恐仑,然后把該結(jié)構(gòu)體的指針給到myBlock灰署,而我們初始化的那個p則給了__Block_byref_p_0內(nèi)部的p對象

3.2 __block變量的內(nèi)存管理

  1. 當(dāng)__block變量在棧上時判帮,不會對指向的對象產(chǎn)生強(qiáng)引用

  2. 當(dāng)__block變量被copy到堆時
    會調(diào)用__block變量內(nèi)部的copy函數(shù),copy函數(shù)內(nèi)部會調(diào)用_Block_object_assign函數(shù)_Block_object_assign函數(shù)會根據(jù)所指向?qū)ο蟮男揎椃╛_strong溉箕、__weak晦墙、__unsafe_unretained)做出相應(yīng)的操作,形成強(qiáng)引用(retain)或者弱引用(MRC除外肴茄,MRC時不會retain)

  3. 如果__block變量從堆上移除
    會調(diào)用__block變量內(nèi)部的dispose函數(shù)晌畅,dispose函數(shù)內(nèi)部會調(diào)用_Block_object_dispose函數(shù)
    _Block_object_dispose函數(shù)會自動釋放指向的對象(release)

和沒有__block修飾的auto對象變量差不多,只是第二條中對MRC不起作用

15351806310781.jpg
15351808003801.jpg

這里的Block0相當(dāng)于myBlock寡痰,__block變量就是p,15行的時候變量MyPerson類型的變量p抗楔,就變成了指向__Block_byref_p_0類型的指針了,且處于棧中氓癌,到了21行結(jié)束谓谦,由于是arc環(huán)境,myBlock就是為右邊的blockcopy后的處于上了,這是變量p也會被拷貝到堆上,當(dāng)23執(zhí)行的時候調(diào)用的就是堆上的block贪婉,訪問的也是堆上的內(nèi)容反粥,對于block內(nèi)部的NSLog(@"---%@",p.name);則是結(jié)構(gòu)體p內(nèi)部的MyPerson類型的p對象

3.3 __block的__forwarding指針

可以看到__Block_byref_p_0結(jié)構(gòu)如下

15351812044975.jpg

有一個__forwarding,在main.m中初始化的時候傳的就是__Block_byref_p_0自身的地址,取值的時候也是通過p->__forwarding->p去取值豈不是多此一舉才顿?

15351812636865.jpg

15351813367663.jpg

其實這是不管當(dāng)__block修飾的結(jié)構(gòu)體變量莫湘,處于棧上還是被復(fù)制到堆上,都可以訪問到同一個p變量

我們把代碼改下

15351815132066.jpg

此時會輸出rose

代碼過了21行郑气,myBlock就已經(jīng)在上了幅垮,到了23行訪問還是棧中的結(jié)構(gòu)體變量,那為何還是打印rose呢尾组,就是因為有了__forwarding指針的作用忙芒,保證了此時不管在中還是在中都可以訪問到同一個MyPerson類型的變量p,當(dāng)__block修飾的變量從棧中copy到堆中的時候發(fā)送的事情入下圖

15351817089007.jpg

可以看出蘋果的實現(xiàn)是通過把變量放在堆區(qū)的方式來實現(xiàn)修改__block捕獲的變量的讳侨,也可以看出__block對對象的內(nèi)存影響還是蠻大的呵萨。

(完)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市跨跨,隨后出現(xiàn)的幾起案子潮峦,更是在濱河造成了極大的恐慌,老刑警劉巖勇婴,帶你破解...
    沈念sama閱讀 211,743評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件忱嘹,死亡現(xiàn)場離奇詭異,居然都是意外死亡耕渴,警方通過查閱死者的電腦和手機(jī)拘悦,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,296評論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來橱脸,“玉大人窄做,你說我怎么就攤上這事∥考迹” “怎么了椭盏?”我有些...
    開封第一講書人閱讀 157,285評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長吻商。 經(jīng)常有香客問我掏颊,道長,這世上最難降的妖魔是什么艾帐? 我笑而不...
    開封第一講書人閱讀 56,485評論 1 283
  • 正文 為了忘掉前任乌叶,我火速辦了婚禮,結(jié)果婚禮上柒爸,老公的妹妹穿的比我還像新娘准浴。我一直安慰自己,他們只是感情好捎稚,可當(dāng)我...
    茶點故事閱讀 65,581評論 6 386
  • 文/花漫 我一把揭開白布乐横。 她就那樣靜靜地躺著求橄,像睡著了一般。 火紅的嫁衣襯著肌膚如雪葡公。 梳的紋絲不亂的頭發(fā)上罐农,一...
    開封第一講書人閱讀 49,821評論 1 290
  • 那天,我揣著相機(jī)與錄音催什,去河邊找鬼涵亏。 笑死,一個胖子當(dāng)著我的面吹牛蒲凶,可吹牛的內(nèi)容都是我干的气筋。 我是一名探鬼主播,決...
    沈念sama閱讀 38,960評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼旋圆,長吁一口氣:“原來是場噩夢啊……” “哼裆悄!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起臂聋,我...
    開封第一講書人閱讀 37,719評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎或南,沒想到半個月后孩等,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體肄方,經(jīng)...
    沈念sama閱讀 44,186評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡权她,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,516評論 2 327
  • 正文 我和宋清朗相戀三年隅要,在試婚紗的時候發(fā)現(xiàn)自己被綠了步清。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片虏肾。...
    茶點故事閱讀 38,650評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡封豪,死狀恐怖吹埠,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情雌续,我是刑警寧澤,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布受啥,位于F島的核電站滚局,受9級特大地震影響藤肢,放射性物質(zhì)發(fā)生泄漏嘁圈。R本人自食惡果不足惜最住,卻給世界環(huán)境...
    茶點故事閱讀 39,936評論 3 313
  • 文/蒙蒙 一怠惶、第九天 我趴在偏房一處隱蔽的房頂上張望策治。 院中可真熱鬧通惫,春花似錦、人聲如沸檩电。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,757評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽烹卒。三九已至,卻和暖如春逢勾,著一層夾襖步出監(jiān)牢的瞬間溺拱,已是汗流浹背谣辞。 一陣腳步聲響...
    開封第一講書人閱讀 31,991評論 1 266
  • 我被黑心中介騙來泰國打工句占, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留躯嫉,地道東北人。 一個月前我還...
    沈念sama閱讀 46,370評論 2 360
  • 正文 我出身青樓擂啥,卻偏偏與公主長得像,于是被迫代替她去往敵國和親舱痘。 傳聞我的和親對象是個殘疾皇子芭逝,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,527評論 2 349

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

  • 前言 Blocks是C語言的擴(kuò)充功能旬盯,而Apple 在OS X Snow Leopard 和 iOS 4中引入了這...
    小人不才閱讀 3,759評論 0 23
  • block.png iOS代碼塊Block 概述 代碼塊Block是蘋果在iOS4開始引入的對C語言的擴(kuò)展,用來實...
    全棧農(nóng)民工閱讀 588評論 0 1
  • 上一篇文章iOS底層原理總結(jié) - 探尋block的本質(zhì)(一)中已經(jīng)介紹過block的底層本質(zhì)實現(xiàn)以及了解了變量的捕...
    xx_cc閱讀 10,641評論 8 54
  • 談職業(yè)這個問題切厘,對下學(xué)期讀大二的小譚來說,好像有點遠(yuǎn)培他,或者說舀凛,她自己還覺得有點遠(yuǎn)。 其實也不遠(yuǎn)了吧馋记,回想一下抗果,過去...
    不與周郎便閱讀 335評論 0 3
  • 1、有男人的獨立擔(dān)當(dāng)(兒子像爸爸這點) 2寄啼、為人善良(兒子像爸爸) 3、對人真誠 4涕刚、孝順父母(兒子像爸爸) 5杜漠、...
    麗人d閱讀 415評論 0 0