前言
Blocks是C語言的擴充功能,而Apple 在OS X Snow Leopard 和 iOS 4中引入了這個新功能“Blocks”。從那開始,Block就出現在iOS和Mac系統各個API中,並被大家廣泛使用。一句話來形容Blocks,帶有自動變數(區域性變數)的匿名函式。
Block在OC中的實現如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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種,有興趣的看看官方文件)
以上介紹是Block的簡要實現,接下來我們來仔細研究一下Block的捕獲外部變數的特性以及__block的實現原理。
研究工具:clang
為了研究編譯器的實現原理,我們需要使用 clang 命令。clang 命令可以將 Objetive-C 的原始碼改寫成 C / C++ 語言的,藉此可以研究 block 中各個特性的原始碼實現方式。該命令是
1 |
clang -rewrite-objc block.c |
目錄
- 1.Block捕獲外部變數實質
- 2.Block的copy和release
- 3.Block中__block實現原理
一.Block捕獲外部變數實質
拿起我們的Block一起來捕捉外部變數吧。
說到外部變數,我們要先說一下C語言中變數有哪幾種。一般可以分為一下5種:
- 自動變數
- 函式引數
- 靜態變數
- 靜態全域性變數
- 全域性變數
研究Block的捕獲外部變數就要除去函式引數這一項,下面一一根據這4種變數型別的捕獲情況進行分析。
我們先根據這4種型別
- 自動變數
- 靜態變數
- 靜態全域性變數
- 全域性變數
寫出Block測試程式碼。
這裡很快就出現了一個錯誤,提示說自動變數沒有加__block,由於__block有點複雜,我們先實驗靜態變數,靜態全域性變數,全域性變數這3類。測試程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
#import 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; } |
執行結果
1 2 |
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 |
這裡就有2點需要弄清楚了
1.為什麼在Block裡面不加__bolck不允許更改變數?
2.為什麼自動變數的值沒有增加,而其他幾個變數的值是增加的?自動變數是什麼狀態下被block捕獲進去的?
為了弄清楚這2點,我們用clang轉換一下原始碼出來分析分析。
(main.m程式碼行37行,檔案大小832bype, 經過clang轉換成main.cpp以後,程式碼行數飆升至104810行,檔案大小也變成了3.1MB)
原始碼如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
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這個結構體的成員變數了。
接著看建構函式,
1 |
__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結構體,初始化如下
1 2 3 4 5 6 7 8 9 |
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這個函式的實現
1 2 3 4 5 6 7 8 9 |
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中改變外部變數的值,所以編譯過程中就報編譯錯誤。錯誤就是最開始的那張截圖。
1 |
Variable is not assignable(missing __block type specifier) |
小結一下:
到此為止,上面提出的第二個問題就解開答案了。自動變數是以值傳遞方式傳遞到Block的建構函式裡面去的。Block只捕獲Block中會用到的變數。由於只捕獲了自動變數的值,並記憶體地址,所以Block內部不能改變自動變數的值。Block捕獲的外部變數可以改變值的是靜態變數,靜態全域性變數,全域性變數。上面例子也都證明過了。
剩下問題一我們還沒有解決。
回到上面的例子上面來,4種變數裡面只有靜態變數,靜態全域性變數,全域性變數這3種是可以在Block裡面被改變值的。仔細觀看原始碼,我們能看出這3個變數可以改變值的原因。
- 靜態全域性變數,全域性變數由於作用域的原因,於是可以直接在Block裡面被改變。他們也都儲存在全域性區。
- 靜態變數傳遞給Block是記憶體地址值,所以能在Block裡面直接改變值。
根據官方文件我們可以瞭解到,蘋果要求我們在自動變數前加入 __block關鍵字(__block storage-class-specifier儲存域類說明符),就可以在Block裡面改變外部自動變數的值了。
總結一下在Block中改變變數值有2種方式,一是傳遞記憶體地址指標到Block中,二是改變儲存區方式(__block)。
先來實驗一下第一種方式,傳遞記憶體地址到Block中,改變變數的值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#import int main(int argc, const char * argv[]) { NSMutableString * str = [[NSMutableString alloc]initWithString:@"Hello,"]; void (^myBlock)(void) = ^{ [str appendString:@"World!"]; NSLog(@"Block中 str = %@",str); }; NSLog(@"Block外 str = %@",str); myBlock(); return 0; } |
控制檯輸出:
1 2 |
Block 外 str = Hello, Block 中 str = Hello,World! |
看結果是成功改變了變數的值了,轉換一下原始碼。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; NSMutableString *str; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSMutableString *_str, int flags=0) : str(_str) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; static void __main_block_func_0(struct __main_block_impl_0 *__cself) { NSMutableString *str = __cself->str; // bound by copy ((void (*)(id, SEL, NSString *))(void *)objc_msgSend)((id)str, sel_registerName("appendString:"), (NSString *)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_33ff12_mi_1); NSLog((NSString *)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_33ff12_mi_2,str); } static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->str, (void*)src->str, 3/*BLOCK_FIELD_IS_OBJECT*/);} static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->str, 3/*BLOCK_FIELD_IS_OBJECT*/);} 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[]) { NSMutableString * str = ((NSMutableString *(*)(id, SEL, NSString *))(void *)objc_msgSend)((id)((NSMutableString *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSMutableString"), sel_registerName("alloc")), sel_registerName("initWithString:"), (NSString *)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_33ff12_mi_0); void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, str, 570425344)); NSLog((NSString *)&__NSConstantStringImpl__var_folders_45_k1d9q7c52vz50wz1683_hk9r0000gn_T_main_33ff12_mi_3,str); ((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock); return 0; } |
在__main_block_func_0裡面可以看到傳遞的是指標。所以成功改變了變數的值。
至於原始碼裡面的copy和dispose下一節會講到。
改變外部變數值的第二種方式是加 __block這個放在第三章裡面討論,接下來我們先討論一下Block的copy的問題,因為這個問題會關係到 __block儲存域的問題。
二.Block的copy和dispose
OC中,一般Block就分為以下3種,_NSConcreteStackBlock,_NSConcreteMallocBlock,_NSConcreteGlobalBlock。
先來說明一下3者的區別。
1.從捕獲外部變數的角度上來看
- _NSConcreteStackBlock:
只用到外部區域性變數、成員屬性變數,且沒有強指標引用的block都是StackBlock。
StackBlock的生命週期由系統控制的,一旦返回之後,就被系統銷燬了。 - _NSConcreteMallocBlock:
有強指標引用或copy修飾的成員屬性引用的block會被複制一份到堆中成為MallocBlock,沒有強指標引用即銷燬,生命週期由程式設計師控制 - _NSConcreteGlobalBlock:
沒有用到外界變數或只用到全域性變數、靜態變數的block為_NSConcreteGlobalBlock,生命週期從建立到應用程式結束。
沒有用到外部變數肯定是_NSConcreteGlobalBlock,這點很好理解。不過只用到全域性變數、靜態變數的block也是_NSConcreteGlobalBlock。舉例如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
#import int global_i = 1; static int static_global_j = 2; int main(int argc, const char * argv[]) { static int static_k = 3; void (^myBlock)(void) = ^{ NSLog(@"Block中 變數 = %d %d %d",static_global_j ,static_k, global_i); }; NSLog(@"%@",myBlock); myBlock(); return 0; } |
輸出:
1 |
Block中 變數 = 2 3 1 |
可見,只用到全域性變數、靜態變數的block也可以是_NSConcreteGlobalBlock。
所以在ARC環境下,3種型別都可以捕獲外部變數。
2.從持有物件的角度上來看:
- _NSConcreteStackBlock是不持有物件的。
1 2 3 4 5 6 7 8 9 10 11 |
//以下是在MRC下執行的 NSObject * obj = [[NSObject alloc]init]; NSLog(@"1.Block外 obj = %lu",(unsigned long)obj.retainCount); void (^myBlock)(void) = ^{ NSLog(@"Block中 obj = %lu",(unsigned long)obj.retainCount); }; NSLog(@"2.Block外 obj = %lu",(unsigned long)obj.retainCount); myBlock(); |
輸出:
1 2 3 |
1.Block外 obj = 1 2.Block外 obj = 1 Block中 obj = 1 |
- _NSConcreteMallocBlock是持有物件的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
//以下是在MRC下執行的 NSObject * obj = [[NSObject alloc]init]; NSLog(@"1.Block外 obj = %lu",(unsigned long)obj.retainCount); void (^myBlock)(void) = [^{ NSLog(@"Block中 obj = %lu",(unsigned long)obj.retainCount); }copy]; NSLog(@"2.Block外 obj = %lu",(unsigned long)obj.retainCount); myBlock(); [myBlock release]; NSLog(@"3.Block外 obj = %lu",(unsigned long)obj.retainCount); |
輸出:
1 2 3 4 |
1.Block外 obj = 1 2.Block外 obj = 2 Block中 obj = 2 3.Block外 obj = 1 |
- _NSConcreteGlobalBlock也不持有物件
1 2 3 4 5 6 7 8 |
//以下是在MRC下執行的 void (^myBlock)(void) = ^{ NSObject * obj = [[NSObject alloc]init]; NSLog(@"Block中 obj = %lu",(unsigned long)obj.retainCount); }; myBlock(); |
輸出:
1 |
Block 中 obj = 1 |
由於_NSConcreteStackBlock所屬的變數域一旦結束,那麼該Block就會被銷燬。在ARC環境下,編譯器會自動的判斷,把Block自動的從棧copy到堆。比如當Block作為函式返回值的時候,肯定會copy到堆上。
1.手動呼叫copy
2.Block是函式的返回值
3.Block被強引用,Block被賦值給__strong或者id型別
4.呼叫系統API入參中含有usingBlcok的方法
以上4種情況,系統都會預設呼叫copy方法把Block賦複製
但是當Block為函式引數的時候,就需要我們手動的copy一份到堆上了。這裡除去系統的API我們不需要管,比如GCD等方法中本身帶usingBlock的方法,其他我們自定義的方法傳遞Block為引數的時候都需要手動copy一份到堆上。
copy函式把Block從棧上拷貝到堆上,dispose函式是把堆上的函式在廢棄的時候銷燬掉。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#define Block_copy(...) ((__typeof(__VA_ARGS__))_Block_copy((const void *)(__VA_ARGS__))) #define Block_release(...) _Block_release((const void *)(__VA_ARGS__)) // 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) __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2); // Lose the reference, and if heap based and last reference, recover the memory BLOCK_EXPORT void _Block_release(const void *aBlock) __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2); // Used by the compiler. Do not call this function yourself. BLOCK_EXPORT void _Block_object_assign(void *, const void *, const int) __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2); // Used by the compiler. Do not call this function yourself. BLOCK_EXPORT void _Block_object_dispose(const void *, const int) __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_2); |
上面是原始碼中2個常用的巨集定義和4個常用的方法,一會我們就會看到這4個方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
static void *_Block_copy_internal(const void *arg, const int flags) { struct Block_layout *aBlock; const bool wantsOne = (WANTS_ONE & flags) == WANTS_ONE; // 1 if (!arg) return NULL; // 2 aBlock = (struct Block_layout *)arg; // 3 if (aBlock->flags & BLOCK_NEEDS_FREE) { // latches on high latching_incr_int(&aBlock->flags); return aBlock; } // 4 else if (aBlock->flags & BLOCK_IS_GLOBAL) { return aBlock; } // 5 struct Block_layout *result = malloc(aBlock->descriptor->size); if (!result) return (void *)0; // 6 memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first // 7 result->flags &= ~(BLOCK_REFCOUNT_MASK); // XXX not needed result->flags |= BLOCK_NEEDS_FREE | 1; // 8 result->isa = _NSConcreteMallocBlock; // 9 if (result->flags & BLOCK_HAS_COPY_DISPOSE) { (*aBlock->descriptor->copy)(result, aBlock); // do fixup } return result; } |
上面這一段是Block_copy的一個實現,實現了從_NSConcreteStackBlock複製到_NSConcreteMallocBlock的過程。對應有9個步驟。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
void _Block_release(void *arg) { // 1 struct Block_layout *aBlock = (struct Block_layout *)arg; if (!aBlock) return; // 2 int32_t newCount; newCount = latching_decr_int(&aBlock->flags) & BLOCK_REFCOUNT_MASK; // 3 if (newCount > 0) return; // 4 if (aBlock->flags & BLOCK_NEEDS_FREE) { if (aBlock->flags & BLOCK_HAS_COPY_DISPOSE)(*aBlock->descriptor->dispose)(aBlock); _Block_deallocator(aBlock); } // 5 else if (aBlock->flags & BLOCK_IS_GLOBAL) { ; } // 6 else { printf("Block_release called upon a stack Block: %p, ignored\\n", (void *)aBlock); } } |
上面這一段是Block_release的一個實現,實現了怎麼釋放一個Block。對應有6個步驟。
上述2個方法的詳細解析可以看這篇文章
回到上一章節中最後的例子,字串的例子中來,轉換原始碼之後,我們會發現多了一個copy和dispose方法。
因為在C語言的結構體中,編譯器沒法很好的進行初始化和銷燬操作。這樣對記憶體管理來說是很不方便的。所以就在 __main_block_desc_0結構體中間增加成員變數 void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*)和void (*dispose)(struct __main_block_impl_0*),利用OC的Runtime進行記憶體管理。
相應的增加了2個方法。
1 2 3 |
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->str, (void*)src->str, 3/*BLOCK_FIELD_IS_OBJECT*/);} static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->str, 3/*BLOCK_FIELD_IS_OBJECT*/);} |
這裡的_Block_object_assign和_Block_object_dispose就對應著retain和release方法。
BLOCK_FIELD_IS_OBJECT 是Block截獲物件時候的特殊標示,如果是截獲的__block,那麼是BLOCK_FIELD_IS_BYREF。
三.Block中__block實現原理
我們繼續研究一下__block實現原理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#import int main(int argc, const char * argv[]) { __block int i = 0; void (^myBlock)(void) = ^{ i ++; NSLog(@"%d",i); }; myBlock(); return 0; } |
把上述程式碼用clang轉換成原始碼。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
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,第四個是它的大小,第五個是變數值,名字和變數名同名。
1 |
__attribute__((__blocks__(byref))) __Block_byref_i_0 i = {(void*)0,(__Block_byref_i_0 *)&i, 0, sizeof(__Block_byref_i_0), 0}; |
原始碼中是這樣初始化的。__forwarding指標初始化傳遞的是自己的地址。然而這裡__forwarding指標真的永遠指向自己麼?我們來做一個實驗。
1 2 3 4 5 6 7 8 |
//以下程式碼在MRC中執行 __block int i = 0; NSLog(@"%p",&i); void (^myBlock)(void) = [^{ i ++; NSLog(@"這是Block 裡面%p",&i); }copy]; |
我們把Block拷貝到了堆上,這個時候列印出來的2個i變數的地址就不同了。
1 2 3 |
0x7fff5fbff818 這是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)來訪問到變數值。
所以在__main_block_func_0函式裡面就是寫的(i->__forwarding->i)。
這裡還有一個需要注意的地方。還是從例子說起:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
//以下程式碼在MRC中執行 __block int i = 0; NSLog(@"%p",&i); void (^myBlock)(void) = ^{ i ++; NSLog(@"Block 裡面的%p",&i); }; NSLog(@"%@",myBlock); myBlock(); |
結果和之前copy的例子完全不同。
1 2 3 |
0x7fff5fbff818 ** 0x7fff5fbff818 |
Block在捕獲住__block變數之後,並不會複製到堆上,所以地址也一直都在棧上。這與ARC環境下的不一樣。
ARC環境下,不管有沒有copy,__block都會變copy到堆上,Block也是__NSMallocBlock。
MRC環境下,只有copy,__block才會被複制到堆上,否則,__block一直都在棧上,block也只是__NSStackBlock,這個時候__forwarding指標就只指向自己了。
至此,文章開頭提出的問題一,也解答了。__block的實現原理也已經明瞭。
最後
關於Block捕獲外部變數有很多用途,用途也很廣,只有弄清了捕獲變數和持有的變數的概念以後,之後才能清楚的解決Block迴圈引用的問題。
再次回到文章開頭,5種變數,自動變數,函式引數 ,靜態變數,靜態全域性變數,全域性變數,如果嚴格的來說,捕獲是必須在Block結構體__main_block_impl_0裡面有成員變數的話,Block能捕獲的變數就只有帶有自動變數和靜態變數了。捕獲進Block的物件會被Block持有。
自動變數的值,被copy進了Block,不帶__block的自動變數只能在裡面被訪問,並不能改變值。
帶__block的自動變數 和 靜態變數 就是直接地址訪問。所以在Block裡面可以直接改變變數的值。
而剩下的靜態全域性變數,全域性變數,函式引數,也是可以在直接在Block中改變變數值的,但是他們並沒有變成Block結構體__main_block_impl_0的成員變數,因為他們的作用域大,所以可以直接更改他們的值。
值得注意的是,靜態全域性變數,全域性變數,函式引數他們並不會被Block持有,也就是說不會增加retainCount值。
請大家多多指點。
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!
任選一種支付方式