一、Objective-C發(fā)展史
Objective-C從1983年誕生,已經(jīng)走過了30多年的歷程。隨著時(shí)間的推移,Objective-C支持很多特性,下面是幾個(gè)重要的發(fā)展節(jié)點(diǎn):
- Object Oriented C —1983
- Retain and Release
- Properties —2006(Objective-C2.0發(fā)布)
- Blocks —Mac OS X 10.6(June 8, 2009) & iOS4.0(June 21, 2010)
- ARC —Mac OS X 10.6&iOS5(2011)
以上發(fā)展節(jié)點(diǎn)來自這里,時(shí)間來自維基百科和其他網(wǎng)站。至于為什么會(huì)先說這些,主要我發(fā)現(xiàn)很多同學(xué)在了解一項(xiàng)技術(shù)時(shí),沒有一個(gè)立體的概念,經(jīng)常看后會(huì)很快忘記。當(dāng)我們對一個(gè)知識的來龍去脈了解時(shí),這個(gè)知識點(diǎn)就不再是個(gè)點(diǎn),而會(huì)變成一個(gè)面,起到幫助我們記憶的作用。另外我建議大家深入學(xué)習(xí)某項(xiàng)技術(shù)時(shí),不要僅看一些人的分享,現(xiàn)在知識分享已經(jīng)沒有門檻,經(jīng)常會(huì)看到人云亦云的博客(當(dāng)然也可能包括我:)),最好的方式就是先從博客上了解知識,然后從官方途徑上去學(xué)習(xí)驗(yàn)證。推薦大家?guī)讉€(gè)網(wǎng)站,第一個(gè)當(dāng)然是蘋果的開發(fā)文檔;第二個(gè)就是WWDC視頻;如果你還是不盡興,想從源碼方面上了解,可以去看下蘋果開放的源碼-runtime、libDispatch、libclosure,和clang文檔
二、Block的由來
閉包
在講Block之前,我們先了解下閉包的概念:
在計(jì)算機(jī)科學(xué)中,閉包(英語:Closure),又稱詞法閉包(Lexical Closure)或函數(shù)閉包(function closures),是引用了自由變量的函數(shù)。這個(gè)被引用的自由變量將和這個(gè)函數(shù)一同存在,即使已經(jīng)離開了創(chuàng)造它的環(huán)境也不例外。—維基百科
從上面維基百科的解釋中,我們可以看到,一個(gè)閉包即有捕獲自由變量的特性,又具有函數(shù)的特性。閉包在我們?nèi)粘9ぷ髦校?jīng)常用于回調(diào),尤其在延遲調(diào)用(也稱為惰性求值)的情況下非常有用。比如我們經(jīng)常需要在網(wǎng)絡(luò)請求完成時(shí)更新UI,網(wǎng)絡(luò)請求需要一定時(shí)間,在請求結(jié)束時(shí)再調(diào)用更新UI的方法。
Block
先從蘋果官方文檔上,看下關(guān)于Block的描述
Block objects are a C-level syntactic and runtime feature. They are similar to standard C functions, but in addition to executable code they may also contain variable bindings to automatic (stack) or managed (heap) memory. A block can therefore maintain a set of state (data) that it can use to impact behavior when executed.
簡單來講Block是Apple實(shí)現(xiàn)的閉包,它適用于C++,Objective-C,Objective-C++。另外Block需要runtime和clang的支持,下面會(huì)講到
三、Block使用
先看一個(gè)使用Block的場景,代碼如下:
int multiplier = 7;
int (^myBlock)(int) = ^(int num) {
return num * multiplier;
};
Block的使用和函數(shù)指針使用非常相似,最大的不同,可能就是函數(shù)指針的*變成了^。下面我們看下Block有哪些東西
三、Block存儲域
在iOS開發(fā)過程中,有三種不同類型的Block,分別是:
-
_NSConcreteStackBlock
, 棧上的Block,在出了作用域之后會(huì)被釋放 -
_NSConcreteMallocBlock
,堆上的Block,在MRR環(huán)境需要手動(dòng)釋放 -
_NSConcreteGlobalBlock
,全局的Block,存在data區(qū)域,生命周期和應(yīng)用一樣
在我們初始化Block時(shí)只有_NSConcreteStackBlock/_NSConcreteGlobalBlock
兩種類型。注意我們是無法生成_NSConcreteMallocBlock
的,只有在_NSConcreteStackBlock
調(diào)用__Block_copy
時(shí)才會(huì)被copy到堆上,生成_NSConcreteMallocBlock
。這一點(diǎn)可以從clang文檔中了解到,可能現(xiàn)在看懂這些還比較困難,沒關(guān)系,我們下面在Block內(nèi)部實(shí)現(xiàn)會(huì)講到。
有兩個(gè)問題需要我們注意下:
-
在初始化Block時(shí),我們怎么判斷是
_NSConcreteStackBlock
還是_NSConcreteGlobalBlock
?這里有個(gè)快速判斷的方法。如果Block的body里使用到了外部的非全局變量和非static靜態(tài)變量,那么這個(gè)Block就會(huì)在棧上創(chuàng)建即
_NSConcreteStackBlock
。反之如果沒有引用變量或者僅引用了全局變量或者static靜態(tài)變量則是全局Block_NSConcreteGlobalBlock
,下面有幾個(gè)例子:// _NSConcreteGlobalBlock int multiplier = 7; // 全局變量 { int (^myBlock)(int) = ^(int num) { return num * multiplier; }; } // _NSConcreteGlobalBlock static int multiplier = 7; // 靜態(tài)變量 int (^myBlock)(int) = ^(int num) { return num * multiplier; }; // _NSConcreteStackBlock MRR環(huán)境 int multiplier = 7; // 局部變量或者實(shí)例變量 int (^myBlock)(int) = ^(int num) { return num * multiplier; }; // _NSConcreteMallocBlock ARC環(huán)境 // 注意這里雖然在打印myBlock時(shí)顯示為NSMallocBlock,但是這并不是說我們創(chuàng)建出了_NSConcreteMallocBlock,而是被隱式地調(diào)用了copy int multiplier = 7; // 局部變量或者實(shí)例變量 int (^myBlock)(int) = ^(int num) { return num * multiplier; };
-
Block在MRR環(huán)境和ARC環(huán)境下使用時(shí),需要注意哪些情況?
1、在MRR環(huán)境下系統(tǒng)提供了兩種方式把Block從棧上copy到堆上,第一種是顯式調(diào)用copy,第二種是顯式調(diào)用Block_copy宏。當(dāng)然如果使用屬性的話,則需要用copy標(biāo)記,如
@property (copy) int(^Blk)(int)
而使用retain標(biāo)記屬性是不行的,大家可以試一下
2、在ARC環(huán)境下,編譯器幫助我們自動(dòng)插入了copy操作,省去了我們很多的工作量,以下幾種情況編譯器會(huì)隱式執(zhí)行copy操作:
- 將Block作為函數(shù)返回值時(shí)
- 將Block賦值給
__strong
修改的局部變量,或者標(biāo)記為strong
和copy
的屬性時(shí) - 向Cocoa框架含有usingBlock的方法或者GCD的API傳遞Block參數(shù)時(shí)
雖然上面的幾種情況幾乎涵蓋了所有Block使用場景,不過仍然一種情況需要我們考慮到,就是把Block當(dāng)方法參數(shù)傳遞給我們自定義方法時(shí)進(jìn)行傳遞時(shí),系統(tǒng)是不幫我們copy的,在使用時(shí)需要我們手動(dòng)進(jìn)行copy,如:
{ int multiplier = 7; print(^(int num) { return num * multiplier; };) } void print(Blk blk) { // NSStackBlock blk(1); }
四、Block的內(nèi)部實(shí)現(xiàn)
在看源碼之前,我們先大概捋下如果讓我們自己實(shí)現(xiàn)一個(gè)閉包該怎么實(shí)現(xiàn),或者需要考慮什么?比如如何捕獲外部變量;如何管理外部變量的內(nèi)存;調(diào)用方式是怎樣的?如果解決了這些問題,我們也能寫一個(gè)閉包的實(shí)現(xiàn),無非是沒有編譯器的加持,調(diào)用起來不是那么優(yōu)雅。好了,說了這么多,我們帶著這些疑問去看Block的實(shí)現(xiàn)源碼,思路會(huì)更加清晰。
我們先準(zhǔn)備好clang里的說明文檔(這個(gè)文檔解釋Block內(nèi)部實(shí)現(xiàn))和Block實(shí)現(xiàn)源碼。
Block的實(shí)現(xiàn)主要包括四個(gè)文件:Block.h
、Block_private.h
、data.c
、runtime.c
-
Block.h
// Create a heap based copy of a Block or simply add a reference to an existing one. // This must be paired with Block_release to recover memory, even when running // under Objective-C Garbage Collection. BLOCK_EXPORT void *_Block_copy(const void *aBlock); // Lose the reference, and if heap based and last reference, recover the memory BLOCK_EXPORT void _Block_release(const void *aBlock); // Used by the compiler. Do not call this function yourself. BLOCK_EXPORT void _Block_object_assign(void *, const void *, const int); // Used by the compiler. Do not call this function yourself. BLOCK_EXPORT void _Block_object_dispose(const void *, const int); // Used by the compiler. Do not use these variables yourself. BLOCK_EXPORT void * _NSConcreteGlobalBlock[32]; BLOCK_EXPORT void * _NSConcreteStackBlock[32]; #if __cplusplus } #endif // Type correct macros #define Block_copy(...) ((__typeof(__VA_ARGS__))_Block_copy((const void *)(__VA_ARGS__))) #define Block_release(...) _Block_release((const void *)(__VA_ARGS__))
這個(gè)文件包括Block向外提供的方法,我們可以看到
Block_copy
宏實(shí)現(xiàn),其實(shí)就是調(diào)用了_Block_copy
方法,Block_release
宏調(diào)用了_Block_release
方法,所以我們也可以將宏調(diào)用改為方法調(diào)用。這個(gè)文件是暴露出去的,我們可以在工程中看到:路徑在/usr/include/Block.h
,這四個(gè)文件只有這個(gè)文件是暴露的,用來給我們調(diào)用 -
data.c
BLOCK_EXPORT void * _NSConcreteStackBlock[32] = { 0 }; BLOCK_EXPORT void * _NSConcreteMallocBlock[32] = { 0 }; BLOCK_EXPORT void * _NSConcreteAutoBlock[32] = { 0 }; // only used in GC BLOCK_EXPORT void * _NSConcreteFinalizingBlock[32] = { 0 }; // only used in GC BLOCK_EXPORT void * _NSConcreteGlobalBlock[32] = { 0 }; BLOCK_EXPORT void * _NSConcreteWeakBlockVariable[32] = { 0 }; // only used in GC
這個(gè)文件定義了6種Block類型。其中三種類型只有GC環(huán)境才會(huì)出現(xiàn),因?yàn)镚C在ARC推出后,已被蘋果廢棄了。所以我們只關(guān)心其中三種類型即可,這也印證了我們上面說的三種Block類型
BLOCK_EXPORT void * _NSConcreteStackBlock[32] = { 0 }; BLOCK_EXPORT void * _NSConcreteMallocBlock[32] = { 0 }; BLOCK_EXPORT void * _NSConcreteGlobalBlock[32] = { 0 };
-
Block_private.h
這個(gè)文件就是Block的具體結(jié)構(gòu)
-
runtime.c
這個(gè)文件描述了Block的copy/release和Block持有變量的copy/release操作。
好了,這就是Block的所有文件,Block_private.h和runtime.c兩個(gè)文件是我們需要重點(diǎn)關(guān)注。哦,其實(shí)還有一個(gè)關(guān)聯(lián)的文件沒說,先放著不管,在講到runtime.c會(huì)引出該文件,這個(gè)文件很重要,如果沒有這個(gè)文件我們在閱讀到某塊代碼時(shí)會(huì)有點(diǎn)莫名其妙。
Block的內(nèi)部結(jié)構(gòu)
首先我們從Block內(nèi)部結(jié)構(gòu)說起,上面我們已經(jīng)講到,它在Block_private.h文件中,先貼下Block的結(jié)構(gòu)代碼
#define BLOCK_DESCRIPTOR_1 1
/* Block描述1
* 如果是_NSConcreteStackBlock時(shí),在copy到堆上時(shí),需要確定Block的大小,用來在堆上分配空間
*/
struct Block_descriptor_1 {
uintptr_t reserved;
uintptr_t size; // Block's size
};
#define BLOCK_DESCRIPTOR_2 1
/* Block描述2
* 如果是_NSConcreteStackBlock并且捕獲了__block修飾的變量或者(id, NSObject, __attribute__((NSObject)), block, ...)類型的變量,在copy到堆上時(shí)需要生成copy方法,這個(gè)方法用來解決捕獲的變量在被copy時(shí)要執(zhí)行的動(dòng)作。
* 如果是_NSConcreteGlobalBlock則不會(huì)生成該結(jié)構(gòu)
*/
struct Block_descriptor_2 {
// requires BLOCK_HAS_COPY_DISPOSE
void (*copy)(void *dst, const void *src);
void (*dispose)(const void *);
};
// Block的內(nèi)部布局
struct Block_layout {
void *isa; // initialized to &_NSConcreteStackBlock or &_NSConcreteGlobalBlock
volatile int32_t flags; // contains ref count 這個(gè)值主要用來告知系統(tǒng)Block在copy時(shí)應(yīng)該執(zhí)行什么操作
int32_t reserved;
void (*invoke)(void *, ...); // function ptr
struct Block_descriptor_1 *descriptor;
// imported variables
};
從上面我們可以了解到,Block由三個(gè)struct構(gòu)成,每個(gè)struct我都做了詳細(xì)的注釋。Block_layout
中的flags
在文件中也有描述:
// Values for Block_layout->flags to describe block objects
enum {
BLOCK_DEALLOCATING = (0x0001), // runtime only use in GC
BLOCK_REFCOUNT_MASK = (0xfffe), // runtime
BLOCK_NEEDS_FREE = (1 << 24), // runtime
BLOCK_HAS_COPY_DISPOSE = (1 << 25), // compiler
BLOCK_HAS_CTOR = (1 << 26), // compiler: helpers have C++ code only use in GC
BLOCK_IS_GC = (1 << 27), // runtime only use in GC
BLOCK_IS_GLOBAL = (1 << 28), // compiler
BLOCK_USE_STRET = (1 << 29), // compiler: undefined if !BLOCK_HAS_SIGNATURE only use in GC
BLOCK_HAS_SIGNATURE = (1 << 30), // compiler only use in GC
BLOCK_HAS_EXTENDED_LAYOUT=(1 << 31) // compiler only use in GC
};
去掉GC環(huán)境需要的值
// Values for Block_layout->flags to describe block objects
enum {
BLOCK_REFCOUNT_MASK = (0xfffe), // runtime
BLOCK_NEEDS_FREE = (1 << 24), // runtime
BLOCK_HAS_COPY_DISPOSE = (1 << 25), // compiler
BLOCK_IS_GLOBAL = (1 << 28), // compiler
};
flags
只有這四種值需要我們關(guān)注,其中是BLOCK_HAS_COPY_DISPOSE
和BLOCK_IS_GLOBAL
是我們在使用block時(shí)由編譯器根據(jù)上下文生成的。另外兩個(gè)是在Block被copy時(shí),runtime用到和修改的,這里的runtime特指runtime.c文件,在分析runtime.c文件時(shí),再具體說明。
Block在捕獲非全局和非靜態(tài)變量時(shí),都是copy的不可變變量,在Block的body里是不能修改這個(gè)值的,編譯器會(huì)給我們提示如:
(注解:Block的body里使用全局變量或者靜態(tài)變量,這些變量并不會(huì)進(jìn)行copy,它只做使用,也不會(huì)去管理這些變量的內(nèi)存,如果Block僅僅使用了全局變量或者靜態(tài)變量,Block為_NSConcreteGlobalBlock
類型)
如果需要修改變量值,我們可以通過__block
修飾變量:
__block int multiplier = 7; // 局部變量或者實(shí)例變量
int (^myBlock)(int) = ^(int num) {
multiplier = 5;
return num * multiplier;
};
myBlock(1);
__block
修飾的變量會(huì)被Block_byref
這樣的結(jié)構(gòu)包起來,具體如下
struct Block_byref {
void *isa;
struct Block_byref *forwarding; // 初始化時(shí)會(huì)指向自己,當(dāng)Block被copy時(shí),Block_byref也會(huì)被copy到堆上,forwarding會(huì)指向堆上的Block_byref
volatile int32_t flags; // contains ref count
uint32_t size; // Block_byref大小,用來copy時(shí)分配內(nèi)存
};
// Block_byref被copy到堆上和釋放時(shí)需要的操作
struct Block_byref_2 {
// requires BLOCK_BYREF_HAS_COPY_DISPOSE
void (*byref_keep)(struct Block_byref *dst, struct Block_byref *src);
void (*byref_destroy)(struct Block_byref *);
};
如果變量屬于id, NSObject, __attribute__((NSObject)), block, …
這種類型,則會(huì)生成byref_keep
和byref_destroy
方法,來管理變量的內(nèi)存,如果是修飾的是自動(dòng)變量如int,CGPoint,enum類型,則不會(huì)生成結(jié)構(gòu)體Block_byref_2
Block如何管理內(nèi)存
這里引出另外一個(gè)文件runtime.c,該文件包含了Block的copy/release、id, NSObject, __attribute__((NSObject)), block, …
類型變量的copy/release和__block
修飾的變量的copy/release的具體實(shí)現(xiàn)。里面有幾個(gè)方法是我們需要關(guān)注的:
-
Block的copy方法:
_Block_copy
_Block_copy
該方法用來確定Block怎樣進(jìn)行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.
struct Block_layout *result = malloc(aBlock->descriptor->size);
if (!result) return NULL;
memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
// 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;
}
}
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
}
這個(gè)方法通過aBlock->flags
確定不同類型Block應(yīng)該怎么copy,flags
的值都在Block_private.h
文件中,上 面分析這個(gè)文件時(shí)有講到,如果忘記了,往前翻一下。
當(dāng)flags
為BLOCK_IS_GLOBAL
即Block的isa
為_NSConcreteGlobalBlock
時(shí)直接返回不做任何操作
void *_Block_copy(const void *arg) {
struct Block_layout *aBlock;
// The following would be better done as a switch statement
aBlock = (struct Block_layout *)arg;
...
else if (aBlock->flags & BLOCK_IS_GLOBAL) {
return aBlock;
}
...
}
當(dāng)為BLOCK_HAS_COPY_DISPOSE
即Block為_NSConcreteStackBlock
時(shí),則在堆上生成同樣大小的Block,把堆上的Block的flags改為BLOCK_HAS_COPY_DISPOSE|BLOCK_NEEDS_FREE|2
,把isa指向_NSConcreteMallocBlock
void *_Block_copy(const void *arg) {
struct Block_layout *aBlock;
// The following would be better done as a switch statement
aBlock = (struct Block_layout *)arg;
...
else {
// Its a stack block. Make a copy.
struct Block_layout *result = malloc(aBlock->descriptor->size);
if (!result) return NULL;
memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
// 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;
}
}
另外會(huì)調(diào)用_Block_call_copy_helper
方法,這個(gè)方法實(shí)際上會(huì)調(diào)用Block_descriptor_2
(Block_private.h
里有講到)的copy方法
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
}
如果Block已經(jīng)被copy到堆上,再調(diào)用copy方法,則只需要增加Block的引用計(jì)數(shù)即可,從實(shí)現(xiàn)上看,Block在連續(xù)調(diào)用Block_copy
時(shí)僅增加了Block的引用計(jì)數(shù),并沒有增加對Block持有的變量的引用計(jì)數(shù)
void *_Block_copy(const void *arg) {
struct Block_layout *aBlock;
// 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;
}
...
}
-
上面已經(jīng)說了,如果捕獲的變量為
id, NSObject, __attribute__((NSObject)), block, …
類型變量和__block
修飾的變量,則需要調(diào)用Block_descriptor_2->copy()
方法,該方法最終會(huì)調(diào)用到_Block_object_assign
方法(為什么能調(diào)用到它,我只能說是編譯器干的,現(xiàn)在不需要關(guān)心,等到我們使用clang命令重寫B(tài)lock的使用時(shí),就會(huì)明白了)。_Block_object_assign
方法用來確定被捕獲的變量怎樣進(jìn)行copy
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:
_Block_retain_object(object);
*dest = object;
break;
case BLOCK_FIELD_IS_BLOCK:
*dest = _Block_copy(object);
break;
...
case BLOCK_FIELD_IS_BYREF:
*dest = _Block_byref_copy(object);
break;
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
*dest = object;
break;
...
default:
break;
}
}
該方法根據(jù)不同的flags值,執(zhí)行不同的操作。如果為id, NSObject, __attribute__((NSObject))
類型,則flags為BLOCK_FIELD_IS_OBJECT
,從上面可以看到調(diào)用了_Block_retain_object
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:
_Block_retain_object(object);
*dest = object;
break;
...
}
}
默認(rèn)_Block_retain_object
方法什么也沒做,如果在MRR環(huán)境下,libDispatch會(huì)調(diào)用_Block_use_RR2
方法,_Block_retain_object
方法會(huì)被賦值為retain操作,具體調(diào)用看object.m這個(gè)文件的void_os_object_init(void)
方法,但是在ARC環(huán)境下,該文件是不會(huì)編譯的,文件中也做了說明
object.m
#if _OS_OBJECT_OBJC_ARC
#error "Cannot build with ARC"
#endif
_Block_retain_object
實(shí)現(xiàn)如下
static void _Block_retain_object_default(const void *ptr __unused) { }
// 默認(rèn)_Block_retain_object被賦值為_Block_retain_object_default,即什么都不做
static void (*_Block_retain_object)(const void *ptr) = _Block_retain_object_default;
// Called from CF to indicate MRR. Newer version uses a versioned structure, so we can add more functions
// without defining a new entry point.
// 上面的注釋其實(shí)不是libclosure-65版本的,我從libclosure-63版本摘過來的,不知道蘋果為啥把這個(gè)注釋去掉了,讓人讀起源碼來,會(huì)感覺莫名其妙
void _Block_use_RR2(const Block_callbacks_RR *callbacks) {
_Block_retain_object = callbacks->retain;
_Block_release_object = callbacks->release;
_Block_destructInstance = callbacks->destructInstance;
}
通過上面的分析,可以知道如果在MRR環(huán)境下,Block會(huì)通過_Block_retain_object
方法持有id, NSObject, __attribute__((NSObject))
類型變量。而在ARC環(huán)境下,_Block_retain_object
方法是個(gè)空操作,并不會(huì)持有該類型變量,那ARC環(huán)境下Block怎么樣達(dá)到持有對象類型的變量呢?ARC環(huán)境有了更完善的內(nèi)存管理,如果外部變量由__strong
、copy
、strong
修飾時(shí),Block會(huì)把捕獲的變量用__strong
來修飾進(jìn)而達(dá)到持有的目的。
在使用Block時(shí),最大的困擾就是RetainCycle(循環(huán)引用),原因很簡單:對象A持有了Block,Block又持有了對象A。因?yàn)锽lock持有了對象A,所以對象A想釋放則必須要先釋放Block,而Block又由于被對象A持有也釋放不了,這就造成了循環(huán)引用。解決循環(huán)引用有兩種辦法,一種是手動(dòng)把Block置為nil來釋放Block對對象A的引用;另外一種就是禁止Block強(qiáng)持有對象A,在MRR和ARC環(huán)境下禁止Block持有對象A的做法是不一樣,ARC環(huán)境下只需要把變量加上__weak
修飾就可以避免Block持有變量;而在MRR環(huán)境下只能通過避免調(diào)用_Block_retain_object
方法,怎么避免呢,可以往下繼續(xù)看
(這里吐槽下,蘋果的文檔真是亂,什么地方都有,找起來很麻煩不說,libDispatch里面竟然也有Block實(shí)現(xiàn),看文檔的日期,竟然比libclosure-65還新,一度讓我不知道該看哪個(gè),最后我發(fā)現(xiàn),libDispatch里的版本其實(shí)不是最新的Block實(shí)現(xiàn),比如libclosure-65版本廢棄了_Block_use_RR
方法,徹底使用_Block_use_RR2
按照文檔上的解釋,說這種調(diào)用方式可以隨意增加方法;()
當(dāng)變量由__block
修飾時(shí),該變量會(huì)被打包成Block_byref
類型,flags會(huì)被標(biāo)記為BLOCK_FIELD_IS_BYREF
,
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_BYREF:
*dest = _Block_byref_copy(object);
break;
...
}
}
我們可以看到實(shí)際上是調(diào)用了_Block_byref_copy
方法
該方法先在堆上生成同樣大小的Block_byref
賦值給堆上的Block,并把flags設(shè)置為src->flags | BLOCK_BYREF_NEEDS_FREE | 4
,
static struct Block_byref *_Block_byref_copy(const void *arg) {
struct Block_byref *src = (struct Block_byref *)arg;
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;
copy->forwarding = copy; // patch heap copy to point to itself
src->forwarding = copy; // patch stack to point to heap copy
copy->size = src->size;
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;
...
(*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));
}
}
...
return src->forwarding;
}
從上面可以看到最終會(huì)調(diào)用Block_byref
的copy方法,該方法又會(huì)調(diào)用_Block_object_assign
方法,如果__block
修飾的變量是id, NSObject, __attribute__((NSObject))
類型,則flags
會(huì)被設(shè)置成BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT
,我們看到僅僅做了賦值操作。所以通過__block
修飾可以避免調(diào)用到_Block_retain_object
方法,也就是在MRR環(huán)境下我們可以通過__block
來避免Block強(qiáng)持有變量,進(jìn)而避免循環(huán)引用
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_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
*dest = object;
break;
...
}
}
當(dāng)然Block也支持嵌套Block使用,flags會(huì)被標(biāo)記為BLOCK_FIELD_IS_BLOCK
,被捕獲的Block被copy就是調(diào)用上面的_Block_copy
方法
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_BLOCK:
*dest = _Block_copy(object);
break;
...
}
}
回頭看下runtime.c這個(gè)文件的幾個(gè)方法
_Block_copy
方法用來確定_NSConcreteStackBlock
、_NSConcreteGlobalBlock
、_NSConcreteMallocBlock
三種類型在Block調(diào)用copy時(shí)執(zhí)行的動(dòng)
_Block_object_assign
方法用來確定Block捕獲了id, NSObject, __attribute__((NSObject)), block, …
類型變量和__block
修飾的變量在_NSConcreteStackBlock
類型Block copy到堆上時(shí)執(zhí)行的動(dòng)作
_Block_byref_copy
方法用來確定被__block
修飾的變量在_NSConcreteStackBlock
類型Block copy到堆上時(shí)執(zhí)行的動(dòng)作
通過上面的分析,Block在使用不同類型的外部變量時(shí),內(nèi)存管理有一下幾種情況:
- 當(dāng)外部變量是全局變量或者static靜態(tài)變量時(shí),只使用且不需要管理內(nèi)存;
- 當(dāng)外部變量是值類型如int、CGPoint時(shí)進(jìn)行值const copy,不需要管理內(nèi)存;
- 當(dāng)外部變量是對象型變量
id, NSObject, __attribute__((NSObject))
時(shí),進(jìn)行指針const copy,在Block被copy到堆上時(shí)增加引用計(jì)數(shù)用來持有該變量,在MRR環(huán)境通過_Block_retain_object
方法持有變量,在ARC環(huán)境下通過__strong
持有變量。這里是解決循環(huán)引用的關(guān)鍵 - 當(dāng)外部變量被
__block
修飾時(shí),會(huì)使用Block_byref
struct包裝該變量,在Block被copy到堆上時(shí)copyBlock_byref
,如果__block
修飾的是id, NSObject, __attribute__((NSObject))
對象型類型,該變量只做指針copy不會(huì)增加變量的引用計(jì)數(shù)。當(dāng)__block
修飾的是值類型時(shí),做值const copy - 當(dāng)外部變量是
block
類型時(shí),在Block被copy到堆上時(shí),調(diào)用_Block_copy
進(jìn)行持有,如果外部block
類型變量也持有變量,則遞歸進(jìn)行copy
五、Block使用-續(xù)
第三部分講到Block怎么樣使用,第四節(jié)講到了Block的內(nèi)部結(jié)構(gòu)和內(nèi)存管理。這里大家可能有個(gè)疑問,Block在使用時(shí),并沒有聲明那一堆struct啊,而且像什么isa、flags值都是誰設(shè)置的呢。
先看下Block使用例子:
int multiplier = 7;
int (^myBlock)(int) = ^(int num) {
return num * multiplier;
};
再看下Block_private.h
文件中Block的結(jié)構(gòu):
struct Block_descriptor_1 {
uintptr_t reserved;
uintptr_t size; // Block's size
};
struct Block_descriptor_2 {
// requires BLOCK_HAS_COPY_DISPOSE
void (*copy)(void *dst, const void *src);
void (*dispose)(const void *);
};
// Block的內(nèi)部布局
struct Block_layout {
void *isa; // initialized to &_NSConcreteStackBlock or &_NSConcreteGlobalBlock
volatile int32_t flags; // contains ref count 這個(gè)值主要用來告知系統(tǒng)Block在copy時(shí)應(yīng)該執(zhí)行什么操作
int32_t reserved;
void (*invoke)(void *, ...); // function ptr
struct Block_descriptor_1 *descriptor;
// imported variables
};
正常來講,我們應(yīng)該先聲明Block_layout
對象,然后實(shí)現(xiàn)invoke
方法,告知系統(tǒng)Block是_NSConcreteStackBlock
還是_NSConcreteGlobalBlock
,再實(shí)現(xiàn)BLOCK_DESCRIPTOR_2
的copy方法,管理捕獲的對象的內(nèi)存。可以看到,這種調(diào)用方式太麻煩了,而且需要我們深刻理解Block_layout
里每個(gè)變量的含義,稍不留神就會(huì)出錯(cuò)。為了讓我們使用更簡單,這一堆變量的設(shè)置都交給了編譯器去實(shí)現(xiàn)。編譯器可以聰明地把上面的結(jié)構(gòu)轉(zhuǎn)成下面的結(jié)構(gòu)。這里我們借助編譯器前端clang,使用clang -rewrite-objc xxx.m
命令重寫成c++代碼,看下編譯器是怎么轉(zhuǎn)換的
好了,我們先建個(gè)工程,假設(shè)叫BlockImpl;然后創(chuàng)建文件,比如也叫BlockImpl,把上面那段代碼copy進(jìn)來
#import "BlockImpl.h"
@implementation BlockImpl
- (void)test {
int multiplier = 7; // 局部變量或者實(shí)例變量
int (^myBlock)(int) = ^(int num) {
return num * multiplier;
};
myBlock(1);
}
@end
然后,我們在終端中輸入命令clang -rewrite-objc BlockImpl.m
(當(dāng)然你首先要cd 該文件所在文件夾:)),回車,你會(huì)發(fā)現(xiàn)在BlockImpl.m
同級文件夾中生成了一個(gè)名字叫BlockImpl.cpp
文件。這個(gè)文件就是重寫后的c++文件。打開后如下:
#ifndef __OBJC2__
#define __OBJC2__
#endif
struct objc_selector; struct objc_class;
struct __rw_objc_super {
struct objc_object *object;
struct objc_object *superClass;
__rw_objc_super(struct objc_object *o, struct objc_object *s) : object(o), superClass(s) {}
};
#ifndef _REWRITER_typedef_Protocol
typedef struct objc_object Protocol;
#define _REWRITER_typedef_Protocol
#endif
#define __OBJC_RW_DLLIMPORT extern
...
幾行代碼被重寫成將近10萬行代碼,沒事兒,不要怕,我們只需要關(guān)注跟我們相關(guān)的代碼,在這我把相關(guān)代碼摘出來,如下:
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
// @implementation BlockImpl
struct __BlockImpl__test_block_impl_0 {
struct __block_impl impl;
struct __BlockImpl__test_block_desc_0* Desc;
int multiplier;
__BlockImpl__test_block_impl_0(void *fp, struct __BlockImpl__test_block_desc_0 *desc, int _multiplier, int flags=0) : multiplier(_multiplier) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static int __BlockImpl__test_block_func_0(struct __BlockImpl__test_block_impl_0 *__cself, int num) {
int multiplier = __cself->multiplier; // bound by copy
return num * multiplier;
}
static struct __BlockImpl__test_block_desc_0 {
size_t reserved;
size_t Block_size;
} __BlockImpl__test_block_desc_0_DATA = { 0, sizeof(struct __BlockImpl__test_block_impl_0)};
static void _I_BlockImpl_test(BlockImpl * self, SEL _cmd) {
int multiplier = 7;
int (*myBlock)(int) = ((int (*)(int))&__BlockImpl__test_block_impl_0((void *)__BlockImpl__test_block_func_0, &__BlockImpl__test_block_desc_0_DATA, multiplier));
((int (*)(__block_impl *, int))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock, 1);
}
// @end
摘完后,代碼是不是清爽了許多,好了,下一步我們就嘗試對應(yīng)下,看看重寫后的代碼是不是和Block_private.h
文件中定義的結(jié)構(gòu)一樣:
1、__BlockImpl__test_block_impl_0
和__block_impl
加起來對應(yīng)上了Block_layout
。這里有個(gè)知識點(diǎn)注意下,struct并沒有改變變量在內(nèi)存的位置,所以這兩個(gè)是可以劃等號的
2、__BlockImpl__test_block_desc_0
和Block_descriptor_1
對應(yīng)
3、Block的body被轉(zhuǎn)換成了函數(shù)指針__BlockImpl__test_block_func_0
從上面的分析,可以看到它們倆是完全可以對應(yīng)上的。另外我們也可以看到系統(tǒng)為做了一些自動(dòng)化的工作,比如為isa賦值&_NSConcreteStackBlock
;myBlock(1)
調(diào)用轉(zhuǎn)換為函數(shù)調(diào)用((int (*)(__block_impl *, int))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock, 1);
;局部變量multiplier
被copy到了__BlockImpl__test_block_impl_0
上,函數(shù)調(diào)用時(shí)也是使用的__BlockImpl__test_block_impl_0
的multiplier
值,而不是那個(gè)局部變量,這也說明了Block不能隨意地修改外部局部變量(當(dāng)然添加__block
修飾符才可以,第三、四部分有講到原因)。
clang -rewrite-objc BlockImpl.m
以MRR方式重寫,如果要想要以ARC方式,加上-fobjc-arc
和-fobjc-runtime=macosx-10.7
,即clang -rewrite-objc -fobjc-arc -fobjc-runtime=macosx-10.7 BlockImpl.m
,通過-rewrite-object
命令重寫下你的應(yīng)用吧,結(jié)合第四部分,你會(huì)深刻了解Block的工作方式和循環(huán)引用產(chǎn)生的原因和解決方案(第四部分已經(jīng)加粗說明)
循環(huán)引用
很多人搞不明白,ARC的循環(huán)引用是怎么引起的,總以為和MRR是一樣的,其實(shí)第四部分已經(jīng)說明過一次了,在這里單拎出來強(qiáng)調(diào)下。MRR環(huán)境下是_Block_retain_object
實(shí)現(xiàn)強(qiáng)引用外部變量的,這個(gè)可以自己寫下代碼用-rewrite-objc
命令重寫下,很容易理解。在ARC環(huán)境_Block_retain_object
其實(shí)是個(gè)空操作,在第四部分已經(jīng)說明。ARC是通過__strong
實(shí)現(xiàn)變量的持有的,下面我們寫一個(gè)循環(huán)引用的例子
@interface BlockImpl ()
@property (nonatomic, copy) void (^myBlock)(int);
@end
@implementation BlockImpl
- (void)testRetainCycle {
self.myBlock = ^(int num) {
NSLog(@"%@", self);
};
self.myBlock(1);
}
@end
使用clang -rewrite-objc -fobjc-arc -fobjc-runtime=macosx-10.7 BlockImpl.m
,得到
***
// 只貼出來最重要的部分
struct __BlockImpl__testRetainCycle_block_impl_0 {
struct __block_impl impl;
struct __BlockImpl__testRetainCycle_block_desc_0* Desc;
BlockImpl *const __strong self; // 看這里,Block通過__strong持有了self
__BlockImpl__testRetainCycle_block_impl_0(void *fp, struct __BlockImpl__testRetainCycle_block_desc_0 *desc, BlockImpl *const __strong _self, int flags=0) : self(_self) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
***
如果禁止Block的持有,則只能通過__weak
修飾self,如下
@interface BlockImpl ()
@property (nonatomic, copy) void (^myBlock)(int);
@end
@implementation BlockImpl
- (void)testRetainCycle {
__weak __typeof(self) weakSelf = self;
self.myBlock = ^(int num) {
NSLog(@"%@", weakSelf);
};
self.myBlock(1);
}
@end
轉(zhuǎn)化為
struct __BlockImpl__testRetainCycle_block_impl_0 {
struct __block_impl impl;
struct __BlockImpl__testRetainCycle_block_desc_0* Desc;
BlockImpl *const __weak weakSelf; // 看這里,__weak修飾,Block不再強(qiáng)持有self
__BlockImpl__testRetainCycle_block_impl_0(void *fp, struct __BlockImpl__testRetainCycle_block_desc_0 *desc, BlockImpl *const __weak _weakSelf, int flags=0) : weakSelf(_weakSelf) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
所以ARC環(huán)境下__weak
可以解決循環(huán)引用
小結(jié)
從Objective-C的發(fā)展史引出了主題Block。在講Block前,先熟悉了閉包的概念,然后了解到Block其實(shí)就是閉包的一種實(shí)現(xiàn)。閉包的實(shí)質(zhì)就是捕獲了外部變量的函數(shù),Block要解決捕獲變量和變量內(nèi)存管理相關(guān)的問題。在使用時(shí)又用讓編譯器簡化了我們使用的成本。第四部分在講Block的內(nèi)存管理時(shí),又講到了MRR環(huán)境和ARC環(huán)境循環(huán)引用的原因和解決方案
網(wǎng)上已經(jīng)有很多篇關(guān)于Block的實(shí)現(xiàn),為什么我還要再寫一篇?有兩個(gè)原因:其實(shí)在2014年底的時(shí)候,我寫過一篇關(guān)于Block原理的文章,當(dāng)時(shí)還有自己的博客,后來博客到期了,文章也丟了;(;另外我找遍了所有網(wǎng)上的博客,發(fā)現(xiàn)千篇一律,而且并沒有說明白循環(huán)引用產(chǎn)生的原因和為什么加了__block
(MRR)、__weak
(ARC)就能避免循環(huán)引用。
最后感謝你花了這么長時(shí)間看我的這篇絮絮叨叨的文章:)