Block函數(shù)有三種:
第一種:全局block
void (^block)(void) = ^{
NSLog(@"block!");
};
NSLog(@"%@",block);
打印結(jié)果:<__NSGlobalBlock__: 0x10d94f088>
第二種:堆區(qū)block
int a = 10;
void (^block)(void) = ^{
NSLog(@"block - %d!",a);
};
NSLog(@"%@",block);
打印結(jié)果:<__NSMallocBlock__: 0x6000020eb0c0>
第三種:棧區(qū)block,棧區(qū)block在iOS14后,越來(lái)越少,因此需要使用__weak使其不在強(qiáng)持有。
int a = 10;
void (^__weak block)(void) = ^{
NSLog(@"block - %d!",a);
};
NSLog(@"%@",block);
<__NSStackBlock__: 0x7ffeeba41478>
全局訪問(wèn)外界變量強(qiáng)引用變成堆區(qū),弱引用變成棧區(qū)。
既然是block,那就存在循環(huán)引用問(wèn)題,那就先要了解循環(huán)引用的概念,按照正常的流程來(lái)說(shuō),例如A持有B,B的引用計(jì)數(shù)加1,而當(dāng)A發(fā)送dealloc信號(hào)之后,B的引用計(jì)數(shù)需要減1變?yōu)?,那么dealloc才會(huì)正常被調(diào)用;而循環(huán)引用就是A持有B,B也持有A,構(gòu)成了相互持有,那么在釋放的時(shí)候,誰(shuí)也釋放不了對(duì)方,就造成了循環(huán)引用問(wèn)題。
那么如何解決循環(huán)引用問(wèn)題呢?
來(lái)看一段代碼:
typedef void(^WXBlock)(void);
@interface ViewController ()
@property (nonatomic, copy) WXBlock block;
@property (nonatomic, copy) NSString *name;
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 循環(huán)引用
self.name = @"Block";
self.block = ^(void) {
NSLog(@"%@",self.name);
};
self.block();
}
在上面一段代碼中,肯定是會(huì)造成循環(huán)引用的,因?yàn)閟elf引用了block,而bloc也引用了self;類似于self -> block ->self;
那么解決循環(huán)引用,相信很多人都知道是用__weak;它加入了一張弱引用表,增加__weak typeof(self) weakSelf = self;這一行實(shí)現(xiàn)弱引用,就類似于self -> block ->weakSelf -> self;
那么weakSelf持有強(qiáng)引用對(duì)象self,引用計(jì)數(shù)是不會(huì)增加的,因此weakSelf持有的self在weakSelf生命周期結(jié)束之后,也就進(jìn)行釋放了。
下面是執(zhí)行的結(jié)果:

那么這種方式來(lái)解決循環(huán)引用是會(huì)存在某些問(wèn)題的,例如修改部分代碼,異步延遲兩秒執(zhí)行:
self.block = ^(void) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@",weakSelf.name);
});
};
那么在延遲兩秒執(zhí)行后,還沒來(lái)得及調(diào)用,self就被釋放了,因此self的生命周期是不足以得到保證的。

那么我們又可以在block函數(shù)內(nèi)部對(duì)weakSelf進(jìn)行強(qiáng)引用,就可以解決這個(gè)問(wèn)題。
增加代碼__strong typeof(self) strongSelf = weakSelf;
打印結(jié)果為:

