一、Objective-C發展史
Objective-C從1983年誕生,已經走過了30多年的歷程。隨著時間的推移,Objective-C支持很多特性,下面是幾個重要的發展節點:
- Object Oriented C —1983
- Retain and Release
- Properties —2006(Objective-C2.0發布)
- Blocks —Mac OS X 10.6(June 8, 2009) & iOS4.0(June 21, 2010)
- ARC —Mac OS X 10.6&iOS5(2011)
以上發展節點來自這里,時間來自維基百科和其他網站。至于為什么會先說這些,主要我發現很多同學在了解一項技術時,沒有一個立體的概念,經常看后會很快忘記。當我們對一個知識的來龍去脈了解時,這個知識點就不再是個點,而會變成一個面,起到幫助我們記憶的作用。另外我建議大家深入學習某項技術時,不要僅看一些人的分享,現在知識分享已經沒有門檻,經常會看到人云亦云的博客(當然也可能包括我:)),最好的方式就是先從博客上了解知識,然后從官方途徑上去學習驗證。推薦大家幾個網站,第一個當然是蘋果的開發文檔;第二個就是WWDC視頻;如果你還是不盡興,想從源碼方面上了解,可以去看下蘋果開放的源碼-runtime、libDispatch、libclosure,和clang文檔
二、Block的由來
閉包
在講Block之前,我們先了解下閉包的概念:
在計算機科學中,閉包(英語:Closure),又稱詞法閉包(Lexical Closure)或函數閉包(function closures),是引用了自由變量的函數。這個被引用的自由變量將和這個函數一同存在,即使已經離開了創造它的環境也不例外。—維基百科
從上面維基百科的解釋中,我們可以看到,一個閉包即有捕獲自由變量的特性,又具有函數的特性。閉包在我們日常工作中,經常用于回調,尤其在延遲調用(也稱為惰性求值)的情況下非常有用。比如我們經常需要在網絡請求完成時更新UI,網絡請求需要一定時間,在請求結束時再調用更新UI的方法。
Block
先從蘋果官方文檔上,看下關于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實現的閉包,它適用于C++,Objective-C,Objective-C++。另外Block需要runtime和clang的支持,下面會講到
三、Block使用
先看一個使用Block的場景,代碼如下:
int multiplier = 7;
int (^myBlock)(int) = ^(int num) {
return num * multiplier;
};
Block的使用和函數指針使用非常相似,最大的不同,可能就是函數指針的*變成了^。下面我們看下Block有哪些東西
三、Block存儲域
在iOS開發過程中,有三種不同類型的Block,分別是:
-
_NSConcreteStackBlock
, 棧上的Block,在出了作用域之后會被釋放 -
_NSConcreteMallocBlock
,堆上的Block,在MRR環境需要手動釋放 -
_NSConcreteGlobalBlock
,全局的Block,存在data區域,生命周期和應用一樣
在我們初始化Block時只有_NSConcreteStackBlock/_NSConcreteGlobalBlock
兩種類型。注意我們是無法生成_NSConcreteMallocBlock
的,只有在_NSConcreteStackBlock
調用__Block_copy
時才會被copy到堆上,生成_NSConcreteMallocBlock
。這一點可以從clang文檔中了解到,可能現在看懂這些還比較困難,沒關系,我們下面在Block內部實現會講到。
有兩個問題需要我們注意下:
-
在初始化Block時,我們怎么判斷是
_NSConcreteStackBlock
還是_NSConcreteGlobalBlock
?這里有個快速判斷的方法。如果Block的body里使用到了外部的非全局變量和非static靜態變量,那么這個Block就會在棧上創建即
_NSConcreteStackBlock
。反之如果沒有引用變量或者僅引用了全局變量或者static靜態變量則是全局Block_NSConcreteGlobalBlock
,下面有幾個例子:// _NSConcreteGlobalBlock int multiplier = 7; // 全局變量 { int (^myBlock)(int) = ^(int num) { return num * multiplier; }; } // _NSConcreteGlobalBlock static int multiplier = 7; // 靜態變量 int (^myBlock)(int) = ^(int num) { return num * multiplier; }; // _NSConcreteStackBlock MRR環境 int multiplier = 7; // 局部變量或者實例變量 int (^myBlock)(int) = ^(int num) { return num * multiplier; }; // _NSConcreteMallocBlock ARC環境 // 注意這里雖然在打印myBlock時顯示為NSMallocBlock,但是這并不是說我們創建出了_NSConcreteMallocBlock,而是被隱式地調用了copy int multiplier = 7; // 局部變量或者實例變量 int (^myBlock)(int) = ^(int num) { return num * multiplier; };
-
Block在MRR環境和ARC環境下使用時,需要注意哪些情況?
1、在MRR環境下系統提供了兩種方式把Block從棧上copy到堆上,第一種是顯式調用copy,第二種是顯式調用Block_copy宏。當然如果使用屬性的話,則需要用copy標記,如
@property (copy) int(^Blk)(int)
而使用retain標記屬性是不行的,大家可以試一下
2、在ARC環境下,編譯器幫助我們自動插入了copy操作,省去了我們很多的工作量,以下幾種情況編譯器會隱式執行copy操作:
- 將Block作為函數返回值時
- 將Block賦值給
__strong
修改的局部變量,或者標記為strong
和copy
的屬性時 - 向Cocoa框架含有usingBlock的方法或者GCD的API傳遞Block參數時
雖然上面的幾種情況幾乎涵蓋了所有Block使用場景,不過仍然一種情況需要我們考慮到,就是把Block當方法參數傳遞給我們自定義方法時進行傳遞時,系統是不幫我們copy的,在使用時需要我們手動進行copy,如:
{ int multiplier = 7; print(^(int num) { return num * multiplier; };) } void print(Blk blk) { // NSStackBlock blk(1); }
四、Block的內部實現
在看源碼之前,我們先大概捋下如果讓我們自己實現一個閉包該怎么實現,或者需要考慮什么?比如如何捕獲外部變量;如何管理外部變量的內存;調用方式是怎樣的?如果解決了這些問題,我們也能寫一個閉包的實現,無非是沒有編譯器的加持,調用起來不是那么優雅。好了,說了這么多,我們帶著這些疑問去看Block的實現源碼,思路會更加清晰。
我們先準備好clang里的說明文檔(這個文檔解釋Block內部實現)和Block實現源碼。
Block的實現主要包括四個文件: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__))
這個文件包括Block向外提供的方法,我們可以看到
Block_copy
宏實現,其實就是調用了_Block_copy
方法,Block_release
宏調用了_Block_release
方法,所以我們也可以將宏調用改為方法調用。這個文件是暴露出去的,我們可以在工程中看到:路徑在/usr/include/Block.h
,這四個文件只有這個文件是暴露的,用來給我們調用 -
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
這個文件定義了6種Block類型。其中三種類型只有GC環境才會出現,因為GC在ARC推出后,已被蘋果廢棄了。所以我們只關心其中三種類型即可,這也印證了我們上面說的三種Block類型
BLOCK_EXPORT void * _NSConcreteStackBlock[32] = { 0 }; BLOCK_EXPORT void * _NSConcreteMallocBlock[32] = { 0 }; BLOCK_EXPORT void * _NSConcreteGlobalBlock[32] = { 0 };
-
Block_private.h
這個文件就是Block的具體結構
-
runtime.c
這個文件描述了Block的copy/release和Block持有變量的copy/release操作。
好了,這就是Block的所有文件,Block_private.h和runtime.c兩個文件是我們需要重點關注。哦,其實還有一個關聯的文件沒說,先放著不管,在講到runtime.c會引出該文件,這個文件很重要,如果沒有這個文件我們在閱讀到某塊代碼時會有點莫名其妙。
Block的內部結構
首先我們從Block內部結構說起,上面我們已經講到,它在Block_private.h文件中,先貼下Block的結構代碼
#define BLOCK_DESCRIPTOR_1 1
/* Block描述1
* 如果是_NSConcreteStackBlock時,在copy到堆上時,需要確定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到堆上時需要生成copy方法,這個方法用來解決捕獲的變量在被copy時要執行的動作。
* 如果是_NSConcreteGlobalBlock則不會生成該結構
*/
struct Block_descriptor_2 {
// requires BLOCK_HAS_COPY_DISPOSE
void (*copy)(void *dst, const void *src);
void (*dispose)(const void *);
};
// Block的內部布局
struct Block_layout {
void *isa; // initialized to &_NSConcreteStackBlock or &_NSConcreteGlobalBlock
volatile int32_t flags; // contains ref count 這個值主要用來告知系統Block在copy時應該執行什么操作
int32_t reserved;
void (*invoke)(void *, ...); // function ptr
struct Block_descriptor_1 *descriptor;
// imported variables
};
從上面我們可以了解到,Block由三個struct構成,每個struct我都做了詳細的注釋。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環境需要的值
// 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
只有這四種值需要我們關注,其中是BLOCK_HAS_COPY_DISPOSE
和BLOCK_IS_GLOBAL
是我們在使用block時由編譯器根據上下文生成的。另外兩個是在Block被copy時,runtime用到和修改的,這里的runtime特指runtime.c文件,在分析runtime.c文件時,再具體說明。
Block在捕獲非全局和非靜態變量時,都是copy的不可變變量,在Block的body里是不能修改這個值的,編譯器會給我們提示如:
(注解:Block的body里使用全局變量或者靜態變量,這些變量并不會進行copy,它只做使用,也不會去管理這些變量的內存,如果Block僅僅使用了全局變量或者靜態變量,Block為_NSConcreteGlobalBlock
類型)
如果需要修改變量值,我們可以通過__block
修飾變量:
__block int multiplier = 7; // 局部變量或者實例變量
int (^myBlock)(int) = ^(int num) {
multiplier = 5;
return num * multiplier;
};
myBlock(1);
__block
修飾的變量會被Block_byref
這樣的結構包起來,具體如下
struct Block_byref {
void *isa;
struct Block_byref *forwarding; // 初始化時會指向自己,當Block被copy時,Block_byref也會被copy到堆上,forwarding會指向堆上的Block_byref
volatile int32_t flags; // contains ref count
uint32_t size; // Block_byref大小,用來copy時分配內存
};
// Block_byref被copy到堆上和釋放時需要的操作
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, …
這種類型,則會生成byref_keep
和byref_destroy
方法,來管理變量的內存,如果是修飾的是自動變量如int,CGPoint,enum類型,則不會生成結構體Block_byref_2
Block如何管理內存
這里引出另外一個文件runtime.c,該文件包含了Block的copy/release、id, NSObject, __attribute__((NSObject)), block, …
類型變量的copy/release和__block
修飾的變量的copy/release的具體實現。里面有幾個方法是我們需要關注的:
-
Block的copy方法:
_Block_copy
_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;
// 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
}
這個方法通過aBlock->flags
確定不同類型Block應該怎么copy,flags
的值都在Block_private.h
文件中,上 面分析這個文件時有講到,如果忘記了,往前翻一下。
當flags
為BLOCK_IS_GLOBAL
即Block的isa
為_NSConcreteGlobalBlock
時直接返回不做任何操作
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;
}
...
}
當為BLOCK_HAS_COPY_DISPOSE
即Block為_NSConcreteStackBlock
時,則在堆上生成同樣大小的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;
}
}
另外會調用_Block_call_copy_helper
方法,這個方法實際上會調用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已經被copy到堆上,再調用copy方法,則只需要增加Block的引用計數即可,從實現上看,Block在連續調用Block_copy
時僅增加了Block的引用計數,并沒有增加對Block持有的變量的引用計數
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;
}
...
}
-
上面已經說了,如果捕獲的變量為
id, NSObject, __attribute__((NSObject)), block, …
類型變量和__block
修飾的變量,則需要調用Block_descriptor_2->copy()
方法,該方法最終會調用到_Block_object_assign
方法(為什么能調用到它,我只能說是編譯器干的,現在不需要關心,等到我們使用clang命令重寫Block的使用時,就會明白了)。_Block_object_assign
方法用來確定被捕獲的變量怎樣進行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;
}
}
該方法根據不同的flags值,執行不同的操作。如果為id, NSObject, __attribute__((NSObject))
類型,則flags為BLOCK_FIELD_IS_OBJECT
,從上面可以看到調用了_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;
...
}
}
默認_Block_retain_object
方法什么也沒做,如果在MRR環境下,libDispatch會調用_Block_use_RR2
方法,_Block_retain_object
方法會被賦值為retain操作,具體調用看object.m這個文件的void_os_object_init(void)
方法,但是在ARC環境下,該文件是不會編譯的,文件中也做了說明
object.m
#if _OS_OBJECT_OBJC_ARC
#error "Cannot build with ARC"
#endif
_Block_retain_object
實現如下
static void _Block_retain_object_default(const void *ptr __unused) { }
// 默認_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.
// 上面的注釋其實不是libclosure-65版本的,我從libclosure-63版本摘過來的,不知道蘋果為啥把這個注釋去掉了,讓人讀起源碼來,會感覺莫名其妙
void _Block_use_RR2(const Block_callbacks_RR *callbacks) {
_Block_retain_object = callbacks->retain;
_Block_release_object = callbacks->release;
_Block_destructInstance = callbacks->destructInstance;
}
通過上面的分析,可以知道如果在MRR環境下,Block會通過_Block_retain_object
方法持有id, NSObject, __attribute__((NSObject))
類型變量。而在ARC環境下,_Block_retain_object
方法是個空操作,并不會持有該類型變量,那ARC環境下Block怎么樣達到持有對象類型的變量呢?ARC環境有了更完善的內存管理,如果外部變量由__strong
、copy
、strong
修飾時,Block會把捕獲的變量用__strong
來修飾進而達到持有的目的。
在使用Block時,最大的困擾就是RetainCycle(循環引用),原因很簡單:對象A持有了Block,Block又持有了對象A。因為Block持有了對象A,所以對象A想釋放則必須要先釋放Block,而Block又由于被對象A持有也釋放不了,這就造成了循環引用。解決循環引用有兩種辦法,一種是手動把Block置為nil來釋放Block對對象A的引用;另外一種就是禁止Block強持有對象A,在MRR和ARC環境下禁止Block持有對象A的做法是不一樣,ARC環境下只需要把變量加上__weak
修飾就可以避免Block持有變量;而在MRR環境下只能通過避免調用_Block_retain_object
方法,怎么避免呢,可以往下繼續看
(這里吐槽下,蘋果的文檔真是亂,什么地方都有,找起來很麻煩不說,libDispatch里面竟然也有Block實現,看文檔的日期,竟然比libclosure-65還新,一度讓我不知道該看哪個,最后我發現,libDispatch里的版本其實不是最新的Block實現,比如libclosure-65版本廢棄了_Block_use_RR
方法,徹底使用_Block_use_RR2
按照文檔上的解釋,說這種調用方式可以隨意增加方法;()
當變量由__block
修飾時,該變量會被打包成Block_byref
類型,flags會被標記為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;
...
}
}
我們可以看到實際上是調用了_Block_byref_copy
方法
該方法先在堆上生成同樣大小的Block_byref
賦值給堆上的Block,并把flags設置為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;
}
從上面可以看到最終會調用Block_byref
的copy方法,該方法又會調用_Block_object_assign
方法,如果__block
修飾的變量是id, NSObject, __attribute__((NSObject))
類型,則flags
會被設置成BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT
,我們看到僅僅做了賦值操作。所以通過__block
修飾可以避免調用到_Block_retain_object
方法,也就是在MRR環境下我們可以通過__block
來避免Block強持有變量,進而避免循環引用
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;
...
}
}
當然Block也支持嵌套Block使用,flags會被標記為BLOCK_FIELD_IS_BLOCK
,被捕獲的Block被copy就是調用上面的_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這個文件的幾個方法
_Block_copy
方法用來確定_NSConcreteStackBlock
、_NSConcreteGlobalBlock
、_NSConcreteMallocBlock
三種類型在Block調用copy時執行的動
_Block_object_assign
方法用來確定Block捕獲了id, NSObject, __attribute__((NSObject)), block, …
類型變量和__block
修飾的變量在_NSConcreteStackBlock
類型Block copy到堆上時執行的動作
_Block_byref_copy
方法用來確定被__block
修飾的變量在_NSConcreteStackBlock
類型Block copy到堆上時執行的動作
通過上面的分析,Block在使用不同類型的外部變量時,內存管理有一下幾種情況:
- 當外部變量是全局變量或者static靜態變量時,只使用且不需要管理內存;
- 當外部變量是值類型如int、CGPoint時進行值const copy,不需要管理內存;
- 當外部變量是對象型變量
id, NSObject, __attribute__((NSObject))
時,進行指針const copy,在Block被copy到堆上時增加引用計數用來持有該變量,在MRR環境通過_Block_retain_object
方法持有變量,在ARC環境下通過__strong
持有變量。這里是解決循環引用的關鍵 - 當外部變量被
__block
修飾時,會使用Block_byref
struct包裝該變量,在Block被copy到堆上時copyBlock_byref
,如果__block
修飾的是id, NSObject, __attribute__((NSObject))
對象型類型,該變量只做指針copy不會增加變量的引用計數。當__block
修飾的是值類型時,做值const copy - 當外部變量是
block
類型時,在Block被copy到堆上時,調用_Block_copy
進行持有,如果外部block
類型變量也持有變量,則遞歸進行copy
五、Block使用-續
第三部分講到Block怎么樣使用,第四節講到了Block的內部結構和內存管理。這里大家可能有個疑問,Block在使用時,并沒有聲明那一堆struct啊,而且像什么isa、flags值都是誰設置的呢。
先看下Block使用例子:
int multiplier = 7;
int (^myBlock)(int) = ^(int num) {
return num * multiplier;
};
再看下Block_private.h
文件中Block的結構:
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的內部布局
struct Block_layout {
void *isa; // initialized to &_NSConcreteStackBlock or &_NSConcreteGlobalBlock
volatile int32_t flags; // contains ref count 這個值主要用來告知系統Block在copy時應該執行什么操作
int32_t reserved;
void (*invoke)(void *, ...); // function ptr
struct Block_descriptor_1 *descriptor;
// imported variables
};
正常來講,我們應該先聲明Block_layout
對象,然后實現invoke
方法,告知系統Block是_NSConcreteStackBlock
還是_NSConcreteGlobalBlock
,再實現BLOCK_DESCRIPTOR_2
的copy方法,管理捕獲的對象的內存。可以看到,這種調用方式太麻煩了,而且需要我們深刻理解Block_layout
里每個變量的含義,稍不留神就會出錯。為了讓我們使用更簡單,這一堆變量的設置都交給了編譯器去實現。編譯器可以聰明地把上面的結構轉成下面的結構。這里我們借助編譯器前端clang,使用clang -rewrite-objc xxx.m
命令重寫成c++代碼,看下編譯器是怎么轉換的
好了,我們先建個工程,假設叫BlockImpl;然后創建文件,比如也叫BlockImpl,把上面那段代碼copy進來
#import "BlockImpl.h"
@implementation BlockImpl
- (void)test {
int multiplier = 7; // 局部變量或者實例變量
int (^myBlock)(int) = ^(int num) {
return num * multiplier;
};
myBlock(1);
}
@end
然后,我們在終端中輸入命令clang -rewrite-objc BlockImpl.m
(當然你首先要cd 該文件所在文件夾:)),回車,你會發現在BlockImpl.m
同級文件夾中生成了一個名字叫BlockImpl.cpp
文件。這個文件就是重寫后的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萬行代碼,沒事兒,不要怕,我們只需要關注跟我們相關的代碼,在這我把相關代碼摘出來,如下:
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
摘完后,代碼是不是清爽了許多,好了,下一步我們就嘗試對應下,看看重寫后的代碼是不是和Block_private.h
文件中定義的結構一樣:
1、__BlockImpl__test_block_impl_0
和__block_impl
加起來對應上了Block_layout
。這里有個知識點注意下,struct并沒有改變變量在內存的位置,所以這兩個是可以劃等號的
2、__BlockImpl__test_block_desc_0
和Block_descriptor_1
對應
3、Block的body被轉換成了函數指針__BlockImpl__test_block_func_0
從上面的分析,可以看到它們倆是完全可以對應上的。另外我們也可以看到系統為做了一些自動化的工作,比如為isa賦值&_NSConcreteStackBlock
;myBlock(1)
調用轉換為函數調用((int (*)(__block_impl *, int))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock, 1);
;局部變量multiplier
被copy到了__BlockImpl__test_block_impl_0
上,函數調用時也是使用的__BlockImpl__test_block_impl_0
的multiplier
值,而不是那個局部變量,這也說明了Block不能隨意地修改外部局部變量(當然添加__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
命令重寫下你的應用吧,結合第四部分,你會深刻了解Block的工作方式和循環引用產生的原因和解決方案(第四部分已經加粗說明)
循環引用
很多人搞不明白,ARC的循環引用是怎么引起的,總以為和MRR是一樣的,其實第四部分已經說明過一次了,在這里單拎出來強調下。MRR環境下是_Block_retain_object
實現強引用外部變量的,這個可以自己寫下代碼用-rewrite-objc
命令重寫下,很容易理解。在ARC環境_Block_retain_object
其實是個空操作,在第四部分已經說明。ARC是通過__strong
實現變量的持有的,下面我們寫一個循環引用的例子
@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
轉化為
struct __BlockImpl__testRetainCycle_block_impl_0 {
struct __block_impl impl;
struct __BlockImpl__testRetainCycle_block_desc_0* Desc;
BlockImpl *const __weak weakSelf; // 看這里,__weak修飾,Block不再強持有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環境下__weak
可以解決循環引用
小結
從Objective-C的發展史引出了主題Block。在講Block前,先熟悉了閉包的概念,然后了解到Block其實就是閉包的一種實現。閉包的實質就是捕獲了外部變量的函數,Block要解決捕獲變量和變量內存管理相關的問題。在使用時又用讓編譯器簡化了我們使用的成本。第四部分在講Block的內存管理時,又講到了MRR環境和ARC環境循環引用的原因和解決方案
網上已經有很多篇關于Block的實現,為什么我還要再寫一篇?有兩個原因:其實在2014年底的時候,我寫過一篇關于Block原理的文章,當時還有自己的博客,后來博客到期了,文章也丟了;(;另外我找遍了所有網上的博客,發現千篇一律,而且并沒有說明白循環引用產生的原因和為什么加了__block
(MRR)、__weak
(ARC)就能避免循環引用。
最后感謝你花了這么長時間看我的這篇絮絮叨叨的文章:)