時代背景
受到培訓(xùn)碱屁、疫情、大廠裁員的多重打擊蛾找,IT行業(yè)現(xiàn)在極度內(nèi)卷娩脾,而 IT行業(yè)里iOS是當(dāng)之無愧的卷王之王。
沒啥好說的打毛,其他行業(yè)與iOS都不在一個維度里面柿赊,根本不與辯駁。
不適應(yīng)行業(yè)幻枉,就要重新選擇行業(yè)碰声。我選擇適應(yīng)行業(yè)。
為什么要研究
之所以選擇block進行研究熬甫,道理很簡單:之前對block沒有深入研究過奥邮。
因為block用起來很簡單,除了要考慮:
- 1. 變量前加__block可以在block內(nèi)部改變值
- 2. weak-strong搭配解決引用循環(huán)
之外罗珍,根本不用思考就可以輕松使用洽腺,而OC也是為了方便我們使用才設(shè)計出block。
初衷不是讓每個開發(fā)者都要「搞明白怎么實現(xiàn)block覆旱,才可以使用block蘸朋!」,只是很多人被迫營業(yè)了扣唱。
比如現(xiàn)在藕坯,大家都知道了而我們不知道,顯得逼格很低噪沙,所以非常需要多深入了解一點炼彪。
進行底層研究之前,關(guān)于clang查看cpp源碼文件正歼,可以看下: iOS 使用Clang命令失敗的解決
哪些方面值得研究
暫時總結(jié)出這幾點:
- block本質(zhì)是什么辐马,怎么實現(xiàn)?
能手寫做一下實現(xiàn)過程嗎- block有哪些類型局义,什么情況下會改變喜爷?
- block捕獲變量有哪幾種方式冗疮,__block對變量會做什么操作?
- block循環(huán)引用是常見的有哪些檩帐,怎么產(chǎn)生的术幔,怎么解決?
接下來的內(nèi)容是我查閱資料后湃密,加上了一些個人理解诅挑,簡明扼要的說出重點供各位同學(xué)參考。
1.「Block」的本質(zhì)與實現(xiàn)
從表層OC使用來說泛源,block本質(zhì)是個對象(Object)拔妥,擁有isa指針,可以當(dāng)作數(shù)據(jù)進行傳遞(例如放入NSDictionary中)俩由。
從底層C++實現(xiàn)來說毒嫡,block本質(zhì)是個結(jié)構(gòu)體(Struct)癌蚁,并且由多個不同作用的結(jié)構(gòu)體和函數(shù)構(gòu)成幻梯。
接下來,看一下函數(shù)內(nèi)創(chuàng)建的簡單block努释,執(zhí)行clang編譯前后的源碼對比:
OC
int main() { // 對應(yīng)找下面的main函數(shù)
@autoreleasepool {
void (^IIIIIIII)(void) = ^{ // block名稱是IIIIIIII
123456789; // 方便找clang后所在位置碘梢,如果是字符串會變成亂碼
};
}
return 0;
}
C++
struct __block_impl { // block對象
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
static struct __main_block_desc_0 { // block描述信息
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
struct __main_block_impl_0 { // block結(jié)構(gòu)體創(chuàng)建
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) { // 執(zhí)行方法
123456789;
}
int main() { // 對應(yīng)上面的main函數(shù)
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
void (*IIIIIIII)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0,
&__main_block_desc_0_DATA));
}
return 0;
}
思考:創(chuàng)建block結(jié)構(gòu)體的名稱「__main_block_impl_0」里的main,是不是與block所在的main函數(shù)關(guān)系呢伐蒂?
我們能夠發(fā)現(xiàn)名稱其實是「__xxx_block_impl_n」這樣的結(jié)構(gòu)煞躬,通過實踐得出:
- 如果是block在方法內(nèi)創(chuàng)建,則xxx為所在方法名逸邦,否則xxx為block自身名稱恩沛。
- 最后的數(shù)字n,在函數(shù)作用域內(nèi)有n個block時缕减,是這些block的自增id雷客,而函數(shù)外則一直為0。
可以看出C++的代碼風(fēng)格桥狡,讓做iOS開發(fā)的非常不適應(yīng):長長短短下劃線搅裙,分不清究竟是干啥的。
原因在于C的命名風(fēng)格是下劃線命名法裹芝,不是駝峰命名法的部逮,我們把這個方法名轉(zhuǎn)換一下試試看:
O-C++
上面代碼中相同顏色的方框,對應(yīng)的是相同的結(jié)構(gòu)名嫂易。按照顏色兄朋,推薦的理解順序是紅綠藍白。
轉(zhuǎn)變以后怜械,看起來是不是沒有那么陌生了呢蜈漓?其中的細節(jié)穆桂,我已經(jīng)在上面的代碼中逐行加了注釋。
我們能夠看出block的結(jié)構(gòu)中融虽,最得我們關(guān)注的四個結(jié)構(gòu):
- block對象 BlockObject ? __block_impl
- block創(chuàng)建 CreateBlock ? __main_block_impl_0
- block執(zhí)行 BlockAction ? __main_block_func_0
- block信息 BlockDesc ? __main_block_desc_0
合起來就是:對象-創(chuàng)建-執(zhí)行-信息享完。
其中,在block捕獲外部變量時有额,BlockDesc中會提供拷貝方法copy以及釋放方法dispose般又,后面再詳細說這里。
2.「Block」的類型
網(wǎng)上很多文章中提到的是這三種類型:
- _NSConcreteGlobalBlock「全局block」
- _NSConcreteStackBlock「棧block」
- _NSConcreteMallocBlock「堆block」
或者另外三種類型:
- __NSGlobalBlock __ 「全局block」
- __NSStackBlock __ 「棧block」
- __NSMallocBlock __ 「堆block」
但是我認為巍佑,拋開程序運行過程不提茴迁,單說這兩大類名稱都是不準(zhǔn)確的。
我們從OC打印和C++源碼中萤衰,可以看到:
O-Block:
打印__NSGlobalBlock堕义,源碼是_NSConcreteGlobalBlock
I-Block:
打印__NSGlobalBlock,源碼卻是_NSConcreteStackBlock
同一個block卻出現(xiàn)類型不同的情況脆栋,原因在于打印是運行時倦卖,源碼是編譯期。
所以椿争,我們應(yīng)該通過程序運行的不同時期進行區(qū)分怕膛,而不應(yīng)將兩類名稱進行混用。
編譯期名稱
- 函數(shù)外初始化的block均為_NSConcreteGlobalBlock
- 函數(shù)內(nèi)初始化的block均為_NSConcreteStackBlock
- 我們無法主動初始化_NSConcreteMallocBlock類型的block
運行時名稱
- _NSConcreteGlobalBlock在任何情況下秦踪,都會生成__NSGlobalBlock
- _NSConcreteStackBlock在沒有引入外部變量1的情況下褐捻,會轉(zhuǎn)為__NSGlobalBlock,在ARC下其他情況2都會自動拷貝到堆椅邓,轉(zhuǎn)為__NSMallocBlock
注釋:
1.引入外部變量:是指使用了外部變量柠逞,不論是用于打印還是作為參數(shù)。
2.其他情況:ARC下block任意使用場景都是堆狀態(tài)景馁,不必深究出現(xiàn)的樣式多少種板壮,底層來說只能算作是同一種情況。
至于為什么編譯的_NSConcreteStackBlock裁僧,運行時如何變?yōu)開_NSGlobalBlock个束,猜測可能是系統(tǒng)在運行時創(chuàng)建block過程中進行了判斷處理。
3.「Block」的變量捕獲
3.1 Block的變量捕獲有哪些方式聊疲?
研究什么是變量捕獲之前茬底,先列出我們常見的多種變量類型:全局變量、靜態(tài)變量获洲、普通數(shù)值變量阱表、OC對象以及帶上__block修飾的變量,在block的代碼塊中打印一下,看看結(jié)果:
#import <Foundation/Foundation.h>
#import <malloc/malloc.h>
int ivar_global_int = 11111;
char * ivar_global_char = "1";
int main() {
@autoreleasepool {
static int ivar_static_int = 2;
int ivar_int = 3;
NSArray * ivar_array = @[@"1"];
NSMutableArray * ivar_marray = [[NSMutableArray alloc] init];
NSObject * ivar_object = [[NSObject alloc] init];
__block int ivar_int_block = 4;
__block NSArray * ivar_array_block = @[@"2"];
__block NSMutableArray * ivar_marray_block = [[NSMutableArray alloc] init];
__block NSObject * ivar_object_block = [[NSObject alloc] init];
void (^IIIIIIIII)(void) = ^{
ivar_global_int = 0;
NSLog(@"ivar_global_int: %d %p", ivar_global_int, &ivar_global_int);
NSLog(@"ivar_global_char: %s %p", ivar_global_char, &ivar_global_char);
NSLog(@"ivar_static_int: %d %p", ivar_static_int, &ivar_static_int);
NSLog(@"ivar_int: %d %p", ivar_int, &ivar_int);
NSLog(@"ivar_int_block: %d %p", ivar_int_block, &ivar_int_block);
NSLog(@"ivar_array: _ %p", &ivar_array);
NSLog(@"ivar_array_block: _ %p", ivar_array_block);
NSLog(@"ivar_marray: _ %p", ivar_marray);
NSLog(@"ivar_marray_block: _ %p", ivar_marray_block);
NSLog(@"ivar_object: _ %p", ivar_object);
NSLog(@"ivar_object_block: _ %p", ivar_object_block);
};
IIIIIIIII();
}
return 0;
}
2022-05-06 17:16:48.131846+0800 BlockClangTest[20824:284328] ivar_global_int: 0 0x100008010
2022-05-06 17:16:48.132069+0800 BlockClangTest[20824:284328] ivar_global_char: 1 0x100008018
2022-05-06 17:16:48.132090+0800 BlockClangTest[20824:284328] ivar_static_int: 2 0x100008020
2022-05-06 17:16:48.132103+0800 BlockClangTest[20824:284328] ivar_int: 3 0x10072c7a8
2022-05-06 17:16:48.132113+0800 BlockClangTest[20824:284328] ivar_int_block: 4 0x10072c7c8
2022-05-06 17:16:48.132123+0800 BlockClangTest[20824:284328] ivar_array: _ 0x10072c770
2022-05-06 17:16:48.132133+0800 BlockClangTest[20824:284328] ivar_array_block: _ 0x100008050
2022-05-06 17:16:48.132200+0800 BlockClangTest[20824:284328] ivar_marray: _ 0x10072c6d0
2022-05-06 17:16:48.132269+0800 BlockClangTest[20824:284328] ivar_marray_block: _ 0x10072c720
2022-05-06 17:16:48.132289+0800 BlockClangTest[20824:284328] ivar_object: _ 0x1007291b0
2022-05-06 17:16:48.132305+0800 BlockClangTest[20824:284328] ivar_object_block: _ 0x1007288b0
Program ended with exit code: 0
思考:OC的內(nèi)存對齊最爬?
從打印的前三行涉馁,我們能夠看出基本數(shù)值類型的內(nèi)存對齊是 8 字節(jié):
0x100008018 - 0x100008010 = 8
0x100008020 - 0x100008018 = 8 (提示:十六進制下8需要加8才可以進位變成10)
而OC對象的內(nèi)存對齊是 16 字節(jié),上圖中看不出這個結(jié)論爱致。
不過我們可以實例化一個NSObject對象object烤送,通過打印 sizeof(object) 結(jié)果為 8 以及 malloc_size((__bridge const void *)object) 結(jié)果為 16 間接得出。
進行clang編譯后糠悯,變量相關(guān)的代碼量非常多帮坚,從中找出最關(guān)鍵的block創(chuàng)建結(jié)構(gòu)體指針的源碼,修改了一下對齊格式便于閱讀互艾,如下:
// 生成block結(jié)構(gòu)體指針
void (*IIIIIIIII)(void) = ((void (*)())&__main_block_impl_0
((void *)__main_block_func_0,
&__main_block_desc_0_DATA,
&ivar_static_int,
ivar_int,
ivar_array,
ivar_marray,
ivar_object,
(__Block_byref_ivar_int_block_0 *)&ivar_int_block,
(__Block_byref_ivar_array_block_1 *)&ivar_array_block,
(__Block_byref_ivar_marray_block_2 *)&ivar_marray_block,
(__Block_byref_ivar_object_block_3 *)&ivar_object_block,
570425344));
// 調(diào)用block方法 代碼就是: IIIIIIIII()
((void (*)(__block_impl *))((__block_impl *)IIIIIIIII)->FuncPtr)((__block_impl *)IIIIIIIII);
從這段源碼中能夠看出试和, 所謂block捕獲變量,不過是將這些變量作為參數(shù)傳遞到結(jié)構(gòu)體內(nèi)部纫普!
仔細看看阅悍,我們還能察覺到有些變量沒有參與這個block的創(chuàng)建,它們是全局變量昨稼。而參與創(chuàng)建的變量节视,有些帶有&符號表示指針訪問,還有一些不帶的表示值訪問悦昵。
另外肴茄,帶__block修飾的變量晌畅,變成了編譯器自動生成的新結(jié)構(gòu)體 __Block_byref_xxx_n 的實例變量但指,并且?guī)в?strong>&符號進行指針訪問。
這些差異充分說明不同類型的變量抗楔,被捕捉的方式是不同的棋凳。
總結(jié)一下,我們可以直觀的發(fā)現(xiàn)規(guī)律:
網(wǎng)上很多文章都只將局部變量分成了兩類:static變量和auto變量连躏,并將auto變量統(tǒng)歸為值訪問剩岳。但是,我認為這個說法不是很妥當(dāng)入热,在接下來的小節(jié)3.2中拍棕,我們可以探究一下這個問題。
3.2 __block對變量會做什么操作勺良?
從上一個小節(jié)「3.1 Block的變量捕獲有哪些方式绰播?」的第一個圖的代碼中,我們提取出一行帶__block的簡單代碼進行單獨分析尚困,這行代碼會編譯成 __Block_byref_ivar_int_block_0 結(jié)構(gòu)體和__attribute __ 開頭的兩段代碼蠢箩。
__block int ivar_int_block = 4; // 會編譯成下面的struct結(jié)構(gòu)體和__attribute__開頭的兩段代碼
我們想要修改一個變量的值時,通常我們會在前面帶上__block修飾符。普通auto變量被__block修飾后谬泌,會被包裝成一個新的結(jié)構(gòu)體滔韵,也可以看作為是一個新的OC對象:
// block新對象
struct __Block_byref_ivar_int_block_0 {
void *__isa; // isa指針
__Block_byref_ivar_int_block_0 *__forwarding; // 指向自身的指針 被copy到堆以后 指向堆對象內(nèi)存地址
int __flags; // 不太清楚干啥的
int __size; // 內(nèi)存大小
int ivar_int_block; // 攜帶的數(shù)值
};
這個新的結(jié)構(gòu)體 __Block_byref_ivar_int_block_0 創(chuàng)建實例變量 ivar_int_block,并使用C語言的 __attribute __ 指令來設(shè)置變量的屬性掌实,包含自身內(nèi)存地址陪蜻、內(nèi)存大小、變量數(shù)值等信息贱鼻,如下:
// 相當(dāng)于 __block int ivar_int_block = 4; 這行代碼
__attribute__((__blocks__(byref))) __Block_byref_ivar_int_block_0 ivar_int_block = {
(void*)0, // 對應(yīng)__isa
(__Block_byref_ivar_int_block_0 *)&ivar_int_block, // 對應(yīng)__forwarding
0, // 對應(yīng)__flags
sizeof(__Block_byref_ivar_int_block_0), // 對應(yīng)__size
4 // 對應(yīng)ivar_int_block
};
思考:__forwarding指針是自身內(nèi)存地址囱皿,有什么用?
網(wǎng)上一些文章稱忱嘹,在變量a被block拷貝到堆以后:
1.變量a的__forwording指針地址就變成了堆中新變量b的地址
2.被拷貝到堆的新變量b箱吕,__forwording指針地址是b的自身地址
感到非常合理置谦,但是我也不知道怎么驗證。
從下面block創(chuàng)建結(jié)構(gòu)體指針的源碼中,能夠發(fā)現(xiàn)被block捕獲的不是代碼中的看似帶__block的int變量购笆,而是新的實例變量,并且新變量不像普通變量一樣進行值傳遞矫渔,而是帶上了&符號進行指針傳遞崔列。
所以,我們能夠總結(jié)出屁桑,帶有__block修飾變量應(yīng)該歸為指針訪問医寿,不應(yīng)該和普通auto變量一樣認為是值訪問。
這里有兩點需要思考:
- 使用__block來修飾變量蘑斧,是否為block捕獲變量過程的一個環(huán)節(jié)靖秩?
當(dāng)然不是,沒有任何直接關(guān)系:不修飾也能捕獲竖瘾,修飾了也可以不使用block沟突。這里可以認為創(chuàng)建了一個新的變量,而這個被修飾的數(shù)值參與生成新變量的過程捕传,與接下來的block捕獲變量的過程毫無關(guān)系惠拭。
- 這里被block捕獲的是誰,是舊變量的數(shù)值還是新的Block變量庸论?
從源碼中能夠分析出职辅,看似存在的“舊變量”,已經(jīng)被新的Block變量收在里面聂示,它就像是西游記里“紫金紅葫蘆”一樣域携,“舊變量”想出去是出不去的,更不能被直接拿來使用了催什,而block創(chuàng)建當(dāng)然也只能使用新變量涵亏。
如果不通過自己的理解來領(lǐng)悟事物的底層邏輯宰睡,終究還是感覺心里沒譜。在網(wǎng)上搜尋別人的看法后气筋,我從中發(fā)現(xiàn)了一些自我感覺矛盾的地方拆内,而現(xiàn)在弄明白后理解的更加深刻,實現(xiàn)了學(xué)習(xí)-理解-批判-進步這個我瞎編的過程宠默。
3.3 Block對被捕獲變量的內(nèi)存管理
關(guān)于捕獲變量相關(guān)的內(nèi)容麸恍,這個文章第38條講述的很細致:
ChenYilong/iOSInterviewQuestions · GitHub
其中主要的幾點總結(jié):
- 1.Block不允許修改外部變量,因為進入 block 中的變量相當(dāng)于進入了另外一個函數(shù)作用域搀矫。
- 2.在 ARC 中無論是否添加 __block抹沪,block 中的 auto 變量都會被拷貝到堆上。
- 3.改外部變量必要條件:將 auto 變量封裝為結(jié)構(gòu)體(對象)瓤球。
眾所周知融欧,程序員只能夠管理堆上的內(nèi)存,所以我們可以想到:
- 1. 全局變量和局部靜態(tài)變量不在堆上卦羡,他們不需要block進行內(nèi)存管理噪馏。
至于其他的變量類型,我們可以列出來進行思考:
- 2. 原本在棧上的變量绿饵。比如:基本數(shù)值類型欠肾。
- 3. 原本就在堆上的變量。比如說:OC實例對象拟赊,__block修飾的變量刺桃。
<#未完工#>