前言
很早開始使用Block的時候只記得以下簡單的用法:block中能夠直接訪問和修改全局變量; 但是, 只能訪問局部變量, 不能修改局部變量; 如果想在block 中修改局部變量需要在局部變量的定義之前加上__block修飾。那么今天就來深入探究一下。
Blocks是C語言的擴充功能,而Apple 在OS X Snow Leopard 和 iOS 4中引入了這個新功能“Blocks”。從那開始,Block就出現在iOS和Mac系統各個API中,并被大家廣泛使用。一句話來形容Blocks,帶有自動變量(局部變量)的匿名函數。
Block在OC中的實現如下:
struct Block_layout {
void *isa;
int flags;
int reserved;
void (*invoke)(void *, ...);
struct Block_descriptor *descriptor;
/* Imported variables. */
};
struct Block_descriptor {
unsigned long int reserved;
unsigned long int size;
void (*copy)(void *dst, void *src);
void (*dispose)(void *);
};
從結構圖中很容易看到isa,所以OC處理Block是按照對象來處理的。在iOS中,isa常見的就是_NSConcreteStackBlock,_NSConcreteMallocBlock,_NSConcreteGlobalBlock這3種(另外只在GC環境下還有3種使用的_NSConcreteFinalizingBlock,_NSConcreteAutoBlock,_NSConcreteWeakBlockVariable,本文暫不談論這3種,有興趣的看看官方文檔)
1、block的內部對于外部及內部變量的處理
我們先根據這4種類型
自動變量
靜態變量
靜態全局變量
全局變量
寫出Block測試代碼。
#import <Foundation/Foundation.h>
int global_i = 1;
static int static_global_j = 2;
int main(int argc, const char * argv[]) {
static int static_k = 3;
int val = 4;
void (^myBlock)(void) = ^{
global_i ++;
static_global_j ++;
static_k ++;
NSLog(@"Block中 global_i = %d,static_global_j = %d,static_k = %d,val = %d",global_i,static_global_j,static_k,val);
};
global_i ++;
static_global_j ++;
static_k ++;
val ++;
NSLog(@"Block外 global_i = %d,static_global_j = %d,static_k = %d,val = %d",global_i,static_global_j,static_k,val);
myBlock();
return 0;
}
運行結果
Block 外 global_i = 2,static_global_j = 3,static_k = 4,val = 5
Block 中 global_i = 3,static_global_j = 4,static_k = 5,val = 4
由此產生兩個問題:
1.為什么在Block里面不加__bolck不允許更改變量?
2.為什么自動變量的值沒有增加,而其他幾個變量的值是增加的?自動變量是什么狀態下被block捕獲進去的?
使用clang分析源碼
int global_i = 1;
static int static_global_j = 2;
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int *static_k;
int val;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_k, int _val, int flags=0) : static_k(_static_k), val(_val) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int *static_k = __cself->static_k; // bound by copy
int val = __cself->val; // bound by copy
global_i ++;
static_global_j ++;
(*static_k) ++;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_6fe658_mi_0,global_i,static_global_j,(*static_k),val);
}
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)};
int main(int argc, const char * argv[]) {
static int static_k = 3;
int val = 4;
void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &static_k, val));
global_i ++;
static_global_j ++;
static_k ++;
val ++;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_6fe658_mi_1,global_i,static_global_j,static_k,val);
((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
return 0;
}
首先全局變量global_i和靜態全局變量static_global_j的值增加,以及它們被Block捕獲進去,這一點很好理解,因為是全局的,作用域很廣,所以Block捕獲了它們進去之后,在Block里面進行++操作,Block結束之后,它們的值依舊可以得以保存下來。
接下來仔細看看自動變量和靜態變量的問題。 在__main_block_impl_0中,可以看到靜態變量static_k和自動變量val,被Block從外面捕獲進來,成為__main_block_impl_0這個結構體的成員變量了。
接著看構造函數,
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_k, int _val, int flags=0) : static_k(_static_k), val(_val)
這個構造函數中,自動變量和靜態變量被捕獲為成員變量追加到了構造函數中。
main里面的myBlock閉包中的__main_block_impl_0結構體,初始化如下
void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &static_k, val));
impl.isa = &_NSConcreteStackBlock;
impl.Flags = 0;
impl.FuncPtr = __main_block_impl_0;
Desc = &__main_block_desc_0_DATA;
*_static_k = 4;
val = 4;
到此,__main_block_impl_0結構體就是這樣把自動變量捕獲進來的。也就是說,在執行Block語法的時候,Block語法表達式所使用的自動變量的值是被保存進了Block的結構體實例中,也就是Block自身中。
這里值得說明的一點是,如果Block外面還有很多自動變量,靜態變量,等等,這些變量在Block里面并不會被使用到。那么這些變量并不會被Block捕獲進來,也就是說并不會在構造函數里面傳入它們的值。
Block捕獲外部變量僅僅只捕獲Block閉包里面會用到的值,其他用不到的值,它并不會去捕獲。
再研究一下源碼,我們注意到__main_block_func_0這個函數的實現
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int *static_k = __cself->static_k; // bound by copy
int val = __cself->val; // bound by copy
global_i ++;
static_global_j ++;
(*static_k) ++;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_6fe658_mi_0,global_i,static_global_j,(*static_k),val);
}
*我們可以發現,系統自動給我們加上的注釋,bound by copy,自動變量val雖然被捕獲進來了,但是是用 __cself->val來訪問的。Block僅僅捕獲了val的值,并沒有捕獲val的內存地址。所以在__main_block_func_0這個函數中即使我們重寫這個自動變量val的值,依舊沒法去改變Block外面自動變量val的值。
OC可能是基于這一點,在編譯的層面就防止開發者可能犯的錯誤,因為自動變量沒法在Block中改變外部變量的值,所以編譯過程中就報編譯錯誤。
小結一下: 到此為止,上面提出的第二個問題就解開答案了。自動變量是以值傳遞方式傳遞到Block的構造函數里面去的。Block只捕獲Block中會用到的變量。由于只捕獲了自動變量的值,并內存地址,所以Block內部不能改變自動變量的值。Block捕獲的外部變量可以改變值的是靜態變量,靜態全局變量,全局變量。上面例子也都證明過了。
回到上面的例子上面來,4種變量里面只有靜態變量,靜態全局變量,全局變量這3種是可以在Block里面被改變值的。仔細觀看源碼,我們能看出這3個變量可以改變值的原因。
1、靜態全局變量,全局變量由于作用域的原因,于是可以直接在Block里面被改變。他們也都存儲在全局區。
2、靜態變量傳遞給Block是內存地址值,所以能在Block里面直接改變值。
總結一下在Block中改變變量值有2種方式,一是傳遞內存地址指針到Block中,二是改變存儲區方式(__block)。
2、Block中__block實現原理
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
__block int i = 0;
void (^myBlock)(void) = ^{
i ++;
NSLog(@"%d",i);
};
myBlock();
return 0;
}
把上述代碼用clang轉換成源碼。
struct __Block_byref_i_0 {
void *__isa;
__Block_byref_i_0 *__forwarding;
int __flags;
int __size;
int i;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_i_0 *i; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_i_0 *_i, int flags=0) : i(_i->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_i_0 *i = __cself->i; // bound by ref
(i->__forwarding->i) ++;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_3b0837_mi_0,(i->__forwarding->i));
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->i, (void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);}
static struct __main_block_desc_0 {
size_t reserved;
size_t Block_size;
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};
int main(int argc, const char * argv[]) {
__attribute__((__blocks__(byref))) __Block_byref_i_0 i = {(void*)0,(__Block_byref_i_0 *)&i, 0, sizeof(__Block_byref_i_0), 0};
void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_i_0 *)&i, 570425344));
((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
return 0;
}
??
從源碼我們能發現,帶有 __block的變量也被轉化成了一個結構體__Block_byref_i_0,這個結構體有5個成員變量。第一個是isa指針,第二個是指向自身類型的__forwarding指針,第三個是一個標記flag,第四個是它的大小,第五個是變量值,名字和變量名同名。
__attribute__((__blocks__(byref))) __Block_byref_i_0 i = {(void*)0,(__Block_byref_i_0 *)&i, 0, sizeof(__Block_byref_i_0), 0};
源碼中是這樣初始化的。__forwarding指針初始化傳遞的是自己的地址。然而這里__forwarding指針真的永遠指向自己么?我們來做一個實驗。
//以下代碼在MRC中運行
__block int i = 0;
NSLog(@"%p",&i);
void (^myBlock)(void) = [^{
i ++;
NSLog(@"這是Block 里面%p",&i);
}copy];
我們把Block拷貝到了堆上,這個時候打印出來的2個i變量的地址就不同了。
0x7fff5fbff818
<__NSMallocBlock__: 0x100203cc0>
這是Block 里面 0x1002038a8
地址不同就可以很明顯的說明__forwarding指針并沒有指向之前的自己了。那__forwarding指針現在指向到哪里了呢?
Block里面的__block的地址和Block的地址就相差1052。我們可以很大膽的猜想,__block現在也在堆上了。
出現這個不同的原因在于這里把Block拷貝到了堆上。
由第二章里面詳細分析的,堆上的Block會持有對象。我們把Block通過copy到了堆上,堆上也會重新復制一份Block,并且該Block也會繼續持有該__block。當Block釋放的時候,__block沒有被任何對象引用,也會被釋放銷毀。
__forwarding指針這里的作用就是針對堆的Block,把原來__forwarding指針指向自己,換成指向_NSConcreteMallocBlock上復制之后的__block自己。然后堆上的變量的__forwarding再指向自己。這樣不管__block怎么復制到堆上,還是在棧上,都可以通過(i->__forwarding->i)來訪問到變量值。
特別說明:ARC環境下,一旦Block賦值就會觸發copy,__block就會copy到堆上,Block也是__NSMallocBlock。ARC環境下也是存在__NSStackBlock的時候,這種情況下,__block就在棧上。 MRC環境下,只有copy,__block才會被復制到堆上,否則,__block一直都在棧上,block也只是NSStackBlock,這個時候\forwarding指針就只指向自己了。
最后
關于Block捕獲外部變量有很多用途,用途也很廣,只有弄清了捕獲變量和持有的變量的概念以后,之后才能清楚的解決Block循環引用的問題。
再次回到文章開頭,5種變量,自動變量,函數參數 ,靜態變量,靜態全局變量,全局變量,如果嚴格的來說,捕獲是必須在Block結構體__main_block_impl_0里面有成員變量的話,Block能捕獲的變量就只有帶有自動變量和靜態變量了。捕獲進Block的對象會被Block持有。
帶__block的自動變量 和 靜態變量 就是直接地址訪問。所以在Block里面可以直接改變變量的值。
而剩下的靜態全局變量,全局變量,函數參數,也是可以在直接在Block中改變變量值的,但是他們并沒有變成Block結構體__main_block_impl_0的成員變量,因為他們的作用域大,所以可以直接更改他們的值。
值得注意的是,靜態全局變量,全局變量,函數參數他們并不會被Block持有,也就是說不會增加retainCount值。
參考鏈接:
https://juejin.im/post/57ccab0ba22b9d006ba26de1