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后,越來越少,因此需要使用__weak使其不在強持有。
int a = 10;
void (^__weak block)(void) = ^{
NSLog(@"block - %d!",a);
};
NSLog(@"%@",block);
<__NSStackBlock__: 0x7ffeeba41478>
全局訪問外界變量強引用變成堆區(qū),弱引用變成棧區(qū)。
既然是block,那就存在循環(huán)引用問題,那就先要了解循環(huán)引用的概念,按照正常的流程來說,例如A持有B,B的引用計數(shù)加1,而當A發(fā)送dealloc信號之后,B的引用計數(shù)需要減1變?yōu)?,那么dealloc才會正常被調(diào)用;而循環(huán)引用就是A持有B,B也持有A,構(gòu)成了相互持有,那么在釋放的時候,誰也釋放不了對方,就造成了循環(huán)引用問題。
那么如何解決循環(huán)引用問題呢?
來看一段代碼:
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án)引用的,因為self引用了block,而bloc也引用了self;類似于self -> block ->self;
那么解決循環(huán)引用,相信很多人都知道是用__weak;它加入了一張弱引用表,增加__weak typeof(self) weakSelf = self;這一行實現(xiàn)弱引用,就類似于self -> block ->weakSelf -> self;
那么weakSelf持有強引用對象self,引用計數(shù)是不會增加的,因此weakSelf持有的self在weakSelf生命周期結(jié)束之后,也就進行釋放了。
下面是執(zhí)行的結(jié)果:

那么這種方式來解決循環(huá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í)行后,還沒來得及調(diào)用,self就被釋放了,因此self的生命周期是不足以得到保證的。

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

這樣的強引用對象是在block函數(shù)調(diào)用結(jié)束之后,就會進行釋放。
那么使用__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)引用的方式之一,他是自動釋放,下面介紹第二種解決方式,手動釋放,看代碼:
__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對ViewController賦值self,通過在輸出之后,手動將vc置為nil。類似于self->block-> vc=nil ->self;vc被block捕獲,無法自動釋放,那么手動釋放,就解決了釋放這一問題。
接下來介紹第三種解決循環(huán)引用問題,那就是通過參數(shù)來解決問題:
看代碼
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的使用之后,下面來看一下block的底層原理,首先通過xcrun來看一下block的cpp是如何實現(xiàn)的:
#include "stdio.h"
int main(){
void(^block)(void) = ^{
printf("Block - ");
};
block();
return 0;
}
上面的c代碼通過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是一個結(jié)構(gòu)體:


也就是說,block的本質(zhì)就是對象結(jié)構(gòu)體,以函數(shù)作為參數(shù)傳入進來;
由impl.FuncPtr = fp;可以知道block是需要具體函數(shù)實現(xiàn)的;
而*__cself作為匿名參數(shù),因此可以獲取block內(nèi)部的代碼,并執(zhí)行。
那么如果有外界參數(shù)時,block又是如何實現(xiàn)的呢?
通過轉(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的值在編譯時,就自動生成了相應的變量,而在__main_block_func_0的方法中,就通過了一種賦值拷貝的方式賦值給a,但是里面的a和外面的a是不一樣的。
那么對里面的a進行加加,在轉(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是進行了指針拷貝,也就是說兩個變量a同時指向同一片內(nèi)存空間。
總結(jié):
block本質(zhì)一個對象結(jié)構(gòu)體,匿名函數(shù),block自動捕獲外界變量生成同一個屬性來保存,block調(diào)用block()是因為函數(shù)申明,需要有具體的函數(shù)實現(xiàn);而__block的原理就是生成相應的結(jié)構(gòu)體,保存原始變量進行指針拷貝,傳遞指針地址給block。
那到現(xiàn)在為止,其實還沒有探索到一些核心的底層原理,還不清楚,__NSGlobalBlock__,__NSStackBlock__,__NSMallocBlock__在內(nèi)存地址中是如何變化的,以及關于block調(diào)用的問題。
下面我們探索一下運行時的block。
首先創(chuàng)建工程,代碼很簡單:

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

接下來我們手動添加objc_retainBlock的斷點:

執(zhí)行下一步之后,就會進入到objc_retainBlock的匯編,按住control+step into,就進入到了一個_Block_copy的匯編當中:

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

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

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

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

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

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

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

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

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

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

打印簽名信息:
