__block底層原理詳解

_我們先通過一個小場景,開始今天的主題.

在block內(nèi)部修改auto變量

有過開發(fā)經(jīng)驗的同學都知道,在block內(nèi)部是無法修改局部變量的,為什么不能修改呢?我們從底層一探究竟,我們把這段代碼轉成 C++ ,查看一下底層:

如圖所示,是無法修改的,那怎么樣才能在block內(nèi)部修改外部變量呢?有三種方法:
1:使用static修飾age
2:把age變成全局變量
3:使用__block修飾age
前兩種方法我們就不細說了,在Block如何捕獲外部變量一:基本數(shù)據(jù)類型中已經(jīng)說的很明白了,并且前兩種方法會一直保存在內(nèi)存中占用內(nèi)存.我們重點研究__block關鍵字.

一:__block的本質(zhì)

我們把使用__block修飾的age轉換為C++代碼,對比一下:

OC代碼和底層C++代碼對比

可以發(fā)現(xiàn):使用__block修飾的age底層被轉換成了一個__Block_byref_age_0對象,我們重點研究一下這個對象.我們找到main函數(shù)中聲明__block int age = 10這句代碼的底層代碼:
__block修飾的age的底層代碼

之前沒有使用__block修飾時,底層代碼就是int age = 10,使用了__block修飾后,會發(fā)現(xiàn)變化如此之大,我們對這句轉換后的代碼簡化一下:
簡化后的聲明代碼和結構體對應圖

ok,現(xiàn)在我們來梳理一下使用__block修飾的age變量和block的關系,為此,我截了一張 OC 代碼和轉換后的 C++ 代碼的對比圖,這樣能更清晰的展示他們之間的關系:

age 對象和blcok的關系

這張圖和清晰的展示了使用__block修飾的age變量和block的關系,我們思考一下在block內(nèi)部是如何修改age的值得,我們還是通過底層代碼查看:
block代碼塊對比

從截圖中可以看到,block內(nèi)部修改auto變量,是先通過參數(shù)傳遞進來的block找到age結構體,然后通過age結構體找到__forwarding成員,通過之前的分析已經(jīng)知道__forwarding存儲的指針指向age結構體自己,所以本質(zhì)上還是通過age結構體找到存儲auto變量值得age成員,然后修改成 20.

思考一:為什么蘋果要設計forwarding這種多此一舉的方式呢?
因為當block從棧上拷貝到堆上后,__block變量也會拷貝到堆上.這時就有兩份__block變量,一份棧上的,一份堆上的.而棧上的__block變量隨時可能銷毀,訪問時可能出現(xiàn)野指針情況,為了保證始終訪問同一份有效且安全的數(shù)據(jù),需要把棧上__blockforwarding指針指向堆上__block地址.這樣就能保證即使訪問棧上的__block變量也能獲取到堆上的變量值,如圖:

思考二:如果我們修改的外部變量是對象類型,它的底層是怎樣的呢?

對象類型和基本數(shù)據(jù)類型對比

通過對比我們發(fā)現(xiàn),對象類型也會包裝成一個結構體,并且這個結構體里面也會有一個成員存放auto變量的值.區(qū)別是對象類型的結構體里面多了copydispose兩個函數(shù),我們在Block如何捕獲外部變量二:對象類型里面已經(jīng)講過,因為對象類型會涉及到內(nèi)存管理問題.

思考三:如圖所示,我們在block內(nèi)部給外部沒有使用__block修飾的array類型的auto變量添加元素,會編譯成功嗎?


肯定會成功的,這里我們不要搞混淆了,我們給array添加元素,是使用這個地址,而不是修改這個地址,如果我們array = nil這樣才會報錯.

思考四:通過上述分析,我們知道age結構體中會有一個成員存儲auto變量的值,所以現(xiàn)在就有兩個age.一個是age 結構體,另一個是存儲變量值得age成員,那我們?nèi)绻蛴?code>age的地址,會是哪個age的地址呢?

真假age

為了研究清楚這個問題,我們把底層的C++的block結構體挪到我們的OC代碼中,代碼如下:

typedef void(^MYBlock)(void);
//impl結構體
struct __block_impl {
    void *isa;
    int Flags;
    int Reserved;
    void *FuncPtr;
};