這樣的強(qiáng)引用對(duì)象是在block函數(shù)調(diào)用結(jié)束之后,就會(huì)進(jìn)行釋放。
那么使用__weak解決循環(huán)引用就需要weak和strong結(jié)合使用。
完整代碼:
__weak typeof(self) weakSelf = self;
self.block = ^(void) {
__strong typeof(self) strongSelf = weakSelf;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@",strongSelf.name);
});
};
self.block();
__weak只是解決循環(huán)引用的方式之一,他是自動(dòng)釋放,下面介紹第二種解決方式,手動(dòng)釋放,看代碼:
__block ViewController *vc = self;
self.block = ^(void) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@",vc.name);
vc = nil;
});
};
self.block();
使用__block對(duì)ViewController賦值self,通過(guò)在輸出之后,手動(dòng)將vc置為nil。類似于self->block-> vc=nil ->self;vc被block捕獲,無(wú)法自動(dòng)釋放,那么手動(dòng)釋放,就解決了釋放這一問(wèn)題。
接下來(lái)介紹第三種解決循環(huán)引用問(wèn)題,那就是通過(guò)參數(shù)來(lái)解決問(wèn)題:
看代碼
typedef void(^WXBlock)(ViewController *);
self.block = ^(ViewController *vc) {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"%@",vc.name);
});
};
self.block(self);
在了解了block的使用之后,下面來(lái)看一下block的底層原理,首先通過(guò)xcrun來(lái)看一下block的cpp是如何實(shí)現(xiàn)的:
#include "stdio.h"
int main(){
void(^block)(void) = ^{
printf("Block - ");
};
block();
return 0;
}
上面的c代碼通過(guò)xcrun -sdk iphonesimulator clang -arch x86_64 -rewrite-objc block.c轉(zhuǎn)換為:
int main(){
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
return 0;
}
__main_block_impl_0是一個(gè)結(jié)構(gòu)體:


也就是說(shuō),block的本質(zhì)就是對(duì)象結(jié)構(gòu)體,以函數(shù)作為參數(shù)傳入進(jìn)來(lái);
由impl.FuncPtr = fp;可以知道block是需要具體函數(shù)實(shí)現(xiàn)的;
而*__cself作為匿名參數(shù),因此可以獲取block內(nèi)部的代碼,并執(zhí)行。
那么如果有外界參數(shù)時(shí),block又是如何實(shí)現(xiàn)的呢?
通過(guò)轉(zhuǎn)換之后得到了下面的代碼:
int a = 11;
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
return 0;

可以看到,a的值在編譯時(shí),就自動(dòng)生成了相應(yīng)的變量,而在__main_block_func_0的方法中,就通過(guò)了一種賦值拷貝的方式賦值給a,但是里面的a和外面的a是不一樣的。
那么對(duì)里面的a進(jìn)行加加,在轉(zhuǎn)換后為:
__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 11};
void(*block)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
return 0;

可以看到a是進(jìn)行了指針拷貝,也就是說(shuō)兩個(gè)變量a同時(shí)指向同一片內(nèi)存空間。
總結(jié):
block本質(zhì)一個(gè)對(duì)象結(jié)構(gòu)體,匿名函數(shù),block自動(dòng)捕獲外界變量生成同一個(gè)屬性來(lái)保存,block調(diào)用block()是因?yàn)楹瘮?shù)申明,需要有具體的函數(shù)實(shí)現(xiàn);而__block的原理就是生成相應(yīng)的結(jié)構(gòu)體,保存原始變量進(jìn)行指針拷貝,傳遞指針地址給block。
那到現(xiàn)在為止,其實(shí)還沒有探索到一些核心的底層原理,還不清楚,__NSGlobalBlock__,__NSStackBlock__,__NSMallocBlock__在內(nèi)存地址中是如何變化的,以及關(guān)于block調(diào)用的問(wèn)題。
下面我們探索一下運(yùn)行時(shí)的block。
首先創(chuàng)建工程,代碼很簡(jiǎn)單:

利用真機(jī)進(jìn)行調(diào)試,打開匯編模式;
就出現(xiàn)了如下圖所示的代碼,這邊它執(zhí)行了一個(gè)objc_retainBlock的跳轉(zhuǎn);

接下來(lái)我們手動(dòng)添加objc_retainBlock的斷點(diǎn):

執(zhí)行下一步之后,就會(huì)進(jìn)入到objc_retainBlock的匯編,按住control+step into,就進(jìn)入到了一個(gè)_Block_copy的匯編當(dāng)中:

