block在我們的代碼中經(jīng)常使用,通過block我們實現(xiàn)了高內(nèi)聚、低耦合,極大的方便了我們的編程,今天我們探究一下block的底層原理。
什么是block?
block是將函數(shù)及其執(zhí)行上下文封裝起來的對象。

通過
clang編譯查看底層代碼

其中
ViewController是文件名,viewDidLoad是方法名
-
搜索
__ViewController__viewDidLoad_block_impl_0
-
搜索
__ViewController__viewDidLoad_block_func_0
-
搜索
__block_impl
block內(nèi)部有isa指針,所以其本質(zhì)也是OC對象,他有4個屬性 isaFlagsReservedFuncPtr保存了方法實現(xiàn)的地址
所以說block是將函數(shù)及其執(zhí)行上下文封裝起來的對象。
既然block內(nèi)部封裝了函數(shù),那么它同樣也有參數(shù)和返回值。
block的類型
block有3種類型,全局block(__NSGlobalBlock__)、棧block(__NSStackBlock__)和堆block(__NSMallocBlock__)
- 全局
block
不使用外部變量的block是全局block
void (^block) (void) = ^ {
NSLog(@"%d",3);
};
NSLog(@"%@",block);
打印結(jié)果:

- 棧
block
使用外部變量,并且未進行copy操作的是棧block
int a = 10;
NSLog(@"%@",[^ {
NSLog(@"%d",a);
} class]);
打印結(jié)果:

- 堆
block
使用外部變量,并且進行了copy操作的是堆block
int a = 10;
void (^block) (void) = ^ {
NSLog(@"%d",a);
};
NSLog(@"%@",[block class]);
打印結(jié)果:

訪問了外部變量,并且強引用的是堆block
block的變量捕獲

- 用
clang編譯
結(jié)構(gòu)體的構(gòu)造方法,多了一個參數(shù)a

結(jié)構(gòu)體中的變量也增加了一個
a,而且是在編譯時就自動生成了。

函數(shù)方法中,用一個新的變量賦值,是值拷貝。
前面的a并不是我們寫入block中的a,這也就解釋了局部變量可以被捕獲,但是不能被修改的原因。要想修改a的值,需要用__block修飾。
-
__block
再次用clang編譯
在_I_ViewController_viewDidLoad方法中,多了一行。多了一個__Block_byref_a_0結(jié)構(gòu)體。 -
查看
__Block_byref_a_0
結(jié)構(gòu)體中有一個a,__forwarding保存的是a的地址 -
查看
__ViewController__viewDidLoad_block_impl_0
__ViewController__viewDidLoad_block_impl_0中多了一個__Block_byref_a_0類型的a -
再看
__ViewController__viewDidLoad_block_func_0
函數(shù)傳入的是_a->__forwarding,所以這里是指針拷貝,可以直接修改a的值。
block的循環(huán)引用
block的循環(huán)引用是經(jīng)常出現(xiàn)的問題,請看下一段代碼,dealloc方法在ViewController回退時是否會被調(diào)用?
@interface ViewController ()
@property (nonatomic,copy) NSString *name;
@property (nonatomic,copy) dispatch_block_t block;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.name = @"Tom";
self.block = ^{
NSLog(@"%@",self.name);
};
self.block();
}
- (void)dealloc {
NSLog(@"dealloc 來了");
}
@end
當ViewController退出的時候,打印結(jié)果如下:

dealloc方法沒有調(diào)用,發(fā)生了循環(huán)引用。

self持有block,block又捕獲self.name,self和block相互持有,發(fā)生循環(huán)引用。
解決辦法
方案一:自動釋放
解決辦法相信大家也都知道,只需要在block之前用__weak修飾self即可。

此時持有情況變?yōu)榱?br>
self -> block -> weakSelf -\-> self -> name,而weakSelf在一張弱引用表里,不會對self的引用計數(shù)產(chǎn)生影響。
然而這樣是有問題的

我們添加一段代碼,延遲2秒打印,進去后直接出來。

dealloc方法先走,才開始打印,這個時候VC已經(jīng)被釋放了,所以打印為null,self的生命周期并沒有得到保全。
- 添加一段代碼,以上情況得到解決
打印結(jié)果
self會在打印結(jié)束之后才釋放。引用關(guān)系鏈就變成了下面這樣:
self -> block -> strongSelf -> weakSelf -\-> self -> name
方案二:手動釋放
使用__block修飾符,在block中手動釋放VC。