//age結構體
struct __Block_byref_age_0 {
    void *__isa;
    struct __Block_byref_age_0 *__forwarding;
    int __flags;
    int __size;
    int age;
};
//Desc結構體
struct __main_block_desc_0 {
    size_t reserved;
    size_t Block_size;
    void (*copy)(void);
    void (*dispose)(void);
};
//block結構體
struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    struct __Block_byref_age_0 *age; // by ref
};

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //聲明age
        __block int age = 10;
    
        //聲明block
        MYBlock block = ^{
            age = 20;
            NSLog(@"%p",&age);
        };  
        struct __main_block_impl_0 *blockImpl = (__bridge struct __main_block_impl_0 *)block;
        //調(diào)用block
        block();
    }
    return 0;
}

這樣我們就實現(xiàn)了把Block類型轉換為struct __main_block_impl_0結構體,方便我們查看block結構體中成員變量的地址.

age結構體中的成員


我們在函數(shù)返回之前設置一個斷點,然后打印了一個地址0x100405998,我們從變量視圖中可以直接看到block結構體中的age結構體的地址是0x100405980,可以看出打印的地址和age結構體的地址并不相同.由此我們可以得出結論:打印的地址是age結構體中的age成員地址,并不是block內(nèi)部捕獲的age.
ok,我們來驗證我們剛剛得出的結論.

  • 驗證方法一:
    我們在變量視圖中看到age結構體的地址是0x100405980,而我們知道,結構體的地址也是結構體第一個成員的地址,由此我們可以手動計算出結構體中age成員的地址:
//age結構體
struct __Block_byref_age_0 {//0x100405980 age結構體地址
  void *__isa;// 占8字節(jié) 0x100405980 第一個成員的地址和age的地址相同
__Block_byref_age_0 *__forwarding;// 占8字節(jié) isa的地址:0x100405980 + isa所占用的8字節(jié) =  0x100405988
 int __flags;// 占4字節(jié)  __forwarding 的地址:0x100405988 + __forwarding 所占用的8字節(jié) = 0x100405990
 int __size;// 占4字節(jié)   __flags地址:0x100405990 + __flags 所占用的4字節(jié) = 0x100405994
 int age; // 占4字節(jié)  __size 地址:0x100405994 + __size 所占用的4字節(jié) = 0x100405998
};

可以看到,我們計算處理的地址0x100405998和打印出來的地址0x100405998是相同的,驗證了我們剛才得出的結論.

  • 驗證方法二:通過命令行打印地址:

    我們通過LLDB命令p/x &(blockImpl->age->age)打印出的age地址和NSLog輸出的地址是相同的,再一次驗證了我們剛才的結論.這張截圖中的地址和上一張圖中的地址不一樣,是因為這張圖是重新運行代碼截取的,變量的內(nèi)存地址已經(jīng)改變了,大家不用糾結這里.
    ?我們思考一下蘋果為什么這樣設計?因為蘋果要隱藏它內(nèi)部的實現(xiàn),我們在修改__block修飾的age的值時,從表面看會以為真的是在直接修改age的值,如果不了解底層實現(xiàn)的話,根本就不知道被__block修飾的age已經(jīng)被包裝成了一個對象,而我們實際修改的是age結構體中的age成員的值.

二:__block的內(nèi)存管理

我們在block如何捕獲對象類型的文章中已經(jīng)知道,如果block訪問的是對象類型的變量,那么__main_block_desc_0結構體中會增加copy,dispose兩個函數(shù)指針,這兩個函數(shù)會根據(jù)變量的修飾符(__strong,__weak,__unretained)進行相應的操作,形成強引用(retain)或者弱引用或者是釋放引用的變量.現(xiàn)在我們來研究一下,使用__block修飾的外部變量,在block內(nèi)部是如何管理的.

聲明一個__block修飾的age變量,并且在block內(nèi)部訪問它:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //聲明 age 變量
        __block int age = 10;

        //聲明block
        MYBlock myblock = ^{
            NSLog(@"%d",age);
        };
        //調(diào)用block
        myblock();
    }
    return 0;
}

然后我們轉成 C++ 代碼,看看block底層是如何管理的:

//desc結構體
static struct __main_block_desc_0 {
    size_t reserved;
    size_t Block_size;
    void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
    void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};

會發(fā)現(xiàn)同訪問了對象類型的變量一樣,__main_block_desc_0結構體中同樣有copy,dispose函數(shù),可能有人會有疑問,因為在block如何捕獲對象類型中我們說過,只有在訪問對象類型的auto變量時才會生成copy dispose.為什么訪問 __block int 時也會產(chǎn)生呢?
在上面我們說過,block在訪問__block修飾的變量時,其底層會被封裝成__Block_byref_age_0類型,在這個類型存在一個void *__isa成員,所以本質(zhì)上它就是一個對象類型.

