1. Block 本質(zhì)
現(xiàn)在我們來(lái)實(shí)現(xiàn)一個(gè)最簡(jiǎn)單的BlockA
void (^BlockA)(void) = ^{
NSLog(@"block A");
};
BlockA();
通過(guò)clang命名轉(zhuǎn)化成C++源碼:
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m
查看源碼會(huì)得到 BlockA 會(huì)生成以下幾個(gè)結(jié)構(gòu)體:
// 函數(shù)棧里面定義的 BlockA
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;
}
};
// __main_block_impl_0 的第一個(gè)結(jié)構(gòu)體成員
// 里面包含有 isa 指針
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
// 里面有個(gè)主要的變量 Block_size ,通過(guò)sizeof(struct __main_block_impl_0)賦值苟穆,實(shí)際上是BlockA的內(nèi)存大小
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)};
// blockA {} 花括號(hào)里面的代碼,需要傳遞 __main_block_impl_0 參數(shù)
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
NSLog((NSString *)&__NSConstantStringImpl__var_folders_50_s42jfyxs01s8t4z0c1q2p2c00000gn_T_main_d4d224_mi_1);
}
根據(jù)BlockA的定義可以看出虏两,Block和對(duì)象一樣擁有isa指針,且有Block_size來(lái)計(jì)算分配內(nèi)存空間的大小的屬性世剖,所以Block也是一個(gè)對(duì)象定罢,對(duì)象就能調(diào)用Class方法(通過(guò)block能夠調(diào)用Class方法也能反推block是一個(gè)對(duì)象)。
接下來(lái)看 BlockA();
轉(zhuǎn)化成的源碼
void (*BlockA)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
// 初始化BlockA旁瘫,傳入 __main_block_func_0 執(zhí)行的函數(shù)地址祖凫,
// 通過(guò) __main_block_impl_0 構(gòu)造函數(shù)可知 __main_block_func_0 會(huì)通過(guò) impl.FuncPtr = fp; 賦值給 FuncPtr
// BlockA() 源碼
((void (*)(__block_impl *))((__block_impl *)BlockA)->FuncPtr)((__block_impl *)BlockA);
// 去掉強(qiáng)制類型轉(zhuǎn)換
BlockA->FuncPtr(BlockA);
// 其實(shí)BlockA()就是通過(guò)函數(shù)指針直接調(diào)用了函數(shù)并且傳入了BlockA對(duì)象
所以Block可以簡(jiǎn)單總結(jié):
- block本質(zhì)上也是一個(gè)OC對(duì)象,它內(nèi)部也有個(gè)isa指針;
- block是封裝了函數(shù)調(diào)用以及函數(shù)調(diào)用環(huán)境的OC對(duì)象.
由上可得Block的內(nèi)存結(jié)構(gòu)圖如下:
2酬凳,Block類型
Block 有三種類型:
1惠况,NSGlobalBlock 全局區(qū)的Block
2,NSStackBlock 棧區(qū)的Block
3宁仔,NSMallocBlock 堆區(qū)的Block
接下來(lái)我們通過(guò)定義幾個(gè)Block來(lái)訪問(wèn)外部變量稠屠,看看他們有什么區(qū)別:
int G = 100;
// 全局區(qū)的block
void (^Block_G)(void) = ^{
NSLog(@"block G %d", G);
};
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 不訪問(wèn)任何變量
void (^BlockA)(void) = ^{
NSLog(@"block A");
};
BlockA();
// 訪問(wèn)auto 局部變量
int b = 10;
void (^BlockB)(void) = ^{
NSLog(@"block B %d", b);
};
b = 20;
BlockB();
// weak block,讓編譯器不進(jìn)行 copy 操作
__weak void (^BlockC)(void) = ^{
NSLog(@"block D %d", b);
};
BlockC();
static int d = 25;
// 訪問(wèn)全局變量
void (^BlockD)(void) = ^{
NSLog(@"block D %d", d);
};
d = 30;
BlockD();
NSLog(@"BlockA class -> %@", [BlockA class]);
NSLog(@"BlockB class -> %@", [BlockB class]);
NSLog(@"BlockC class -> %@", [BlockC class]);
NSLog(@"BlockD class -> %@", [BlockD class]);
NSLog(@"BlockG class -> %@", [Block_G class]);
}
return 0;
}
打印結(jié)果【注arc模式下】:
根據(jù)以上輸出我們帶著幾個(gè)疑問(wèn)來(lái)探尋下Block的實(shí)現(xiàn)原理:
1翎苫,為什么Block能調(diào)用class方法权埠?
2,b煎谍,d變量被修改后攘蔽,為什么Block B里面的輸出值不是20?而BlockD輸出的是30
3呐粘,同樣在main函數(shù)里面定義的BlockA/B/C秩彤,為什么Class類型不一樣?
4事哭,同樣是NSGlobalBlock類型的Block_G與BlockA定義是一樣的嗎?
通過(guò)前面講的瓜富,我們知道了 BlockA
的內(nèi)存結(jié)構(gòu)鳍咱,接下來(lái)我們看下 BlockB 和BlockD的內(nèi)存結(jié)構(gòu):
/* OC代碼
// 訪問(wèn)局部變量
void (^BlockB)(void) = ^{
NSLog(@"block B %d", b);
};
*/
// BlockB 定義
struct __main_block_impl_1 {
struct __block_impl impl;
struct __main_block_desc_1* Desc;
int b;
__main_block_impl_1(void *fp, struct __main_block_desc_1 *desc, int _b, int flags=0) : b(_b) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
/* OC代碼
static int d = 30;
// 訪問(wèn)全局變量
void (^BlockD)(void) = ^{
NSLog(@"block D %d", d);
};
*/
// BlockD 源碼定義
struct __main_block_impl_3 {
struct __block_impl impl;
struct __main_block_desc_3* Desc;
int *d;
__main_block_impl_3(void *fp, struct __main_block_desc_3 *desc, int *_d, int flags=0) : d(_d) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
從上面BlockB和BlockD的內(nèi)存結(jié)構(gòu)可以看出,他們比BlockA內(nèi)部多一個(gè)變量与柑,多出來(lái)的變量其實(shí)Block捕獲外部的變量谤辜,捕獲的變量可以得出:
- BlockB 里面捕獲的是int b 是一個(gè)值類型的int變量蓄坏,所以后面b值修改后,BlockB里面的b不會(huì)變丑念;
- BlockD 里面捕獲的是int *d 是一個(gè)指針變量涡戳,所以后面d值修改后,BlockD通過(guò)指針訪問(wèn)的d還是BlockD外面的變量d脯倚。
以下是Block捕獲變量的規(guī)則:
再來(lái)看下Block_G 的定義如下:
struct __Block_G_block_impl_0 {
struct __block_impl impl;
struct __Block_G_block_desc_0* Desc;
__Block_G_block_impl_0(void *fp, struct __Block_G_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteGlobalBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
BlockA 和 Block_G 的區(qū)別是 isa 指針定義類型不一樣
BlockA:_NSConcreteStackBlock 在函數(shù)棧里面定義的block
Block_G:_NSConcreteGlobalBlock 在全局區(qū)定義的block
其實(shí)在Clang的文檔中渔彰,只定義了兩個(gè)Block類型: _NSConcreteGlobalBlock 和 _NSConcreteStackBlock 。而在Console中的Log我們看到的3個(gè)類型應(yīng)該是處理過(guò)的顯示推正,這些字樣在蘋果的文檔和Clang/LLVM的文檔中實(shí)難找到恍涂。
Console中輸出的的class類型是根據(jù)訪問(wèn)外部變量來(lái)確定的,其規(guī)則如下:
根據(jù)Block類型及捕獲變量規(guī)則我們就能知道為什么BlockA/B/C的類型為什么不一樣了:
- BlockA沒(méi)有訪問(wèn)任何變量植榕,所以它是NSGlobalBlock類型
- BlockC訪問(wèn)了局部auto變量再沧,所以它是NSStackBlock類型
- BlockB訪問(wèn)了局部auto變量,arc自動(dòng)給他進(jìn)行了copy操作尊残,所以它是NSMallocBlock類型
在ARC環(huán)境下炒瘸,編譯器會(huì)根據(jù)情況自動(dòng)將棧上的block復(fù)制到堆上,比如以下情況:
- block作為函數(shù)返回值時(shí)
- 將block賦值給__strong指針時(shí)
- block作為Cocoa API中方法名含有usingBlock的方法參數(shù)時(shí)
- block作為GCD API的方法參數(shù)時(shí)
MRC下block屬性的建議寫法
@property (copy, nonatomic) void (^block)(void);
ARC下block屬性的建議寫法寝衫,ARC下stong和copy沒(méi)有區(qū)別
@property (strong, nonatomic) void (^block)(void);
@property (copy, nonatomic) void (^block)(void);
3顷扩,Block內(nèi)存管理
Block訪問(wèn)對(duì)象
FRFruit *fruit = [[FRFruit alloc] init];
FRFruit *fruit1 = [[FRFruit alloc] init];
__weak FRFruit *weakfruit = fruit1;
void (^Block_Objct)(void) = ^{
NSLog(@"block %@", fruit);
NSLog(@"block %@", weakfruit);
};
Block_Objct();
轉(zhuǎn)化成C++源碼
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
FRFruit *__strong fruit;
FRFruit *__weak weakfruit;
....
};
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};
可以看出Block里面默認(rèn)捕獲外面的對(duì)象為strong屬性修飾,如果外部是weak屬性的竞端,其內(nèi)部也會(huì)是相應(yīng)的weak屬性修飾屎即;
__main_block_desc_0 結(jié)構(gòu)體里面多了copy和dispose兩個(gè)函數(shù),它們是Block用來(lái)管理內(nèi)存的
來(lái)看下它們的定義:
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
_Block_object_assign((void*)&dst->fruit, (void*)src->fruit, 3);
_Block_object_assign((void*)&dst->weakfruit, (void*)src->weakfruit, 3);
}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
_Block_object_dispose((void*)src->fruit, 3);
_Block_object_dispose((void*)src->weakfruit, 3)
;}
里面主要有 _Block_object_assign() 和 _Block_object_dispose() 兩個(gè)函數(shù)
- 當(dāng)block被copy到堆時(shí)事富;
·會(huì)調(diào)用block內(nèi)部的copy函數(shù)技俐;
·copy函數(shù)內(nèi)部會(huì)調(diào)用 Block_object_assign 函數(shù);
·Block_object_assign 當(dāng)傳入的參數(shù)是stong類型時(shí)會(huì)進(jìn)行retain操作统台,如果是weak指針不會(huì)進(jìn)行retain操作雕擂;- main_block_dispose_0 方法類似C++里面對(duì)析構(gòu)函數(shù),Block釋放時(shí)會(huì)調(diào)用贱勃,Block_object_dispose 函數(shù)則是對(duì)傳入?yún)?shù)進(jìn)行release釋放井赌。
所以當(dāng)block對(duì)象未釋放時(shí),它里面如果是strong修飾的對(duì)象也不會(huì)被釋放贵扰,正因?yàn)槿绱顺鹚耄訠lock常常伴隨著會(huì)出現(xiàn)循環(huán)引用問(wèn)題。
比如我們下面這種用法:
FRFruit *fruit = [[FRFruit alloc] init];
fruit.block_Objct = ^{
NSLog(@"block %@", fruit);
};
fruit.block_Objct();
// fruit 對(duì)象里面有一個(gè)copy修飾的 block_Objct 屬性
Block循環(huán)引用的原理:
我們根據(jù)OC的內(nèi)存管理機(jī)制知道戚绕,當(dāng)對(duì)象需要被釋放時(shí)必須先釋放所有指向它的指針纹坐。
所以如果要先釋放fruit對(duì)象,需要釋放block_Objct里面的變量舞丛,如果釋放block_Objct里面的變量需要先釋放block_Objct耘子,而block_Objct又被fruit強(qiáng)引用果漾,這樣就出現(xiàn)了循環(huán)引用的問(wèn)題。
那么如何解決這個(gè)問(wèn)題呢谷誓?
通常我們ARC環(huán)境下面的解決辦法是通過(guò)__weak
指針來(lái)解決這個(gè)問(wèn)題绒障,通過(guò)上面講的Block里面的變量是通過(guò)訪問(wèn)的外部變量是否是strong
或weak
指針來(lái)進(jìn)行內(nèi)部對(duì)象進(jìn)行相應(yīng)修飾的,所以如果訪問(wèn)的外部對(duì)象是weak
指針時(shí)捍歪,他們的引用關(guān)系就會(huì)如下圖:
weak指針解決循環(huán)引用代碼如下:
FRFruit *fruit = [[FRFruit alloc] init];
__weak FRFruit *weakfruit = fruit;
fruit.block_Objct = ^{
NSLog(@"block %@", weakfruit);
};
fruit.block_Objct();
其實(shí)除了weak
還有__unsafe_unretain
和 __block
户辱,其實(shí)現(xiàn)如下:
// __unsafe_unretain 用法
FRFruit *fruit = [[FRFruit alloc] init];
__unsafe_unretained FRFruit *weakfruit = fruit;
fruit.block_Objct = ^{
NSLog(@"block %@", weakfruit);
};
// __block 用法,主意ARC環(huán)境下 block中需要將變量只為nil费封,且必須調(diào)用block焕妙,才能打破循環(huán)
FRFruit *fruit = [[FRFruit alloc] init];
__block FRFruit *fruit = fruit;
fruit.block_Objct = ^{
NSLog(@"block %@", fruit);
fruit = nil; // MRC 不需要置為nil
};
fruit.block_Objct();
鑒于ARC環(huán)境下weak指針的底層實(shí)現(xiàn)原理(對(duì)象釋放時(shí)會(huì)自動(dòng)指針會(huì)自動(dòng)置為nil),所以推薦使用weak
來(lái)解決循環(huán)引用問(wèn)題弓摘。
MRC環(huán)境下推薦使用__unsafe_unretained
和 __block
4焚鹊,__block 修改局部變量原理
我們知道block
是不能直接修改局部auto變量的,比如以下代碼編譯時(shí)會(huì)直接報(bào)錯(cuò):
int a = 10;
void (^Block)(void) = ^{
a = 20; // 不能修改a變量
};
Block();
因?yàn)楦鶕?jù)計(jì)算機(jī)內(nèi)存分配原理可知韧献,a變量是在棧上的末患,它的內(nèi)存空間在函數(shù)結(jié)束時(shí)就會(huì)被回收,而Block有可能被復(fù)制到堆空間上锤窑,堆上空間的釋放由開發(fā)者控制的璧针,所以函數(shù)結(jié)束時(shí)Block有可能還會(huì)被執(zhí)行,而這時(shí)變量a已經(jīng)被釋放了渊啰,Block就無(wú)法找到變量a的內(nèi)存進(jìn)行賦值探橱,所以這種操作是被禁止的。
如果要修改局部變量绘证,OC提供__block 修飾來(lái)修改隧膏,其用法如下:
int a = 10;
void (^Block)(void) = ^{
a = 20;
};
Block();
那它的實(shí)現(xiàn)原理又是怎樣的呢?接下來(lái)我們看下源碼:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
// 不加__block 時(shí)是 int a 嚷那,
// 加上__block 變成了__Block_byref_a_0 *a
__Block_byref_a_0 *a; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_a_0 *a = __cself->a; // bound by ref
// block里面的訪問(wèn)__block 修飾的變量時(shí)都會(huì)通過(guò)結(jié)構(gòu)體中的 forwarding 指針來(lái)訪問(wèn)
(a->__forwarding->a) = 20;
}
__block 實(shí)際上是將局部變量放在 __Block_byref_a_0 對(duì)象里面胞枕,該對(duì)象里面有一個(gè) __forwarding 指針,最開始__Block_byref_a_0 在棧上時(shí)魏宽, __forwarding 屬性會(huì)指向它自己腐泻,當(dāng)Block復(fù)制到堆上時(shí),__Block_byref_a_0 對(duì)象也會(huì)復(fù)制一份到堆上队询,此時(shí) __forwarding 指針指向的是堆上的那塊內(nèi)存派桩,所以Block實(shí)際上訪問(wèn)的a變量不再是棧上的變量,而是__Block_byref_a_0對(duì)象中堆內(nèi)存的那個(gè)a
forwarding 指針實(shí)現(xiàn)原理:
全文純手寫總結(jié)蚌斩,有些地方總結(jié)的不仔細(xì)铆惑,邏輯也不太清楚,等有時(shí)間會(huì)再修改梳理一下。
如有錯(cuò)誤請(qǐng)指正鸭津,感謝閱讀,謝謝大家肠缨!