前言
這裏 有關於 block 的 5 道測試題,建議你閱讀本文之前先做一下測試。
先介紹一下什麼是閉包。在 wikipedia 上,閉包的定義) 是:
In programming languages, a closure is a function or reference to a function together with a referencing environment—a table storing a reference to each of the non-local variables (also called free variables or upvalues) of that function.
翻譯過來,閉包是一個函數(或指向函數的指針),再加上該函數執行的外部的上下文變量(有時候也稱作自由變量)。
block 實際上就是 Objective-C 語言對於閉包的實現。 block 配合上 dispatch_queue,可以方便地實現簡單的多線程編程和異步編程,關於這個,我之前寫過一篇文章介紹:《使用 GCD》。
本文主要介紹 Objective-C 語言的 block 在編譯器中的實現方式。主要包括:
-
block 的內部實現數據結構介紹
-
block 的三種類型及其相關的內存管理方式
-
block 如何通過 capture 變量來達到訪問函數外的變量
實現方式
數據結構定義
block 的數據結構定義如下(圖片來自 這裏):
對應的結構體定義如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
struct Block_descriptor {
unsigned long int reserved;
unsigned long int size;
void (*copy)(void *dst, void *src);
void (*dispose)(void *);
};
struct Block_layout {
void *isa;
int flags;
int reserved;
void (*invoke)(void *, ...);
struct Block_descriptor *descriptor;
/* Imported variables. */
};
|
通過該圖,我們可以知道,一個 block 實例實際上由 6 部分構成:
-
isa 指針,所有對象都有該指針,用於實現對象相關的功能。
-
flags,用於按 bit 位表示一些 block 的附加信息,本文後面介紹 block copy 的實現代碼可以看到對該變量的使用。
-
reserved,保留變量。
-
invoke,函數指針,指向具體的 block 實現的函數調用地址。
-
descriptor, 表示該 block 的附加描述信息,主要是 size 大小,以及 copy 和 dispose 函數的指針。
-
variables,capture 過來的變量,block 能夠訪問它外部的局部變量,就是因爲將這些變量(或變量的地址)複製到了結構體中。
該數據結構和後面的 clang 分析出來的結構實際是一樣的,不過僅是結構體的嵌套方式不一樣。但這一點我一開始沒有想明白,所以也給大家解釋一下,如下 2 個結構體 SampleA 和 SampleB 在內存上是完全一樣的,原因是結構體本身並不帶有任何額外的附加信息。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
struct SampleA {
int a;
int b;
int c;
};
struct SampleB {
int a;
struct Part1 {
int b;
};
struct Part2 {
int c;
};
};
|
在 Objective-C 語言中,一共有 3 種類型的 block:
-
_NSConcreteGlobalBlock 全局的靜態 block,不會訪問任何外部變量。
-
_NSConcreteStackBlock 保存在棧中的 block,當函數返回時會被銷燬。
-
_NSConcreteMallocBlock 保存在堆中的 block,當引用計數爲 0 時會被銷燬。
我們在下面會分別來查看它們各自的實現方式上的差別。
研究工具:clang
爲了研究編譯器是如何實現 block 的,我們需要使用 clang。clang 提供一個命令,可以將 Objetive-C 的源碼改寫成 c 語言的,藉此可以研究 block 具體的源碼實現方式。該命令是
1
|
clang -rewrite-objc block.c
|
NSConcreteGlobalBlock 類型的 block 的實現
我們先新建一個名爲 block1.c 的源文件:
1
2
3
4
5
6
7
|
#include <stdio.h>
int main()
{
^{ printf("Hello, World!\n"); } ();
return 0;
}
|
然後在命令行中輸入clang
-rewrite-objc block1.c
即可在目錄中看到 clang 輸出了一個名爲 block1.cpp 的文件。該文件就是 block 在 c 語言實現,我將 block1.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
|
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("Hello, World!\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 (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA) ();
return 0;
}
|
下面我們就具體看一下是如何實現的。__main_block_impl_0 就是該 block 的實現,從中我們可以看出:
-
一個 block 實際是一個對象,它主要由一個 isa 和 一個 impl 和 一個 descriptor 組成。
-
在本例中,isa 指向 _NSConcreteGlobalBlock, 主要是爲了實現對象的所有特性,在此我們就不展開討論了。 -
由於 clang 改寫的具體實現方式和 LLVM 不太一樣,並且這裏沒有開啓 ARC。所以這裏我們看到 isa 指向的還是
_NSConcreteStackBlock
。但在
LLVM 的實現中,開啓 ARC 時,block 應該是 _NSConcreteGlobalBlock 類型,具體可以看 《objective-c-blocks-quiz》 第二題的解釋。 -
impl 是實際的函數指針,本例中,它指向 __main_block_func_0。這裏的 impl 相當於之前提到的 invoke 變量,只是 clang 編譯器對變量的命名不一樣而已。
-
descriptor 是用於描述當前這個 block 的附加信息的,包括結構體的大小,需要 capture 和 dispose 的變量列表等。結構體大小需要保存是因爲,每個 block 因爲會 capture 一些變量,這些變量會加到 __main_block_impl_0 這個結構體中,使其體積變大。在該例子中我們還看不到相關 capture 的代碼,後面將會看到。
NSConcreteStackBlock 類型的 block 的實現
我們另外新建一個名爲 block2.c 的文件,輸入以下內容:
1
2
3
4
5
6
7
8
9
10
11
|
#include <stdio.h>
int main() {
int a = 100;
void (^block2)(void) = ^{
printf("%d\n", a);
};
block2();
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
|
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int 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;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int a = __cself->a; // bound by copy
printf("%d\n", a);
}
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 a = 100;
void (*block2)(void) = (void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a);
((void (*)(__block_impl *))((__block_impl *)block2)->FuncPtr)((__block_impl *)block2);
return 0;
}
|
在本例中,我們可以看到:
-
本例中,isa 指向 _NSConcreteStackBlock,說明這是一個分配在棧上的實例。
-
main_block_impl_0 中增加了一個變量 a,在 block 中引用的變量 a 實際是在申明 block 時,被複制到 main_block_impl_0
結構體中的那個變量 a。因爲這樣,我們就能理解,在 block 內部修改變量 a 的內容,不會影響外部的實際變量 a。
-
main_block_impl_0 中由於增加了一個變量 a,所以結構體的大小變大了,該結構體大小被寫在了 main_block_desc_0
中。
我們修改上面的源碼,在變量前面增加 __block 關鍵字:
1
2
3
4
5
6
7
8
9
10
11
12
|
#include <stdio.h>
int main()
{
__block int i = 1024;
void (^block1)(void) = ^{
printf("%d\n", i);
i = 1023;
};
block1();
return 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
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
printf("%d\n", (i->__forwarding->i));
(i->__forwarding->i) = 1023;
}
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()
{
__attribute__((__blocks__(byref))) __Block_byref_i_0 i = {(void*)0,(__Block_byref_i_0 *)&i, 0, sizeof(__Block_byref_i_0), 1024};
void (*block1)(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 *)block1)->FuncPtr)((__block_impl *)block1);
return 0;
}
|
從代碼中我們可以看到:
-
源碼中增加一個名爲 __Block_byref_i_0 的結構體,用來保存我們要 capture 並且修改的變量 i。
-
main_block_impl_0 中引用的是 Block_byref_i_0 的結構體指針,這樣就可以達到修改外部變量的作用。
-
__Block_byref_i_0 結構體中帶有 isa,說明它也是一個對象。
-
我們需要負責 Block_byref_i_0 結構體相關的內存管理,所以 main_block_desc_0 中增加了 copy
和 dispose 函數指針,對於在調用前後修改相應變量的引用計數。
NSConcreteMallocBlock 類型的 block 的實現
NSConcreteMallocBlock 類型的 block 通常不會在源碼中直接出現,因爲默認它是當一個 block 被 copy 的時候,纔會將這個 block 複製到堆中。以下是一個 block 被 copy 時的示例代碼 (來自 這裏),可以看到,在第 8 步,目標的 block
類型被修改爲 _NSConcreteMallocBlock。
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 外的變量引用,block 默認是將其複製到其數據結構中來實現訪問的,如下圖所示(圖片來自 這裏):
對於用 __block 修飾的外部變量引用,block 是複製其引用地址來實現訪問的,如下圖所示(圖片來自 這裏):
LLVM 源碼
在 LLVM 開源的關於 block 的實現源碼,其內容也和我們用 clang 改寫得到的內容相似,印證了我們對於 block 內部數據結構的推測。
ARC 對 block 類型的影響
在 ARC 開啓的情況下,將只會有 NSConcreteGlobalBlock 和 NSConcreteMallocBlock 類型的 block。
原本的 NSConcreteStackBlock 的 block 會被 NSConcreteMallocBlock 類型的 block 替代。證明方式是以下代碼在 XCode 中,會輸出 <__NSMallocBlock__:
0x100109960>
。在蘋果的 官方文檔 中也提到,當把棧中的 block 返回時,不需要調用 copy 方法了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[])
{
@autoreleasepool {
int i = 1024;
void (^block1)(void) = ^{
printf("%d\n", i);
};
block1();
NSLog(@"%@", block1);
}
return 0;
}
|
我個人認爲這麼做的原因是,由於 ARC 已經能很好地處理對象的生命週期的管理,這樣所有對象都放到堆上管理,對於編譯器實現來說,會比較方便。
參考鏈接
希望本文能加深你對於 block 的理解。我在學習中,查閱了以下文章,一併分享給大家。祝大家玩得開心~