Block內(nèi)部是如何管理使用__block修飾的變量的呢?

  • 在棧上的block,不會對__block產(chǎn)生強引用
  • 當block拷貝到堆上時
    1:會調(diào)用block內(nèi)部的copy函數(shù)
    2:copy函數(shù)內(nèi)部會調(diào)用__main_block_copy_0函數(shù)
    3:__main_block_copy_0會調(diào)用_Block_object_assign函數(shù)對__block變量形成強引用(return)
    也就是說,一旦_Block_object_assign調(diào)用,就會對block內(nèi)部的__block變量產(chǎn)生強引用
    如圖:

思考一下:__block int age = 10是存放在棧上的,而MYBlock myblock是存放在堆上的,我們使用堆上的地址去指向椑恚空間肯定是不行的,那block的內(nèi)部是如何處理這種情況的呢?
結論就是,當棧上的blcok拷貝到堆上時,會把__block修飾的變量一同拷貝到堆上,如圖:


而當block從堆中移除時:
1:調(diào)用block內(nèi)部的dispose函數(shù)
2:dispose函數(shù)會調(diào)用_Block_object_dispose函數(shù)
3:_Block_object_dipose函數(shù)會自動釋放引用的__block變量(release)
如圖:

思考一下,蘋果為什么會這么設計呢?其實這個也很好理解,因為使用__blcok修飾的變量,底層其實被封裝成了對象,而我們要在block中使用這個對象,就肯定要對這個對象的引用負責.

既然 block 訪問對象類型的變量和訪問使用 __block 修飾的變量都會增加copy,dispose函數(shù),那么他們之間有沒有區(qū)別呢?

他們之間的區(qū)別就是:
  • 如果使用__block修飾的變量,block內(nèi)部直接對其強引用
  • 如果是對象類型的變量,會根據(jù)變量的修飾符__weak , __strong來決定是否強引用

三:block訪問 __block 修飾的對象類型

到目前為止,我們講解了block訪問 基本數(shù)據(jù)類型 (int age) , __block 修飾的基本數(shù)據(jù)類型 (__block int age), 對象類型 NSObject *object三種情況,下面我們分析一下第四種情況: __blcok 修飾的對象類型.

int main(int argc, const char * argv[]) {
    @autoreleasepool {
    
        Person *person = [[Person alloc]init];
        
        __block Person *blockPerson = person;
        MYBlock block = ^{
            NSLog(@"%@",blockPerson);
        };
        block();
    }
    return 0;
}

我們創(chuàng)建一個Person類,然后使用__block修飾一個person對象,在block 中訪問,查看底層代碼如下:

// __block 底層對象
struct __Block_byref_blockPerson_0 {
  void *__isa;
__Block_byref_blockPerson_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
// __block 中強引用 Person 對象
 Person *__strong blockPerson;
};

// block 底層對象
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
// block中強引用 __Block_byref_blockPerson_0
  __Block_byref_blockPerson_0 *blockPerson; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_blockPerson_0 *_blockPerson, int flags=0) : blockPerson(_blockPerson->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

// block 代碼塊
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_blockPerson_0 *blockPerson = __cself->blockPerson; // bound by ref
            NSLog((NSString *)&__NSConstantStringImpl__var_folders_5t_pxd6sp5x6rl9gnk21q2q934h0000gn_T_main_ea4426_mi_0,(blockPerson->__forwarding->blockPerson));
        }

可以看到使用__block修飾的對象底層被封裝成了__Block_byref_blockPerson_0類型的對象,__Block_byref_blockPerson_0這個對象類型的結構體中有一個Person *__strong blockPerson成員,強引用著我們在main函數(shù)中創(chuàng)建的Person對象,而__main_block_impl_0中的blockPerson又強引用著__Block_byref_blockPerson_0對象,他們的關系如下圖:


我們稍作修改,在blockPerson添加__weak關鍵字:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
    
        Person *person = [[Person alloc]init];
        
        __block __weak Person *blockPerson = person;
        MYBlock block = ^{
            NSLog(@"%@",blockPerson);
        };
        block();
    }
    return 0;
}

查看底層代碼:

