本系列博文總結自《Pro Multithreading and Memory Management for iOS and OS X with ARC》
block 顧名思義就是程式碼塊,將同一邏輯的程式碼放在一個塊,使程式碼更簡潔緊湊,易於閱讀,而且它比函式使用更方便,程式碼更美觀,因而廣受開發者歡迎。但同時 block 也是 iOS 開發中坑最多的地方之一,因此有必要了解下 block 的實現原理,知其然,更知其所以然,才能從根本上避免挖坑和踩坑。
需要知道的是,block 只是 Objective-C 對閉包的實現,並不是 iOS 獨有的概念,在 C++、Java 等語言也有實現閉包,名稱不同而已。
特別宣告
以下研究所用的過程程式碼由 clang 編譯前端生成,僅作理解之用。實際上 clang 根本不會將 block 轉換成人類可讀的程式碼,它對 block 到底做了什麼,誰也不知道。
所以,切勿將過程程式碼當做block的實際實現,切記切記!!!
將下面的 test.m
程式碼用 clang 工具翻譯 test.cpp
程式碼
1 |
clang -rewrite-objc test.m |
test.m 程式碼
1 2 3 4 5 6 7 8 9 10 11 12 13 |
/************* Objective-C 原始碼 *************/ int main() { void (^blk)(void) = ^{ printf("Block\n"); }; blk(); return 0; } |
test.cpp
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 |
/************* 使用 clang 翻譯後如下 *************/ struct __block_impl { void *isa; int Flags; int Reserved; void *FuncPtr; }; struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; static void __main_block_func_0(struct __main_block_impl_0 *__cself) { printf("Block\n"); } 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() { void (*blk)(void) = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA); ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk); return 0; } |
接著,我們逐一來看下這些函式和結構體
block 結構體資訊詳解
struct __block_impl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// __block_impl 是 block 實現的結構體 struct __block_impl { void *isa; int Flags; int Reserved; void *FuncPtr; }; |
isa
指向例項物件,表明 block 本身也是一個 Objective-C 物件。block 的三種型別:_NSConcreteStackBlock
、_NSConcreteGlobalBlock
、_NSConcreteMallocBlock
,即當程式碼執行時,isa 有三種值
123impl.isa = &_NSConcreteStackBlock;impl.isa = &_NSConcreteMallocBlock;impl.isa = &_NSConcreteGlobalBlock;
Flags
按位承載 block 的附加資訊;Reserved
保留變數;FuncPtr
函式指標,指向 Block 要執行的函式,即{ printf("Block\n") }
;
struct __main_block_impl_0
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 |
// __main_block_impl_0 是 block 實現的結構體,也是 block 實現的入口 struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; |
impl
block 實現的結構體變數,該結構體前面已說明;Desc
描述 block 的結構體變數;__main_block_impl_0
結構體的建構函式,初始化結構體變數impl
、Desc
;
static void __main_block_func_0
1 2 3 4 5 6 7 8 9 |
// __main_block_func_0 是 block 要最終要執行的函式程式碼 static void __main_block_func_0(struct __main_block_impl_0 *__cself) { printf("Block\n"); } |
static struct __main_block_desc_0
1 2 3 4 5 6 7 8 9 10 |
// __main_block_desc_0 是 block 的描述資訊結構體 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) }; |
1 |
reserved
結構體資訊保留欄位Block_size
結構體大小
此處已定義了一個該結構體型別的變數 __main_block_desc_0_DATA
block 實現的執行流程
最基礎的 block 實現就這麼簡單。
接著再看 block 獲取外部變數
block 獲取外部變數
執行下面的程式碼
1 2 3 4 5 6 7 8 9 10 11 12 13 |
int main() { int intValue = 1; void (^blk)(void) = ^{ printf("intValue = %d\n", intValue); }; blk(); return 0; } |
列印結果
intValue = 1
和第一段原始碼不同的是,這裡多了個區域性變數 intValue,而且還在 block 裡面獲取到了。
通過前一段對 block 原始碼的學習,我們已經瞭解到 block 的函式定義在 main() 函式之外,那它又是如何獲取 main() 裡面的區域性變數呢?為了解開疑惑,我們再次用 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 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 |
struct __block_impl { void *isa; int Flags; int Reserved; void *FuncPtr; }; struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; int intValue; __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _intValue, int flags=0) : intValue(_intValue) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } }; static void __main_block_func_0(struct __main_block_impl_0 *__cself) { int intValue = __cself->intValue; // bound by copy printf("intValue = %d\n", intValue); } 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 intValue = 1; void (*blk)(void) = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, intValue); ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk); return 0; } |
原來 block 通過引數值傳遞獲取到 intValue
變數,通過函式
__main_block_impl_0 (void *fp, struct __main_block_desc_0 *desc, int _intValue, int flags=0) : intValue(_intValue)
儲存到 __main_block_impl_0
結構體的同名變數 intValue
,通過程式碼 int intValue = __cself->intValue;
取出 intValue
,列印出來。
建構函式
__main_block_impl_0
冒號後的表示式intValue(_intValue)
的意思是,用_intValue
初始化結構體成員變數intValue
。有四種情況下應該使用初始化表示式來初始化成員:
1:初始化const成員
2:初始化引用成員
3:當呼叫基類的建構函式,而它擁有一組引數時
4:當呼叫成員類的建構函式,而它擁有一組引數時
至此,我們已經瞭解了block 的實現,以及獲取外部變數的原理。但是,我們還不能在 block 內修改 intValue 變數。如果你有心試下,在 block 內部修改 intValue
的值,會報編譯錯誤
Variable is not assignable(missing __block type specifier)
那麼如何在 block 內修改外部變數呢,請看下篇 block沒那麼難(二):block 和變數的記憶體管理