Block 梳理與疑問
時隔一年,再次讀 《Objective-C 高級編程》,看到 block 一章,這一次從頭至尾的跟著編譯了一次,理清楚了很多之前不理解的地方,但是也同時多出了許多疑問。本文是在和學渣裙的朋友們分享以后的梳理筆記,有問題歡迎指出,如果能解決最后的幾個小疑問,就更好了。
環境信息
macOS 10.12.1
Xcode 8.2.1
iOS 10.12
一個最基本的 block
Block 編譯后,有兩個最為重要的部分,impl 結構體 與 desc 結構體指針。我們從最為簡單基礎的開始:
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
// 定義一個參數列表與返回值均為空的 block
dispatch_block_t block = ^{
// 僅輸出一句話
NSLog(@"123");
};
// 調用
block();
}
return 0;
}
使用 clang -rewrite-objc xxx.m 命令,編譯后(已刪除一些影響閱讀的字符,用 xxx 代替):
// block 結構體
struct __main_block_impl_0 {
struct __block_impl impl; // 實現
struct __main_block_desc_0* Desc; // 描述
// 在定義 block 時,所調用的 block 初始化方法
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock; // block 的類型(之后會談到)
impl.Flags = flags;
impl.FuncPtr = fp; // block 實現編譯后的函數指針
Desc = desc; // 描述信息
}
};
// block 的描述
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size; // block 所占的內存大小
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)}; // block 描述的初始化方法,可以看出這里的大小計算,僅僅是進行了 sizeof
// block 實現編譯過后的函數
// 即在 block 初始化方法中,賦值給 impl.FuncPtr 的函數指針
// 參數 cself 是 __main_block_impl_0 類型,即與 block 類型相同,其實這里的參數,本身就是 block 自己
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
// 輸出
NSLog((NSString *)&__NSConstantStringImpl__var_xxx_main_c44db5_mi_0);
}
// main 函數
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
// block 的定義
// 可以看出,在 block 定義的時候,就調用了 __main_block_impl_0,即 block 的構造方法
// 傳的參數分別為 __main_block_func_0,即 block 對應的編譯后的實現函數
// __main_block_desc_0_DATA,即 block 描述
dispatch_block_t block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
// block 的調用
// 這里也可以看出,block 的調用即是調用了,初始化時拿到的 FuncPtr 函數指針
// FuncPtr 函數有一個參數,即傳入的 block 自身
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
return 0;
}
block 內存結構
通過編譯后得到的 block 結構體,能大致看出,在沒有引用外部變量的 block 是這樣的:
struct __block_impl impl;
struct __main_block_desc_0 *Desc;
此時的內存結構如下:
isa 指針
在 block 調用構造方法時,編譯器已經自動給 isa 指針賦了初值。我們知道,isa 指針其實很形象,就稱作 is a,在 OC 中表達了對象是什么類型,類所屬哪個元類,其實 block 也是對象,所以它的 isa 指針也是說明它是什么的。如果直接打印 block,則可看到以下三種情況:
<NSStackBlock: 0x1000010c0>,存儲在棧上的 block
<NSMallocBlock: 0x1000010c0>,存儲在堆上的 block
<NSGlobalBlock: 0x1000010c0>,存儲在全局區的 block
但是你會發現,如果直接打印上面我們所寫的 block,輸出的是 NSGlobalBlock 類型,而我們看到的編譯代碼,明明是 stack 的。這是因為 block 的存儲區域,與定義在什么位置、是否引用外部變量、是否作為范圍值、是被哪種類型的變量所接收等等情況相關,這個會在下一小節談到。
引用外部變量的 block
之前介紹了一個空(并未引用變量)的 block,下面來看一個稍微復雜一點的:
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
// 定義局部變量 a
int a = 10;
// 定義 block
dispatch_block_t block = ^{
// 輸出 a 變量
NSLog(@"%d", a);
};
// 調用 block
block();
}
return 0;
}
在學習 block 的基礎知識時,就知道,此時如果在 block 定義之后,去修改 a 的值,block 中的輸出依然不會改變,我們來看一下為什么。
編譯文件:
// 下面僅標注了有變化的變量
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int a; // 在 block 結構體中,多了一個名為 a 的變量
// block 構造方法也多了一個 _a 參數
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
// 描述依然沒有變,size 是直接計算的 __main_block_impl_0
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
// 在 block 的實現函數中,訪問 block 結構體中的 a 變量,并且編譯器在此還說明了是 bound by copy,即值拷貝
int a = __cself->a; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_xxx_0, a);
}
從編譯代碼可以看出,在 block 定義時,就傳入了 block 內部需要用到的 a 變量的值,而并不是引用,所以即使在 block 定義之后,a 變量怎么變,之前 block 所有的 a 的瞬時值,是沒有變化的。
此時,block 的內存結構為:
多出了捕獲的變量 a 的存儲空間,并且,捕獲的變量會接在 Desc 內存后面。
被 __block 修飾的外部變量
如果沒有 __block 修飾,除了在 block 定義之后,就不能拿到變量最新的值以外,我們還不能對變量進行重新賦值(如果是堆上的內存,就是改變地址,即 NSMutableArray 是可以 addObject 的,只是不能 array = @[])。那么,想要解決這兩個問題,我們就需要引入 __block 修飾符:
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
// 定義變量 a,并使用 __block 修飾
__block int a = 10;
dispatch_block_t block = ^{
// 輸出 a
NSLog(@"%d", a); // 輸出 100
// 在 block 內部對 a 重新賦值
a = 50;
};
// 在 block 定義后,對 a 重新賦值
a = 100;
// 調用 block
block();
// 輸出 a
NSLog(@"%d", a); // 輸出 50
}
return 0;
}
這一次,編譯后的代碼變得很復雜了:
// block 結構體
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
// 比起沒有被 __block 修飾的變量(編譯后,block 中是 int a),這里 block 卻不是簡單的拿到 a 地址,即 int *a,而是一個類型為 __Block_byref_a_0 的結構體指針
__Block_byref_a_0 *a; // by ref
// 構造方法多出的參數也變成了,__Block_byref_a_0 結構體指針,并且 a 的值是 a->__forwarding,這個 __forwarding 指針的作用,會在之后介紹
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, int flags=0) : a(_a->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
// a 變量的結構體定義
struct __Block_byref_a_0 {
void *__isa; // isa 指針
__Block_byref_a_0 *__forwarding; // 類型與 a 變量一模一樣的 __forwarding 結構體指針
int __flags;
int __size;
int a; // a 真正的值
};
// block 描述
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
// 這是比起以前,多出的兩個函數指針,一個 copy,一個 dispose
// 這也是 block 中尤為重要的兩個函數
// copy 負責將 block 復制到堆
// dispose 負責在 block 釋放時,釋放 block 所持有的內存
void (copy)(struct __main_block_impl_0, struct __main_block_impl_0);
void (dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};
// block 實現
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
// 訪問到 block 中的 a 結構體指針變量
__Block_byref_a_0 *a = __cself->a; // bound by ref
// 輸出
// 可以看到,這里訪問的 a 的值,是通過 __forwarding 指針訪問的,包括之后的賦值,也是用的 __forwarding
NSLog((NSString *)&__NSConstantStringImpl__var_folders_xxx_0, (a->__forwarding->a));
// 將 a 的值重新賦為 50
(a->__forwarding->a) = 50;
}
// copy 函數
static void __main_block_copy_0(struct __main_block_impl_0dst, struct __main_block_impl_0src) {
// 使用 _Block_object_assign 函數進行拷貝
_Block_object_assign((void)&dst->a, (void)src->a, 8/BLOCK_FIELD_IS_BYREF/);
}
// dispose 函數
static void __main_block_dispose_0(struct __main_block_impl_0src) {
_Block_object_dispose((void)src->a, 8/BLOCK_FIELD_IS_BYREF/);
}
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
// 初始化 a 的結構體變量
// 傳入的參數分別是:
// isa: void *0;
// __forwarding: &a,即 a 結構體變量的地址
// __flags: 0
// __size: sizeof(結構體)
// a: 10,即 a 的值
__attribute__((__blocks__(byref))) __Block_byref_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10};
// block 定義
dispatch_block_t block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, 570425344));
// 對 a 變量賦值
(a.__forwarding->a) = 100;
// block 的調用
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
// 輸出 a
NSLog((NSString *)&__NSConstantStringImpl__var_folders_xxx_1, (a.__forwarding->a));
}
return 0;
}
經過了這一坨編譯后代碼的理解,現在已經腦子已經是一團糨糊了,莫名多出的結構體、__forwarding 指針,copy 與 dispose 函數無一不在提高理解的門檻。代碼都看得懂,但是就是不知道這樣做的目的,下面我們便來將多出的東西,重新整理一次,然后注意理解:
現在的 block 內存結構是怎樣的?
為什么 a 被 __block 修飾以后,就變成了 __Block_byref_a_0 結構體?
多出的 __forwarding 指針是什么?
為什么之后無論是在 block 內部,還是在 block 外部,訪問 a 都變成了 a->__forwarding->a ?
既然定義了 copy 與 dispose 函數,為什么沒有看到顯式調用?如果是隱式調用,那么調用時機是什么時候?
多個 block 對 __block int a = 10; 進行使用,指針會怎樣指向?
當前的 block 內存結構
從 main 函數中 _Block_byref_a_0 的初始化可以看出,給 __forwarding 指針賦的值就是 (__Block_byref_a_0 *)&a,所以 __forwarding 指針是同樣是指向 a 結構體變量本身的。
為什么在 block 中的變量,超出作用域還能使用
在全局區或者是棧上的 block,我們并不能控制它的釋放時機,但是如果 block 在堆中,就可以由我們來控制了。所以,大多數情況下,比如將 block 作為回調方法等時候,block 一般都是在堆上的。
那么,block 是如何拷貝到堆上的呢?這就和 copy 函數有關了。在 ARC 環境下,如果將 block 聲明為:
@property (copy) block;
@property (strong) block;
在賦值時,其實都會調用 Block_copy() 函數,將棧上的 block 拷貝到堆中,此時,block 中所持有的變量就都在堆中了,我們通過管理 block 的生命周期,就能間接管理到 block 持有的變量的生命周期。
block 的 copy 時機
那么 block 何時會 copy 到堆上呢?是顯式,還是隱式?
顯式
在作為屬性定義時,用 copy 和 strong 修飾;
手動調用 [block copy];
隱式
賦值給 __strong 修飾的變量時。因為 ARC 下,__strong 是缺省值,所以只要不是顯式標記了 __unsafe_unretained 或 __weak,block 均會被拷貝到堆上;
作為函數返回值;
含有 usingBlock 的 Cocoa 框架中的方法,如枚舉器;
GCD 的 block。
__forwarding 指針存在的意義
在閱讀編譯代碼時可以發現,block 在讀寫被 __block 標記的變量時,均使用 var->__forwarding->var 來訪問。var 是指針能理解,因為它肯定是對臨時變量進行地址引用,要不然也不能獲得最新的值。但是為什么要在中間加一個 __forwarding 呢?而且 __forwarding 指針還是指向的自己。
來看一個例子(ARC 下):
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
__block int a = 10;
NSLog(@"1. block 定義之前 a 的地址 : %p", &a);
dispatch_block_t __unsafe_unretained block = ^{
a = 100;
NSLog(@"2. 調用 block 時 a 的地址 : %p", &a);
};
NSLog(@"3. block 定義之后 a 的地址 : %p", &a);
dispatch_block_t heapBlock = block;
NSLog(@"4. block 拷貝到堆上 a 的地址 : %p", &a);
block();
}
return 0;
}
上面的例子中,一共輸出了四次 a 的地址。其中,1 和 3 的地址是一樣的,2 和 4 的地址是一樣。
這里我還對 block 特地標記了 __unsafe_unretained,防止在定義賦值的時候,就拷貝到堆。而這之后的 heapBlock 則是因為被 __strong 修飾所以將 block 拷貝到了堆。
在 1、3 輸出的時候,a 還在棧上,此時的 block 內存為:
而在 block 拷貝到堆上以后, __forwarding 指針則指向堆上的 a 結構體,所以,內存變成了這樣:
這樣就保證了棧上和堆上的 block,都能訪問到同一個 a 變量,這也是 __forwarding 指針的作用。
block 的存儲區域
之前談到 block 根據存儲位置不同,可分為三種,堆、棧、全局區。那么這三種 block 是怎樣的呢?
NSStackBlock:block 被定義為臨時變量,并且引用了外部變量;
NSMallocBlock:調用了 copy 函數,被拷貝到堆上的 block;
NSGlobalBlock:定義為全局變量,或者臨時變量但是沒有引用外部變量的 block。
多個 block 對 __block 變量的引用
在 block 引用使用 __block 修飾的外部變量時,編譯器去針對這個外部變量生成了結構體,比如我們上面談到的 __Block_byref_a_0 結構體。
之所以這樣做,也是為了能在多個 block 引用時,能夠給對 __Block_byref_a_0 進行復用。所以,當多個 block 引用該變量時,并不會重復生成結構體,而是對該結構體內存進行持有,在 block 銷毀,調用 dispose 時,對內存進行釋放。
循環引用
OC 中的循環引用是一個老生常談的問題,其中最容易出現循環引用的地方,就是 block。都知道,出現循環引用的原因,是因為兩個變量的相互持有,導致誰也無法釋放。斷開循環引用鏈,最常見的方式是:
在源頭斷開:一方不持有另一方;
通過置空斷開:在已經對象使用完畢,需要釋放的時候,將一方置空。
根據這兩種解決方案,block 解決循環引用對應著兩種方式:
使用 __weak 或者 __unsafe_unretained 修飾 block 內部要用到的變量。
__weak typeof(self) weakSelf = self;
self.block = ^{
NSLog(@"%@", weakSelf);
};
self.block();
使用 __block 修飾變量,然后在 block 調用完畢后,在 block 內部對變量置空。
__block typeof(self) blockSelf = self;
self.block = ^{
NSLog(@"%@", blockSelf);
blockSelf = nil;
};
self.block();
使用第二種方式有一個弊端,就是必須要保證 block 會調用,這樣才有機會斷開循環引用,否則無法解決問題。當然,也有優點,即可以控制另一方的釋放時機,保證不調用,就不會釋放。
__weak 與 __strong
通常我們能看到以下寫法:
__weak typeof(self) weakSelf = self;
self.block = ^{
__strong typeof(self) strongSelf = weakSelf;
NSLog(@"%@", strongSelf);
};
self.block();
__weak 的作用我們剛才已經提到了,但是在 block 內部又使用 __strong 標記是為什么?這樣會造成循環引用嗎?我們來看看 block 實現編譯過后的代碼:
static void __Test__init_block_func_0(struct __Test__init_block_impl_0 *__cself) {
// 這里變成了值拷貝,而不是指針引用
typeof (self) weakSelf = __cself->weakSelf; // bound by copy
// 雖然是 strong 的,但是是在 block 調用時,才將 self 的值拷貝賦值給臨時變量 weakSelf,之后被 strongSelf 引用
// 根據 ARC 的規則,使用 __strong 修飾的變量,出作用域以后,會插入 release 語句,所以在 block 實現結束后,strongSelf 會釋放,并不會造成循環引用
__attribute__((objc_ownership(strong))) Test * strongSelf = weakSelf;
NSLog((NSString *)&__NSConstantStringImpl__var_xxx_0, strongSelf);
}
也是因為 ARC 對 __strong 修飾的變量,出作用域才插入 release 的機制,我們可以知道,之所以在 block 內部使用 __strong 修飾變量,是因為防止在 block 執行過程中,變量被釋放的情況。
在 block 中調用的方法含有 self,是否會造成循環引用
再看下面這段代碼:
(void)testBlock {
__weak typeof(self) weakSelf = self;
self.block = ^{
__strong typeof(self) strongSelf = weakSelf;
// 在 block 實現中,調用了 run 方法,而 run 方法中,又用到了 self
[strongSelf run];
};
self.block();
}(void)run {
NSLog(@"%@", self);
}
之前有人問到這個問題,說如果 block 中調用的方法又用到了 self,會造成循環引用嗎?想想如果會造成,那豈不是很可怕,好像一直這樣寫,都沒什么問題。那這是為什么呢?
別忘了 OC 是消息機制,發送完消息之后就不管了,所以,并不影響消息的實現。
疑問
雖然能將 block 編譯出來看到代碼,但是還是有很多疑問的,希望大家能解答一下。
ARC 與 MRC 下,有無 __block 標識,對 block 持有對象的影響
其實這里是四種狀態:
ARC + 無 __block
MRC + 無 __block
ARC + 有 __block
MRC + 有 __block
首先來看問題 1、2:
-
(instancetype)init {
self = [super init];
if (self) {// 定義一個可變數組 arr NSMutableArray *arr = [NSMutableArray new]; // 輸出 retainCount NSLog(@"1. %ld", CFGetRetainCount((__bridge CFTypeRef)(arr))); // ARC: 1; MRC: 1 // 為減少隱式的 __strong 造成拷貝到堆的影響,所以使用 __unsafe_unretained 修飾 __unsafe_unretained dispatch_block_t block = ^{ NSLog(@"%@", arr); // 輸出調用 block 時,arr 的 retainCount NSLog(@"2. %ld", CFGetRetainCount((__bridge CFTypeRef)(arr))); // ARC: 1; MRC: 1 }; // 在定義完 block 后,arr 的 retainCount NSLog(@"3. %ld", CFGetRetainCount((__bridge CFTypeRef)(arr))); // ARC: 2; MRC: 1 // 顯示拷貝到堆 self.block = block; // 在 block 拷貝到堆以后,arr 的 retainCount NSLog(@"4. %ld", CFGetRetainCount((__bridge CFTypeRef)(arr))); // ARC: 3; MRC: 2 // 如果是 MRC,則手動 release [arr release];
}
return self;
}
// 調用 block
- (void)run {
self.block();
}
可以看到,同樣沒有使用 __block 修飾,ARC 在 block 定義完以后,arr 的 retainCount 要比 MRC 下多 1,這是因為在 block 的結構體中,所定義的 NSMutableArray *arr,默認的缺省值是 __strong,而導致的持有,而 MRC 下,缺省值不是 __strong 造成的。
再來看問題 2、4,還是借助上面的例子,只是在 arr 定義時,在前面使用 __block 進行修飾,但這一次的 retainCount 卻大為不同:
-
(instancetype)init {
self = [super init];
if (self) {__block NSMutableArray *arr = [NSMutableArray new]; NSLog(@"1. %ld", CFGetRetainCount((__bridge CFTypeRef)(arr))); // ARC: 1; MRC: 1 __unsafe_unretained dispatch_block_t block = ^{ NSLog(@"%@", arr); NSLog(@"2. %ld", CFGetRetainCount((__bridge CFTypeRef)(arr))); // ARC: 1; MRC: 1 }; NSLog(@"3. %ld", CFGetRetainCount((__bridge CFTypeRef)(arr))); // ARC: 1; MRC: 1 self.block = block; NSLog(@"4. %ld", CFGetRetainCount((__bridge CFTypeRef)(arr))); // ARC: 1; MRC: 1 [arr release];
}
return self;
} (void)run {
self.block();
}
這一次,我們發現,無論是 ARC,還是 MRC,arr 的 retainCount 始終為 1,在查閱資料后,找到這樣一句話:
在 MRC 下,__block 說明符可被用來避免循環引用,是因為當 block 從棧復制到堆上時,如果變量被 __block 修飾,則不會再次 retain,如果沒有被 __block 修飾,則會被 retain。
但是,從上面的代碼輸出來看,ARC 和 MRC,block 是否拷貝到堆上,都沒有再次對變量進行持有,retainCount 始終為 1,所以,到這里我遇到幾個不太理解的地方:
__block 修飾符不再持有對象,僅僅是在 MRC 下有效,還是 ARC 與 MRC 下效果是相同的?
如果效果是相同的,為什么 __block 不能解決 ARC 下的循環引用問題?
不能解決 ARC 下的循環引用問題,是否是因為 ARC 下,arr 定義時,缺省值是 __strong 導致的?
在 ARC 下,變量出作用域,編譯器插入 release,為什么 arr 的 retainCount 是 1,經過一次 release 以后,并未出現問題,而在 MRC 下,在 block 調用的時候,就會出現 crash?