// __block 底層對象
struct __Block_byref_blockPerson_0 {
  void *__isa;
__Block_byref_blockPerson_0 *__forwarding;
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);
 void (*__Block_byref_id_object_dispose)(void*);
    //這里變成了弱引用,說明這里的引用情況取決于外部變量的修飾符
 Person *__weak blockPerson;
};
// block 底層對象
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
    //這里還是強引用,說明 __weak 關鍵字并不會影響這里
  __Block_byref_blockPerson_0 *blockPerson; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_blockPerson_0 *_blockPerson, int flags=0) : blockPerson(_blockPerson->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
// main 函數(shù)
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_blockPerson_0 *blockPerson = __cself->blockPerson; // bound by ref

            NSLog((NSString *)&__NSConstantStringImpl__var_folders_5t_pxd6sp5x6rl9gnk21q2q934h0000gn_T_main_4ce55f_mi_0,(blockPerson->__forwarding->blockPerson));
        }

查看底層代碼我們發(fā)現(xiàn),block內(nèi)部對于__Block_byref_blockPerson_0的引用沒有變化,但是__Block_byref_blockPerson_0中的blockPerson已經(jīng)從強引用變成了弱引用.如圖:


現(xiàn)在我們來驗證一下__Block_byref_blockPerson_0中的blockPersonPerson對象的強引用和弱引用兩種情況:

我們添加 __weak后再看看:

通過對比我們就驗證了__Block_byref_blockPerson_0中的blockPersonPerson對象的引用存在強引用和弱引用兩種情況,這兩種情況取決于Person對象的修飾符.
這里需要注意一點,__Block_byref_blockPerson_0中的blockPersonPerson對象的強引用只有在ARC環(huán)境下才會retain,在MRC環(huán)境下只會弱引用.如圖:

我們演示一下,我們把環(huán)境切換為 MRC ,然后把 block copy 到堆上:

?大家思考一下,如果把__block去掉,會怎么樣?
如果把__block去掉,blcok 就會對 person 產(chǎn)生強引用,在 block 釋放之前,person 是不會釋放的,因為去掉__block后,就沒有__Block_byref_blockPerson_0這個中間層,blcok 會直接強引用 person對象,如圖:

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末滥玷,一起剝皮案震驚了整個濱河市血当,隨后出現(xiàn)的幾起案子迁霎,更是在濱河造成了極大的恐慌桐绒,老刑警劉巖沉填,帶你破解...
    沈念sama閱讀 222,378評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件妄壶,死亡現(xiàn)場離奇詭異,居然都是意外死亡键耕,警方通過查閱死者的電腦和手機查乒,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,970評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來郁竟,“玉大人,你說我怎么就攤上這事由境∨锬叮” “怎么了?”我有些...
    開封第一講書人閱讀 168,983評論 0 362
  • 文/不壞的土叔 我叫張陵虏杰,是天一觀的道長讥蟆。 經(jīng)常有香客問我,道長纺阔,這世上最難降的妖魔是什么瘸彤? 我笑而不...
    開封第一講書人閱讀 59,938評論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮笛钝,結果婚禮上质况,老公的妹妹穿的比我還像新娘。我一直安慰自己玻靡,他們只是感情好结榄,可當我...
    茶點故事閱讀 68,955評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著囤捻,像睡著了一般臼朗。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,549評論 1 312
  • 那天视哑,我揣著相機與錄音绣否,去河邊找鬼。 笑死挡毅,一個胖子當著我的面吹牛蒜撮,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播慷嗜,決...
    沈念sama閱讀 41,063評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼淀弹,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了庆械?” 一聲冷哼從身側響起薇溃,我...
    開封第一講書人閱讀 39,991評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎缭乘,沒想到半個月后沐序,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,522評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡堕绩,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,604評論 3 342
  • 正文 我和宋清朗相戀三年策幼,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片奴紧。...
    茶點故事閱讀 40,742評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡特姐,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出黍氮,到底是詐尸還是另有隱情唐含,我是刑警寧澤,帶...
    沈念sama閱讀 36,413評論 5 351
  • 正文 年R本政府宣布沫浆,位于F島的核電站捷枯,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏专执。R本人自食惡果不足惜淮捆,卻給世界環(huán)境...
    茶點故事閱讀 42,094評論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望本股。 院中可真熱鬧攀痊,春花似錦、人聲如沸拄显。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,572評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽凿叠。三九已至涩笤,卻和暖如春嚼吞,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背蹬碧。 一陣腳步聲響...
    開封第一講書人閱讀 33,671評論 1 274
  • 我被黑心中介騙來泰國打工舱禽, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人恩沽。 一個月前我還...
    沈念sama閱讀 49,159評論 3 378
  • 正文 我出身青樓誊稚,卻偏偏與公主長得像,于是被迫代替她去往敵國和親罗心。 傳聞我的和親對象是個殘疾皇子里伯,可洞房花燭夜當晚...
    茶點故事閱讀 45,747評論 2 361

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