定義
Block是一個(gè)里面存儲(chǔ)了指向定義block時(shí)的代碼塊的函數(shù)指針,以及block外部上下文變量信息的結(jié)構(gòu)體,簡單說就是:帶有自動(dòng)變量的匿名函數(shù)
Block對(duì)象內(nèi)存相關(guān)
iOS內(nèi)存分布一般為:棧區(qū)、堆區(qū)、全局區(qū)靠胜、常量區(qū)假褪、代碼區(qū).其實(shí)Block也是一個(gè)Objective-C的對(duì)象,常見的有以下三種block
- NSMallocBlock : 存放在堆區(qū)的Block
- NSStackBlock : 存放在棧區(qū)的Block
- NSGlobalBlock: 存放在全局區(qū)的Block
通過代碼實(shí)驗(yàn)(聲明 strong、copy筐带、weak 修飾的 Block彪薛,分別引用全局變量、全局靜態(tài)變量坯汤、局部靜態(tài)變量虐唠、普通外部變量) ,得出初步的結(jié)論:
- Block內(nèi)部沒有引用外部變量,Block在全局區(qū),屬于GlobalBlock
- Block 內(nèi)部有引用外部變量
a. 引用全局變量惰聂、全局靜態(tài)變量疆偿、局部靜態(tài)變量 : Block在全局區(qū),屬于GlobalBlock
b. 引用普通的外部變量,用copy、strong修飾的Block就放在堆區(qū),屬于是MallocBlock.用weak修飾的Block存放在棧區(qū).屬于StackBlock
注意:Block引用普通外部變量,都是在棧區(qū)創(chuàng)建的,只是用strong搓幌、copy修飾的Block會(huì)把它從棧區(qū)拷貝到堆區(qū)一份(棧區(qū)太小了2M),爾weak修飾的Block不會(huì).
通過上面的可以知道,在ARC中,用strong杆故、copy修飾的Block,會(huì)從棧區(qū)拷貝到堆區(qū),所以ARC中,用strong、copy修飾Block效果是一樣的.
Block源碼分析
通過clang命令將Objective-C代碼轉(zhuǎn)成C++代碼,可以了解其底層機(jī)制,有助于我們更深刻的認(rèn)識(shí)其實(shí)現(xiàn)原理.下面是clang相關(guān)命令
//1.最簡單的命令:
clang -rewrite-objc mian.m
//2.但是如果遇到 main.m:9:9: fatal error: 'UIKit/UIKit.h' file not found 類似的錯(cuò)誤需要我們指定下框架
xcrun -sdk iphonesimulator11.4 clang -S -rewrite-objc -fobjc-arc -fobjc-runtime=ios-11.4 main.m
//3.展示 SDK 版本命令
xcodebuild -showsdks
1.下載Block源碼:
https://opensource.apple.com/source/libclosure/libclosure-65/
- 然后將源碼中缺少的庫添加進(jìn)入工程溉愁,具體操作可以參考這篇 Blog:
https://blog.csdn.net/WOTors/article/details/54426316
3.通過上面兩個(gè)步驟处铛,我們就有一個(gè)包含 Block 源碼的工程,然后可以編寫 Block 代碼拐揭,去斷點(diǎn)觀察 Block 具體的執(zhí)行過程撤蟆。
配置工程還是比較麻煩的,這里我上傳了一份:BlockSourceCode
https://github.com/pengxuyuan/PXYFMWK/tree/master/BlockSourceCode
簡單分析Block C++源碼
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
static struct __block_desc_0 {
size_t reserved;
size_t Block_size;
} _block_desc_0_DATA = { 0, sizeof(struct __block_desc_0)};
struct _block_impl_0 {
struct __block_impl impl;
struct __block_desc_0* Desc;
int i; // 這個(gè)是引用外部變量 i
_block_impl_0(void *fp, struct __block_desc_0 *desc, int _i, int flags=0) :i(_i){
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
基本可以得出幾點(diǎn)結(jié)論:
1.結(jié)構(gòu)體中有isa指針,證明Block也是一個(gè)對(duì)象
2.Block底層是用結(jié)構(gòu)體實(shí)現(xiàn)的,結(jié)構(gòu)體 _block_impl_0 包含了 __block_impl 結(jié)構(gòu)體和__block_desc_0結(jié)構(gòu)體(作用后續(xù)補(bǔ)充)
3.__block_impl 結(jié)構(gòu)體中的FuncPtr函數(shù)指針,指向的就是我們的Block的具體實(shí)現(xiàn).真正調(diào)用Block就是利用函數(shù)指針去調(diào)用的.
4.為什么能訪問到外部變量就是因?yàn)閷⑼獠孔兞繌?fù)制到了結(jié)構(gòu)體中(int _i 就是外部變量),即自動(dòng)變量回作為成員變量追加到Block結(jié)構(gòu)體中.
分析具有__block修飾外部變量的Block源碼
我們知道Block截獲外部變量是將外部變量作為成員變量追加到Block結(jié)構(gòu)體中國,但是匿名函數(shù)存在作用域的問題,這個(gè)就是為什么我們不能再Block內(nèi)部去修改普通外部變量的原因.所以就出現(xiàn)__block修飾符來解決這個(gè)問題.
下面我們看下__block修飾的變量轉(zhuǎn)換成C++代碼的樣子
//Objective-C 代碼
- (void)blockDataBlockFunction {
__block int a = 100; ///在棧區(qū)
void (^blockDataBlock)(void) = ^{
a = 1000;
NSLog(@"%d", a);
}; ///在堆區(qū)
blockDataBlock();
}
//C++ 代碼
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};
struct __BlockStructureViewController__blockDataBlockFunction_block_impl_0 {
struct __block_impl impl;
struct __BlockStructureViewController__blockDataBlockFunction_block_desc_0* Desc;
__Block_byref_a_0 *a; // by ref
};
具有__block修飾的變量,會(huì)生成一個(gè) Block_byref_a_0結(jié)構(gòu)體來表示外部變量,然后再追加到Block的結(jié)構(gòu)體中,這里生成Block_byref_a_0這個(gè)結(jié)構(gòu)體的原因有兩個(gè):一個(gè)是抽象出一個(gè)結(jié)構(gòu)體,可以讓多個(gè)Block同事引用這個(gè)外部變量:兩一個(gè)是好管理:,因?yàn)锽lock_byref_a_0中有個(gè)非常重要的成員變量forwarding指針,這個(gè)指針非常重要(指向Block_byref_a_0結(jié)構(gòu)體),這里是保證當(dāng)我們將Block從棧區(qū)拷貝到堆區(qū)中,修改的變量是同一份.
Block是如何解決存儲(chǔ)域的問題
首先我們知道Block底層是結(jié)構(gòu)體,Block會(huì)轉(zhuǎn)換成block結(jié)構(gòu)體,__block會(huì)轉(zhuǎn)換成__blcok結(jié)構(gòu)體
然后block沒有截獲外部變量堂污、截獲全局變量的都屬于是全局區(qū)的block,即GlobalBlock:其余的都是棧區(qū)的Block.
為了解決作用域的問題,Block提供了copy函數(shù),將Block從棧復(fù)制到堆上,在MRC環(huán)境下需要我們自己調(diào)用Block_copy函數(shù),這里就是為什么MRC下,我們?yōu)槭裁匆胏opy來修飾Block的原因.
在ARC環(huán)境下,編譯器會(huì)盡可能的給我們自動(dòng)添加copy的操作,這里為什么說盡量呢,因?yàn)橛行┣闆r編譯器無法判斷的時(shí)候,就不會(huì)給我們添加copy操作,這里就需要我們自己主動(dòng)調(diào)用copy方法.
__block 變量的存儲(chǔ)域
Block從棧復(fù)制到堆上,__block修飾的變量也會(huì)從棧復(fù)制到堆上;為了結(jié)構(gòu)體__block變量無論在棧上還是在堆上,都可以正確的訪問變量,我們需要forwarding指針
在Block從棧復(fù)制到堆的時(shí)候,原來?xiàng)I辖Y(jié)構(gòu)體的forwarding指針,會(huì)改變指向,直接指向堆上的結(jié)構(gòu)體,這樣就可以保證之后我們都是訪問同一個(gè)結(jié)構(gòu)體中的變量,這里就是問什么__block修飾的變量,在block內(nèi)部中可以修飾的原因了.
Block截獲對(duì)象需要管理對(duì)象的生命周期
我們知道Block引用外部變量會(huì)將其追加到結(jié)構(gòu)體中,但是編譯器是無法判斷C語言結(jié)構(gòu)體的初始化和廢棄的,因此__block-desc_0會(huì)增加成員變量copy和dispose;以及block_copy家肯、block_dispose函數(shù).用來Block從棧復(fù)制到堆、堆上的Block廢棄的時(shí)候分別調(diào)用.
Block會(huì)出現(xiàn)循環(huán)引用
對(duì)于Block循環(huán)引用算是經(jīng)典問題了,當(dāng)A持有B,B持有A的時(shí)候就會(huì)出現(xiàn)循環(huán)引用.Block對(duì)于外部比那兩都會(huì)追加到結(jié)構(gòu)體中,所以在實(shí)現(xiàn)Block時(shí)候需要注意這個(gè)問題.
ARC環(huán)境一般我們用__weak來打破,MRC環(huán)境下的話,我們可以使用__block來打破循環(huán)引用.
Block面試題
- 下面代碼在ARC和MRC環(huán)境下運(yùn)行情況
void exampleA() {
char a = 'A';
^{
printf("%cn", a);
}();
}
exampleA();
答: 首先這個(gè)Block引用了普通的外部變量,所以這個(gè)Block是在棧上創(chuàng)建的.Block是在exampleA()函數(shù)內(nèi)創(chuàng)建的.然后創(chuàng)建完馬上調(diào)用了,這個(gè)時(shí)候 exampleA() 并沒有執(zhí)行完,所以這個(gè)棧Block 是存在的,不會(huì)被 pop 出戰(zhàn).所以在MRC和ARC 環(huán)境下都能正確編譯運(yùn)行.
- 下面代碼在MRC環(huán)境和ARC環(huán)境下運(yùn)行的情況
void exampleB_addBlockToArray(NSMutableArray *array) {
char b = 'B';
[array addObject:^{
printf("%cn", b);
}];
}
void exampleB() {
NSMutableArray *array = [NSMutableArray array];
exampleB_addBlockToArray(array);
void (^block)() = [array objectAtIndex:0];
block();
}
exampleB();
答: 這個(gè)跟第一題的區(qū)別就是將Block的創(chuàng)建放到一個(gè)函數(shù)中去.同理分析exampleB_addBlockToArray中創(chuàng)建的Block也是引用了普通的外部變量,Bloock創(chuàng)建在棧上.
MRC 環(huán)境上,調(diào)用exampleB_addBlockToArray 函數(shù),會(huì)創(chuàng)建一個(gè)棧 Block 存放到數(shù)組中去,然后 exampleB_addBlockToArray 函數(shù)結(jié)束, Block 被pop 出棧. 這個(gè)時(shí)候再去調(diào)用Block , Block 已經(jīng)被釋放了,所以出現(xiàn)異常,不能正確執(zhí)行.
ARC 環(huán)境下,在 NSMutableArray 的 addObject 方法中,編譯器會(huì)自動(dòng)執(zhí)行 Copy 的操作,將Block 從椕瞬拷貝到堆, 所以ARC 環(huán)境沒問題.
修改方案
// 主動(dòng)調(diào)用 copy 方法讨衣,將 Block 從棧拷貝到堆中扒披,Block_copy(<#...#>)
[array addObject:[^{
printf("%cn", b);
} copy]];
- 下面代碼在MRC 和 ARC 環(huán)境下會(huì)出現(xiàn)什么問題
void exampleC_addBlockToArray(NSMutableArray *array) {
[array addObject:^{
printf("Cn");
}];
}
void exampleC() {
NSMutableArray *array = [NSMutableArray array];
exampleC_addBlockToArray(array);
void (^block)() = [array objectAtIndex:0];
block();
}
exampleC();
答:exampleC_addBlockToArray 中的 Block 并沒有引用外部變量值依,所以 Block 是創(chuàng)建在全局區(qū)的,是一個(gè) GlobalBlock碟案,生命周期是跟隨著程序的愿险,故 MRC、ARC 環(huán)境下都可以正確運(yùn)行价说。
- 下面代碼在MRC 和 ARC 環(huán)境下會(huì)出現(xiàn)什么問題
typedef void (^dBlock)();
dBlock exampleD_getBlock() {
char d = 'D';
return ^{
printf("%cn", d);
};
}
void exampleD() {
exampleD_getBlock()();
}
exampleD();
答:這題跟第二題差不多辆亏,區(qū)別在于這里是將 Block 作為函數(shù)返回值了;一樣棧區(qū) Block 在 exampleD_getBlock 函數(shù)執(zhí)行完就會(huì)釋放鳖目,MRC 環(huán)境下會(huì)調(diào)用異常扮叨,但是這里編譯器能檢查到這種情況,這里實(shí)際效果是編譯不通過领迈。
在 ARC 環(huán)境下彻磁,Block 作為函數(shù)返回值碍沐,會(huì)自動(dòng)調(diào)用 Copy 方法,將 Block 從棧復(fù)制到堆上(StackBlock -> MallocBlock)衷蜓,故 ARC 環(huán)境下可以正確運(yùn)行累提。
- 下面代碼在 MRC 環(huán)境 和 ARC 環(huán)境運(yùn)行的情況
typedef void (^eBlock)();
eBlock exampleE_getBlock() {
char e = 'E';
void (^block)() = ^{
printf("%cn", e);
};
return block;
}
void exampleE() {
eBlock block = exampleE_getBlock();
block()
}
exampleE();
答:這題跟第四題是一樣的,這里在 MRC 環(huán)境下磁浇,可以編譯通過斋陪,但是調(diào)用異常;ARC 環(huán)境下可以正確執(zhí)行置吓。
- ARC 環(huán)境下輸出的結(jié)果
__block NSString *key = @"AAA";
objc_setAssociatedObject(self, &key, @1, OBJC_ASSOCIATION_ASSIGN);
id a = objc_getAssociatedObject(self, &key);
void (^block)(void) = ^ {
objc_setAssociatedObject(self, &key, @2, OBJC_ASSOCIATION_ASSIGN);
};
id m = objc_getAssociatedObject(self, &key);
block();
id n = objc_getAssociatedObject(self, &key);
objc_setAssociatedObject(self, &key, @3, OBJC_ASSOCIATION_ASSIGN);
id p = objc_getAssociatedObject(self, &key);
NSLog(@"%@ --- %@ --- %@ --- %@",a,m,n,p);
答:輸入結(jié)果:1 --- (null) --- 2 --- 3无虚,代碼執(zhí)行過程如下:
1.__block 修飾的 key,創(chuàng)建在棧區(qū)衍锚,訪問變量 key 為:&(結(jié)構(gòu)體->forwarding->key) 友题,key 在棧區(qū),此時(shí)利用棧區(qū)地址作為 Key 來存值
2.變量 a 使用棧區(qū)地址取值构拳,故 a 的值為 1
3.聲明一個(gè) block咆爽,引用到了外部變量 key,此時(shí)將 block 從椫蒙拷貝堆斗埂,訪問變量 key 為:&(結(jié)構(gòu)體->forwarding->key) ,key 在堆區(qū)
4.變量 m 用堆區(qū)地址來取值凫海,故為 null
5.執(zhí)行 block呛凶,用堆區(qū)地址將 2 存進(jìn)去
6.變量 n 用堆區(qū)地址來取值,故為 2
7.再用堆區(qū)地址將 3 存進(jìn)去
8.變量 p 用堆區(qū)地址來取值行贪,故為 3
- 有幾種方式去調(diào)用 Block
void (^block)(void) = ^{
NSLog(@"block get called");
};
//1. blcok()
block();
//2. 利用其它方法去執(zhí)行 block
[UIView animateWithDuration:0 animations:block];
//3.
[[NSBlockOperation blockOperationWithBlock:block] start];
//4. NSInvocation
NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v@?"];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
[invocation invokeWithTarget:block];
//5.DLIntrospection invoke
[block invoke];
//6. 指針調(diào)用
void *pBlock = (__bridge void *)block;
void (*invoke)(void *, ...) = *((void **)pBlock + 2);
invoke(pBlock);
//7. 利用 Clang
__strong void(^cleaner)(void) __attribute ((cleanup(blockCleanUp),unused)) = block;
//8. 內(nèi)聯(lián)一個(gè)匯編 完成調(diào)用
asm("callq *0x10(%rax)");
static void blockCleanUp (__strong void (^*block)(void)) {
(*block)();
}
如何通過 Block 實(shí)現(xiàn)鏈?zhǔn)骄幊田L(fēng)格的代碼
具體可看實(shí)現(xiàn):Block ChainProgramming
https://github.com/pengxuyuan/PXYFMWK/blob/master/PXYFMWK/PXYFMWK/PXYFMWK/PXYFMWK/Component/PXYChainProgramming/UIView/UIView%2BPXYChainProgramming.m
具體參考 Masonry , SnapkitBlock 為什么用 Copy 修飾
對(duì)于這個(gè)問題漾稀,得區(qū)分 MRC 環(huán)境 和 ARC 環(huán)境;首先建瘫,通過上面小節(jié)可知崭捍,Block 引用了普通外部變量,都是創(chuàng)建在棧區(qū)的啰脚;對(duì)于分配在棧區(qū)的對(duì)象殷蛇,我們很容易會(huì)在釋放之后繼續(xù)調(diào)用,導(dǎo)致程序奔潰橄浓,所以我們使用的時(shí)候需要將棧區(qū)的對(duì)象移到堆區(qū)粒梦,來延長該對(duì)象的生命周期。
對(duì)于 MRC 環(huán)境荸实,使用 Copy 修飾 Block匀们,會(huì)將棧區(qū)的 Block 拷貝到堆區(qū)。
對(duì)于 ARC 環(huán)境准给,使用 Strong泄朴、Copy 修飾 Block重抖,都會(huì)將棧區(qū)的 Block 拷貝到堆區(qū)。
所以祖灰,Block 不是一定要用 Copy 來修飾的仇哆,在 ARC 環(huán)境下面 Strong 和 Copy 修飾效果是一樣的。