因為
auto類型的局部變量可以被block捕獲,但不能修改,所以這里我們要借助__block就可以在block中修改了。
方案三:VC作為參數(shù)傳入
循環(huán)引用的原因是
self ->block -> self,所以我們只要不直接持有VC就可以解決問題。 把self通過傳參的方式傳進block,就能解決問題。
block的結(jié)構(gòu)與簽名
這里我們通過開啟匯編,看一下block的底層執(zhí)行流程。
- 新建一個工程,寫上如下代碼
- (void)viewDidLoad {
[super viewDidLoad];
void (^block)(void) = ^{
NSLog(@"hello ");
};
block();
}
新建一個工程是因為Xcode會對block的流程進行優(yōu)化緩存,暴露出來的信息會減少,不方便研究。
- 打上斷點,開啟
Always Show Disassembly
1. 沒有引用外界變量

查看匯編

可以看到調(diào)用了一個
objc_retainBlock方法
此時讀寄存器rax,block是一個NSGlobalBlock

-
添加
objc_retainBlock符號斷點
可以看到objc_retainBlock里面有一個_Block_copy方法 -
添加
_Block_copy符號斷點
可以看到,調(diào)用了libsystem庫里的方法。 下載libclosure-74源碼
-
搜索
_Block_copy
首先看到的是一個
Block_layout的結(jié)構(gòu)體,點擊查看
struct Block_layout {
void *isa;
volatile int32_t flags; // contains ref count
int32_t reserved;
BlockInvokeFunction invoke;
struct Block_descriptor_1 *descriptor;
// imported variables
};
這個結(jié)構(gòu)與我們在clang中看到的基本一致。
- 搜索查看
flags
// Values for Block_layout->flags to describe block objects
enum {
BLOCK_DEALLOCATING = (0x0001), // runtime 是否在析構(gòu)
BLOCK_REFCOUNT_MASK = (0xfffe), // runtime
BLOCK_NEEDS_FREE = (1 << 24), // runtime
BLOCK_HAS_COPY_DISPOSE = (1 << 25), // compiler 是否有copy 和dispose
BLOCK_HAS_CTOR = (1 << 26), // compiler: helpers have C++ code
BLOCK_IS_GC = (1 << 27), // runtime
BLOCK_IS_GLOBAL = (1 << 28), // compiler
BLOCK_USE_STRET = (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE
BLOCK_HAS_SIGNATURE = (1 << 30), // compiler 是否有簽名
BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31) // compiler
};
里面保存的是block的一些標識位。
- 搜索查看
Block_descriptor_1
#define BLOCK_DESCRIPTOR_1 1
struct Block_descriptor_1 {
uintptr_t reserved;
uintptr_t size;
};
我們查詢的時候還找到了相應(yīng)的Block_descriptor_2和Block_descriptor_3
#define BLOCK_DESCRIPTOR_2 1
struct Block_descriptor_2 {
// requires BLOCK_HAS_COPY_DISPOSE
BlockCopyFunction copy;
BlockDisposeFunction dispose;
};
#define BLOCK_DESCRIPTOR_3 1
struct Block_descriptor_3 {
// requires BLOCK_HAS_SIGNATURE
const char *signature;
const char *layout; // contents depend on BLOCK_HAS_EXTENDED_LAYOUT
};
這個Block_descriptor_1是每個block都有的,Block_descriptor_2和Block_descriptor_3不是每個block都有的,要看它是否有copy和dispose方法,一級signature和layout。配合上面的標識符,判斷會不會生成這兩個方法。
-
查看驗證
Block_descriptor_2和Block_descriptor_3
getter方法中,首先根據(jù)標識符判斷,沒有就會直接返回NULL,有這兩個結(jié)構(gòu)體時,才會根據(jù)Block_descriptor_1的地址進行內(nèi)存平移得到。 -
回到匯編,執(zhí)行完
objc_retainBlock,通過LLDB調(diào)試
詳細的驗證結(jié)果如下
block的簽名

v:表示無返回值8:參數(shù)大小為8@?:是對象類型,是block類型0:從0號位置開始
2. 捕獲外界變量

-
查看
block類型
此時是NSStackBlock -
執(zhí)行完
objc_retainBlock斷點
block已經(jīng)變?yōu)榱?code>NSMallocBlock
執(zhí)行objc_retainBlock中的_Block_copy方法后,block變?yōu)榱?code>NSMallocBlock。
- 繼續(xù)往下執(zhí)行到
callq
讀取SEL失敗,這里其實是一個_block_invoke方法,control+step into進入即可驗證 - 進入
_block_invoke
從匯編可以看出,在這里執(zhí)行了block里面的NSLog方法。
_Block_copy深入研究
- 查看
_Block_copy源碼
// Copy, or bump refcount, of a block. If really copying, call the copy helper if present.
void *_Block_copy(const void *arg) {
struct Block_layout *aBlock;
if (!arg) return NULL;
// 進來的block強轉(zhuǎn)為Block_layout類型
aBlock = (struct Block_layout *)arg;
if (aBlock->flags & BLOCK_NEEDS_FREE) {//是否需要被釋放
latching_incr_int(&aBlock->flags);
return aBlock;
}
else if (aBlock->flags & BLOCK_IS_GLOBAL) {//為global_block時直接返回,不做處理
return aBlock;
}
else {
// 編譯期不能直接生成堆block,這里只能是棧block
//棧block在這里copy
struct Block_layout *result =
(struct Block_layout *)malloc(aBlock->descriptor->size);//在堆上開辟一塊內(nèi)存
if (!result) return NULL;
memmove(result, aBlock, aBlock->descriptor->size); // 把原來的數(shù)據(jù)拷貝進來
#if __has_feature(ptrauth_calls)
//invoke也拷貝過來
result->invoke = aBlock->invoke;
#endif
// reset refcount
result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING); // XXX not needed
result->flags |= BLOCK_NEEDS_FREE | 2; // logical refcount 1
_Block_call_copy_helper(result, aBlock);
// 把isa的指向改為_NSConcreteMallocBlock
result->isa = _NSConcreteMallocBlock;
return result;
}
}
這個代碼很簡單。
block的三層拷貝

將以上代碼通過
clang編譯,細心的同學會發(fā)現(xiàn)還有兩段這樣的代碼
block_copy_0和block_dispose_0一起組裝成了block_desc_0_DATA,保存進Block_layout組,其實就是Block_descriptor_2中的兩個變量。
- 搜索
_Block_object_assign
根據(jù)case分為幾種情況,這里我們主要研究兩種:
-
BLOCK_FIELD_IS_OBJECT,普通id對象 -
BLOCK_FIELD_IS_BYREF,__block修飾的對象
enum {
// see function implementation for a more complete description of these fields and combinations
BLOCK_FIELD_IS_OBJECT = 3, // id, NSObject, __attribute__((NSObject)), block, ...
BLOCK_FIELD_IS_BLOCK = 7, // a block variable
BLOCK_FIELD_IS_BYREF = 8, // the on stack structure holding the __block variable
BLOCK_FIELD_IS_WEAK = 16, // declared __weak, only used in byref copy helpers
BLOCK_BYREF_CALLER = 128, // called from __block (byref) copy/dispose support routines.
};
BLOCK_FIELD_IS_OBJECT
static void (*_Block_retain_object)(const void *ptr) = _Block_retain_object_default;
static void _Block_retain_object_default(const void *ptr __unused) { }
case BLOCK_FIELD_IS_OBJECT:
/*******
id object = ...;
[^{ object; } copy];
********/
_Block_retain_object(object);
*dest = object;
break;
_Block_retain_object什么都沒有做,直接交給了ARC處理
dest指針指向了object,對對象內(nèi)存進行了持有,這也是block強引用的原因。
BLOCK_FIELD_IS_BYREF
case BLOCK_FIELD_IS_BYREF:
/*******
// copy the onstack __block container to the heap
// Note this __weak is old GC-weak/MRC-unretained.
// ARC-style __weak is handled by the copy helper directly.
__block ... x;
__weak __block ... x;
[^{ x; } copy];
********/
*dest = _Block_byref_copy(object);
break;
只是調(diào)用了_Block_byref_copy
- 查看
_Block_byref_copy
static struct Block_byref *_Block_byref_copy(const void *arg) {
//Block_byref結(jié)構(gòu)體,進來的變量在底層就是這個類型
struct Block_byref *src = (struct Block_byref *)arg;
if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
// 開辟內(nèi)存
struct Block_byref *copy = (struct Block_byref *)malloc(src->size);
copy->isa = NULL;//一些賦值操作
copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4;
copy->forwarding = copy; // forwarding 是外界變量的地址
src->forwarding = copy; // 這兩句代碼就是將外面變量a的指針,和里面copy都指向了新開辟的空間
copy->size = src->size;
//HAS_COPY_DISPOSE的時候
if (src->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) {
// Trust copy helper to copy everything of interest
// If more than one field shows up in a byref block this is wrong XXX
struct Block_byref_2 *src2 = (struct Block_byref_2 *)(src+1);
struct Block_byref_2 *copy2 = (struct Block_byref_2 *)(copy+1);
copy2->byref_keep = src2->byref_keep;
copy2->byref_destroy = src2->byref_destroy;
if (src->flags & BLOCK_BYREF_LAYOUT_EXTENDED) {
struct Block_byref_3 *src3 = (struct Block_byref_3 *)(src2+1);
struct Block_byref_3 *copy3 = (struct Block_byref_3*)(copy2+1);
copy3->layout = src3->layout;
}
(*src2->byref_keep)(copy, src);
}
else {
// Bitwise copy.
// This copy includes Block_byref_3, if any.
memmove(copy+1, src+1, src->size - sizeof(*src));
}
}
// already copied to heap
else if ((src->forwarding->flags & BLOCK_BYREF_NEEDS_FREE) == BLOCK_BYREF_NEEDS_FREE) {
latching_incr_int(&src->forwarding->flags);
}
return src->forwarding;
}





