到這里,就可以清楚的知道block所在的動(dòng)態(tài)庫(kù)在libsystem_blocks.dylib,因此可以在蘋果官網(wǎng)下載所需要的源碼。
在上面試過(guò)轉(zhuǎn)化的cpp文件存在Block_layout,在libsystem_blocks.dylib就有這個(gè)結(jié)構(gòu),它是一個(gè)結(jié)構(gòu)體,里面還有一個(gè)isa,block在底層真正的類型就是Block_layout,在源碼中,很多方法的參數(shù)都有Block_layout:

下面來(lái)研究一下block的全局,堆區(qū)和棧區(qū)地址的變化,將除了26行的斷點(diǎn)留下,其他斷點(diǎn)去掉,重新執(zhí)行程序,通過(guò)控制臺(tái)讀取寄存器信息:
下圖是__NSGlobalBlock__內(nèi)存信息:

下面嘗試一下捕獲外界變量,聲明一個(gè)a,在block中打印出來(lái),重新執(zhí)行程序:
在執(zhí)行程序之后,它并沒有跳轉(zhuǎn)到objc_retainBlock中來(lái),打印的x0信息不對(duì),在objc_retainBlock出打下斷點(diǎn),這時(shí)候讀x0信息,就是棧區(qū)block了,__NSStackBlock__:

__NSMallocBlock__是從__NSStackBlock__拷貝過(guò)去的,那么意味著預(yù)編譯的時(shí)候是__NSStackBlock__,然后讓block copy操作,當(dāng)進(jìn)入了_Block_copy匯編代碼中,在最后一行有一個(gè)ret的返回操作,在此處打斷點(diǎn):
如下圖所示,在經(jīng)過(guò)_Block_copy返回之后,block的內(nèi)存地址發(fā)生了變化,從0x000000016f837728變到0x0000000282e036c0,而__NSStackBlock__也變成了__NSMallocBlock__。

下圖是_Block_copy的底層源碼實(shí)現(xiàn),內(nèi)部實(shí)現(xiàn)了為什么從__NSStackBlock__轉(zhuǎn)換成__NSMallocBlock__:

總結(jié):在block捕獲外界變量時(shí),會(huì)從__NSStackBlock__經(jīng)過(guò)_Block_copy處理變成__NSMallocBlock__。
下面來(lái)看一下block的簽名,在block_layout的結(jié)構(gòu)體當(dāng)中,有很多屬性,其中就存在Block_descriptor_1類型的descriptor,而Block_descriptor_2和Block_descriptor_3都是可選類型,表示不是所有block都存在它們的一些屬性;

而在它們是如何辨別是否需要屬性呢?

看上圖,主要是通過(guò)枚舉值類型和進(jìn)行地址平移來(lái)獲得所需要的屬性:
看下圖的Block_descriptor的源碼實(shí)現(xiàn):

那現(xiàn)在去獲取block的簽名:
執(zhí)行程序,將程序卡在_Block_copy執(zhí)行完之后,讀取寄存器x0的信息:
最終獲取的__NSMallocBlock__的地址是0x0000000281e7c4e0,而在查看block_layout結(jié)構(gòu)之后,通過(guò)x/4gx獲取它的信息,其中第一個(gè)是isa的值,而第4個(gè)就是descriptor:

那我們清楚,descriptor的類型有1,2,3,其中2和3都是可選類型的,并不清楚它們是否存在,因此需要一個(gè)一個(gè)去嘗試,首先打印第四個(gè)地址的內(nèi)存情況,通過(guò)上面給的枚舉值屬性左移的位數(shù),來(lái)查看地址是否有值,經(jīng)過(guò)一翻查詢,Block_descriptor_2是沒有的,而Block_descriptor_3就存在值,在打印第三個(gè)地址之后,得到了它的簽名:

打印簽名信息:
