一 簡介
Objective-C 中的block
,是匿名函數(shù),匿名函數(shù)在別的語言中也被稱作閉包、λ表達式等。Objective-C 中的 block,有以下幾個特性:
- 可以將
block
作為函數(shù),也能作為對象,當(dāng)做屬性持有,定義內(nèi)存修飾詞。 - 可以長期存在,出了定義的作用域,內(nèi)部實現(xiàn)也能執(zhí)行。
- 可以捕獲外部變量,甚至使用
__block
修飾后,block 可以修改這個捕獲變量。
這背后的原理是什么呢,看了很多博客和代碼,理清楚了其中奧秘,在這里總結(jié)一下。
二 結(jié)構(gòu)
block
的結(jié)構(gòu),用clang -rewrite-objc
把objc
代碼轉(zhuǎn)成cpp
代碼,就能很清楚的看到了。在cpp
中,block
實際被編譯器轉(zhuǎn)寫成結(jié)構(gòu)體了。結(jié)構(gòu)體的介紹網(wǎng)上已經(jīng)有很多完善的資料了,我這里直接參考了這篇博客。
// objc 代碼
int test()
{
void (^blk)(void) = ^{
printf("Block\n");
};
blk();
return 0;
}
// cpp 重寫后
// 自己定義的 test 方法
int test()
{
// 聲明&賦值
void (*blk)(void) = ((void (*)())&__test_block_impl_0/*構(gòu)造函數(shù)*/(
(void *)__test_block_func_0,
&__test_block_desc_0_DATA)
);
// 執(zhí)行
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}
// 我們創(chuàng)建的 block 的結(jié)構(gòu)體
struct __test_block_impl_0 {
struct __block_impl impl; // 核心:函數(shù)指針
struct __test_block_desc_0* Desc; // 描述
// 構(gòu)造函數(shù)
__test_block_impl_0(void *fp, struct __test_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;// 棧 block
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
struct __block_impl {
void *isa; // isa指針,所以是objc對象
int Flags;
int Reserved;
void *FuncPtr; // 函數(shù)指針
};
// 靜態(tài)函數(shù)指針
static void __test_block_func_0(struct __test_block_impl_0 *__cself) {
printf("Block\n");
}
// 描述
static struct __test_block_desc_0 {
size_t reserved;
size_t Block_size; // 大小
} __test_block_desc_0_DATA = { 0, sizeof(struct __test_block_impl_0)}; // __test_block_desc_0_DATA 是這個結(jié)構(gòu)體的實例
這個結(jié)構(gòu)剛才引用的博客中已經(jīng)分析的很透徹了,這里就不一步步細(xì)講了。直接說一下我總結(jié)的結(jié)構(gòu)。
__xx_block_impl_0 // block完整結(jié)構(gòu)體
{
__block_impl, // 核心結(jié)構(gòu),具體結(jié)構(gòu)在下面
desc, // block 描述,具體結(jié)構(gòu)在下面
int foo, // 捕獲的變量,依次排列開
...
__xx_block_impl_0(fp, desc, param0,param1...)//構(gòu)造函數(shù)
}
struct __block_impl
{
id isa, // isa 指針
void *fp, // 函數(shù)指針
flag // 標(biāo)記位,用來描述 block 類型
}
block_desc // block 描述
{
size,// 大小
void *copy,// 捕獲對象時,捕獲變量們的copy 函數(shù)
void *dispose// 捕獲對象時,捕獲變量們的dispose 函數(shù)
}
// 靜態(tài)函數(shù)指針
static void __xx_block_func_0() {
// 具體實現(xiàn)
}
在講一下該結(jié)構(gòu)引出的比較讓人疑惑的點:
- 一個
block
定義會被編譯器clang
編譯生成一套特殊結(jié)構(gòu)體,依據(jù)block
行為不同(入?yún)ⅰ⒉东@變量),生成的結(jié)構(gòu)體也不一樣。從結(jié)構(gòu)體名稱類名__方法名__block__impl__序號
也可以知道,它是動態(tài)創(chuàng)建的。
相關(guān)的創(chuàng)建詳情可以參考clang
中genCode
模塊,GCBlocks。 - 結(jié)構(gòu)體是對象,分成三類:
- 棧block
_NSConcreteStackBlock
- 全局block
_NSConcreteGlobalBlock
- 堆block
_NSConcreteMallocBlock
詳細(xì)的區(qū)別參考這里。這里只說一點,棧block在賦值時,會被拷貝到堆上,這樣就通過引用計數(shù)管理了(引用計數(shù)的內(nèi)容可以看我的博客坑位、待填)。
關(guān)于這個拷貝操作,開始我并沒有在代碼中看見,只看見了賦值void (*blk)(void) = ((void (*)())&__test_block_impl_0...
,找了很久,才發(fā)現(xiàn)真實依據(jù)是NSObject.mm。
// The -fobjc-arc flag causes the compiler to issue calls to objc_{retain/release/autorelease/retain_block}
id objc_retainBlock(id x) {
return (id)_Block_copy(x);
}
- 一個奇怪的細(xì)節(jié):
block
聲明時,得到的是__test_block_impl_0
類型變量blk
;執(zhí)行時,卻轉(zhuǎn)成了__block_impl
類型。
這里是因為__block_impl
是__test_block_impl_0
的第一個變量,二者的指針地址相同的,所以可以直接強轉(zhuǎn),詳見這篇的 2.3 部分。
三 參數(shù)捕獲
上面說到block
捕獲的變量不同,動態(tài)生成的結(jié)構(gòu)也不一樣。block可以捕獲:objc 對象
、基礎(chǔ)數(shù)據(jù)類型
、另一個 block
。我們常用的是捕獲基礎(chǔ)數(shù)據(jù)類型和 objc 對象
和基礎(chǔ)數(shù)據(jù)類型
。
下面就常用情況再分成四種情況討論:
這部分有大牛珠玉在前,解釋的比較透徹了。所以我就簡單總結(jié)一下:
- 捕獲基礎(chǔ)數(shù)據(jù),在
block
結(jié)構(gòu)中,會將捕獲參數(shù)添加進去。前面講了,在block
賦值時,會被執(zhí)行一個_Block_copy
操作,這其中對整個block
結(jié)構(gòu)進行拷貝,捕獲的基礎(chǔ)數(shù)據(jù)值也會被拷貝到堆上。 - 捕獲對象變量時,除了會被添加到
block
結(jié)構(gòu)體中,還會額外生成一對被捕獲變量的拷貝函數(shù)
和銷毀函數(shù)
,保存在__block_desc
結(jié)構(gòu)中。這里面描述了被捕獲的變量,如何被拷貝
到堆上去。
// 捕獲變量的copy
// _Block_copy 的時候會調(diào)用
// _Block_copy 在 block 被賦值時候調(diào)用
static void __TestClass__testMethod_block_copy_2(struct __TestClass__testMethod_block_impl_2*dst, struct __TestClass__testMethod_block_impl_2*src) {
_Block_object_assign((void*)&dst->tmpB, (void*)src->tmpB, 8/*BLOCK_FIELD_IS_BYREF*/);
_Block_object_assign((void*)&dst->self, (void*)src->self, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
// 捕獲變量的dispose
// _Block_release 的時候會調(diào)用
// _Block_release 的調(diào)用時機是堆 block 引用計數(shù)為 0 時
static void __TestClass__testMethod_block_dispose_2(struct __TestClass__testMethod_block_impl_2*src) {
_Block_object_dispose((void*)src->tmpB, 8/*BLOCK_FIELD_IS_BYREF*/);
_Block_object_dispose((void*)src->self, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
- 捕獲
__block
修飾的基礎(chǔ)數(shù)據(jù),該基礎(chǔ)數(shù)據(jù)會被轉(zhuǎn)化成一個特殊的結(jié)構(gòu)體:
// 定義處,在棧上
__block int tmpB = 1;
void(^blk003)(int a) = ^(int a) {
// 使用處,在堆上
NSLog(@"tmpB=%d", tmpB);
};
// __block 修飾的 int 變量 tempB
struct __Block_byref_tmpB_0 {
void *__isa;
__Block_byref_tmpB_0 *__forwarding;
int __flags;
int __size;
int tmpB;
};
我們稱它為byref 封裝
,可以看到它也是一個 objc 對象
。所以byref 封裝
和被捕獲變量一樣,會被添加到block 結(jié)構(gòu)
里之外,還會生成copy
和dispose
函數(shù)指針。
byref 封裝
除了封裝了自身的實際值之外,還持有一個自己類型的__forwarding
指針。當(dāng)byref 封裝
在棧上的時候,__forwarding
指針會指向它堆上的拷貝,在堆上的時候回指向自己。這樣就保證了定義處和使用處調(diào)用方式是一樣的,至于源碼是如何實現(xiàn)的,下面會講到。
- 捕獲
__block
修飾的對象,同樣也會生成一個byref 封裝
,結(jié)構(gòu)也和上一條類似:
__block NSString * tempC = [NSString stringWithFormat:@"1"];
void (^test)() = ^ {
NSLog(@"%@",tempC);
};
// __block 修飾的 int 變量 tempC
struct __Block_byref_tempC_0 {
void *__isa;
__Block_byref_tmpC_0 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);//實際賦值__Block_byref_id_object_copy_131
void (*__Block_byref_id_object_dispose)(void*);//同上
NSString *tempC;
};
static void __Block_byref_id_object_copy_131(void *dst, void *src) {
_Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}
static void __Block_byref_id_object_dispose_131(void *src) {
_Block_object_dispose(*(void * *) ((char*)src + 40), 131);
}
但是多處量函數(shù)指針變量,這倆變量在創(chuàng)建時,被傳值__Block_byref_id_object_copy_131
和__Block_byref_id_object_dispose_131
。這倆在byref 封裝
拷貝時會調(diào)用到,用來拷貝NSString *tempC
的。
總結(jié):可以看到 __xx_block_impl_0
結(jié)構(gòu)隨著捕獲變量的復(fù)雜而變得復(fù)雜。而block
從棧到堆的拷貝,是由自身結(jié)構(gòu)
->捕獲變量
->byref的value
,層層進行的。
四 堆 block 拷貝
上面總結(jié)了拷貝的流程,這一節(jié)擼一下代碼,一來加深印象,二來找下幾個常見問題的答案:
- 在 block 中被修改了,棧上的原值也會被修改嗎?
- 循環(huán)引用是如何產(chǎn)生的,
weak-strong-dance
為何能解決? - 使用
weak-strong-dance
,被引用對象何時會為 nil ?
block
結(jié)構(gòu)體的拷貝方法 _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;
// The following would be better done as a switch statement
aBlock = (struct Block_layout *)arg;
if (aBlock->flags & BLOCK_NEEDS_FREE) {
// latches on high
latching_incr_int(&aBlock->flags);
return aBlock;
}
else if (aBlock->flags & BLOCK_IS_GLOBAL) {
return aBlock;
}
else {
// Its a stack block. Make a copy.
// !!!!開辟堆上內(nèi)存空間
struct Block_layout *result =
(struct Block_layout *)malloc(aBlock->descriptor->size);
if (!result) return NULL;
// !!!!將內(nèi)容移到指定堆上
memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
#if __has_feature(ptrauth_calls)
// Resign the invoke pointer as it uses address authentication.
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);
// Set isa last so memory analysis tools see a fully-initialized object.
result->isa = _NSConcreteMallocBlock;
return result;
}
}
_Block_call_copy_helper
參數(shù)拷貝助手
static void _Block_call_copy_helper(void *result, struct Block_layout *aBlock)
{
struct Block_descriptor_2 *desc = _Block_descriptor_2(aBlock);
if (!desc) return;
(*desc->copy)(result, aBlock); // do fixup
}
static void __TestClass__testMethod_block_copy_2(struct __TestClass__testMethod_block_impl_2*dst, struct __TestClass__testMethod_block_impl_2*src) {
_Block_object_assign((void*)&dst->tmpB, (void*)src->tmpB, 8/*BLOCK_FIELD_IS_BYREF*/);
_Block_object_assign((void*)&dst->self, (void*)src->self, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
_Block_object_assign
參數(shù)拷貝函數(shù)
void _Block_object_assign(void *destArg, const void *object, const int flags) {
const void **dest = (const void **)destArg;
switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
case BLOCK_FIELD_IS_OBJECT:
/*******
id object = ...;
[^{ object; } copy];
********/
_Block_retain_object(object);
// static void _Block_retain_object_default(const void *ptr __unused) { }
// static void _Block_destructInstance_default(const void *aBlock __unused) {}
// !!!!都是空方法
*dest = object;
break;
case BLOCK_FIELD_IS_BLOCK:
/*******
void (^object)(void) = ...;
[^{ object; } copy];
********/
// !!!!block 參數(shù),再調(diào)一次 `_Block_copy`
*dest = _Block_copy(object);
break;
case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
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];
********/
// !!!!__block 修飾的基礎(chǔ)數(shù)據(jù)類型
*dest = _Block_byref_copy(object);
break;
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
/*******
// copy the actual field held in the __block container
// Note this is MRC unretained __block only.
// ARC retained __block is handled by the copy helper directly.
__block id object;
__block void (^object)(void);
[^{ object; } copy];
********/
// !!!!__block 修飾的對象
// !!!!直接賦值,對其引用計數(shù)+1
*dest = object;
break;
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT | BLOCK_FIELD_IS_WEAK:
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK | BLOCK_FIELD_IS_WEAK:
/*******
// copy the actual field held in the __block container
// Note this __weak is old GC-weak/MRC-unretained.
// ARC-style __weak is handled by the copy helper directly.
__weak __block id object;
__weak __block void (^object)(void);
[^{ object; } copy];
********/
// !!!!__block __weak 雙重修飾的對象
// !!!!直接賦值,對其引用計數(shù)+1
// !!!!相當(dāng)于 __weak 沒有起作用
*dest = object;
break;
default:
break;
}
}
_Block_byref_copy
對 __forwarding
指針的操作
// Runtime entry points for maintaining the sharing knowledge of byref data blocks.
// A closure has been copied and its fixup routine is asking us to fix up the reference to the shared byref data
// Closures that aren't copied must still work, so everyone always accesses variables after dereferencing the forwarding ptr.
// We ask if the byref pointer that we know about has already been copied to the heap, and if so, increment and return it.
// Otherwise we need to copy it and update the stack forwarding pointer
static struct Block_byref *_Block_byref_copy(const void *arg) {
struct Block_byref *src = (struct Block_byref *)arg;
// !!!! 通過引用計數(shù)來判斷原 byref 的 forwarding 是否為堆上變量
if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
// src points to stack
struct Block_byref *copy = (struct Block_byref *)malloc(src->size);
copy->isa = NULL;
// byref value 4 is logical refcount of 2: one for caller, one for stack
copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4;
// !!!! 堆上 byref 指向自己地址本身
copy->forwarding = copy; // patch heap copy to point to itself
// !!!! 原棧上 byref 指向堆上地址
src->forwarding = copy; // patch stack to point to heap copy
copy->size = src->size;
// !!!! 判斷有 copy 或 dispose 函數(shù),會執(zhí)行相應(yīng)函數(shù)
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) {
//!!!! 引用計數(shù)+1 難道所有對象的引用計數(shù),都是用 flag 來表示的??
latching_incr_int(&src->forwarding->flags);
}
return src->forwarding;
}
static int32_t latching_incr_int(volatile int32_t *where) {
while (1) {
int32_t old_value = *where;
if ((old_value & BLOCK_REFCOUNT_MASK) == BLOCK_REFCOUNT_MASK) {
return BLOCK_REFCOUNT_MASK;
}
if (OSAtomicCompareAndSwapInt(old_value, old_value+2, where)) {
return old_value+2;
}
}
}
五 總結(jié)
關(guān)于 __block 修飾的變量,怎么同修改的
1、__block 修飾的變量,會生成一個結(jié)構(gòu)體 blk_byref。里面有一個 forwarding 指針、一個實際值,開始時 forwarding 指向自己。
2、在 blk 賦值時,(void (^blk2)(void) = ^{ .....} )會把 blk copy 到堆上,同時 copy 捕獲的參數(shù)(剛才的棧上 blk_byref)到堆上。 見 _Block_copy 方法。
3、copy 捕獲的參數(shù)時,設(shè)置 forwarding 指針,棧->堆,堆->自己。見 _Block_object_assign 方法。
4、一個參數(shù)如果被多個 blk 捕獲了(如果 forwarding 指針有值),在 copy 時候,就不在堆上創(chuàng)建新的 blk_byref,這樣就能保證一個變量被多個blk捕獲時,有且只有一份堆結(jié)構(gòu) blk_byref。見 _Block_byref_copy 方法。
5、實際上,所有訪問操作(blk內(nèi),blk外),都是 blk_byref->forwardig->val,因為棧/堆forwarding都到堆上了,最后實際修改的都是堆上的blk_ref ->val,不會改棧上blk_ref ->val。
sunnyxx 面試題
看完了上面內(nèi)容,可以用下面的幾道題測試下自己的掌握程度
參考:
破弓的《iOS Block》系列
libclosure 源碼
objc4 源碼
http://www.lxweimin.com/p/51d04b7639f1
http://www.lxweimin.com/p/b554e813fce1
http://www.lxweimin.com/p/e42f86a81045
http://clang.llvm.org/docs/Block-ABI-Apple.html#block-escapes
GCBlocks