本文用來介紹 iOS 開發中 『Blocks』的底層原理。我將通過 Blocks 由 OC 轉變的 C++ 源碼來一步步解析 Blocks 的底層原理。
通過本文您將瞭解到:
- Blocks 的實質是什麼?
- Block 截獲局部變量和特殊區域變量
- Block 的存儲區域
- Block 的循環引用
文中 Demo 我已放在了 Github 上,Demo 鏈接:傳送門
- 第一篇鏈接: iOS 開發:『Blocks』詳盡總結 (一)基本使用
1. Blocks 的實質是什麼?
在第一篇中我們講解了 Blocks 的基本使用,也知道了 Blocks 是 帶有局部變量的匿名函數。但是 Block 的實質究竟是什麼呢?類型?變量?還是什麼黑科技?
要想了解 Block 的本質,就需要從 Block 對應的 C++ 源碼來入手。
下面我們通過一步步的源碼剖析來了解 Block 的本質。
1.1 Blocks 由 OC 轉 C++ 源碼方法
- 在項目中添加 blocks.m 文件,並寫好 block 的相關代碼。
- 打開『終端』,執行
cd XXX/XXX
命令,其中XXX/XXX
爲 block.m 所在的目錄。 - 繼續執行
clang -rewrite-objc block.m
- 執行完命令之後,block.m 所在目錄下就會生成一個 block.cpp 文件,這就是我們需要的 block 相關的 C++ 源碼。
1.2 Blocks 源碼概覽
下面我們刪除掉 block.m 其他無關的代碼,只保留 blocks 相關的代碼,可以得到如下結果。
- 轉換前 OC 代碼:
int main () {
void (^myBlock)(void) = ^{
printf("myBlock\n");
};
myBlock();
return 0;
}
- 轉換後 C++ 源碼:
/* 包含 Block 實際函數指針的結構體 */
struct __block_impl {
void *isa;
int Flags;
int Reserved; // 今後版本升級所需的區域大小
void *FuncPtr; // 函數指針
};
/* Block 結構體 */
struct __main_block_impl_0 {
// impl:Block 的實際函數指針,指向包含 Block 主體部分的 __main_block_func_0 結構體
struct __block_impl impl;
// Desc:Desc 指針,指向包含 Block 附加信息的 __main_block_desc_0() 結構體
struct __main_block_desc_0* Desc;
// __main_block_impl_0:Block 構造函數
__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;
}
};
/* Block 主體部分結構體 */
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("myBlock\n");
}
/* Block 附加信息結構體:包含今後版本升級所需區域大小,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)};
/* main 函數 */
int main () {
void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
return 0;
}
下面我們一步步來拆解轉換後的源碼。
1.3 Block 結構體
我們先來看看 __main_block_impl_0
結構體( Block 結構體)
/* Block 結構體 */
struct __main_block_impl_0 {
// impl:Block 的實際函數指針,指向包含 Block 主體部分的 __main_block_func_0 結構體
struct __block_impl impl;
// Desc:Desc 指針,指向包含 Block 附加信息的 __main_block_desc_0() 結構體
struct __main_block_desc_0* Desc;
// __main_block_impl_0:Block 構造函數
__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;
}
};
從上邊我們可以看出,__main_block_impl_0
結構體(Block 結構體)包含了三個部分:
- 成員變量
impl
; - 成員變量
Desc
指針; -
__main_block_impl_0
構造函數。
我們先來把這幾個部分剖析一下。
1.3.1 struct __block_impl impl
說明
第一部分 impl
是 __block_impl
結構體類型的成員變量。__block_impl
包含了 Block 實際函數指針 FuncPtr
,FuncPtr
指針指向 Block 的主體部分,也就是 Block 對應 OC 代碼中的 ^{ printf("myBlock\n"); };
部分。還包含了標誌位 Flags
,今後版本升級所需的區域大小 Reserved
,__block_impl
結構體的實例指針 isa
。
/* 包含 Block 實際函數指針的結構體 */
struct __block_impl {
void *isa; // 用於保存 Block 結構體的實例指針
int Flags; // 標誌位
int Reserved; // 今後版本升級所需的區域大小
void *FuncPtr; // 函數指針
};
1.3.2 struct __main_block_desc_0* Desc
說明
第二部分 Desc 是指向的是 __main_block_desc_0
類型的結構體的指針型成員變量,__main_block_desc_0
結構體用來描述該 Block 的相關附加信息:
- 今後版本升級所需區域大小:
reserved
變量。 - Block 大小:
Block_size
變量。
/* Block 附加信息結構體:包含今後版本升級所需區域大小,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)};
1.3.3 __main_block_impl_0
構造函數說明
第三部分是 __main_block_impl_0
結構體(Block 結構體) 的構造函數,負責初始化 __main_block_impl_0
結構體(Block 結構體) 的成員變量。
__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;
}
關於結構體構造函數中對各個成員變量的賦值,我們需要先來看看 main()
函數中,對該構造函數的調用。
void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
我們可以把上面的代碼稍微轉換一下,去掉不同類型之間的轉換,使之簡潔一點:
struct __main_block_impl_0 temp = __main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
struct __main_block_impl_0 *myBlock = &temp;
這樣,就容易看懂了。該代碼將通過 __main_block_impl_0
構造函數,生成的 __main_block_impl_0
結構體(Block 結構體)類型實例的指針,賦值給 __main_block_impl_0
結構體(Block 結構體)類型的指針變量 myBlock
。
可以看到, 調用 __main_block_impl_0
構造函數的時候,傳入了兩個參數。
-
第一個參數:
__main_block_func_0
。- 其實就是 Block 對應的主體部分,可以看到下面關於
__main_block_func_0
結構體的定義 ,和 OC 代碼中^{ printf("myBlock\n"); };
部分具有相同的表達式。 - 這裏參數中的
__cself
是指向 Block 的值的指針變量,相當於 OC 中的self
。
/* Block 主體部分結構體 */ static void __main_block_func_0(struct __main_block_impl_0 *__cself) { printf("myBlock\n"); }
- 其實就是 Block 對應的主體部分,可以看到下面關於
-
第二個參數:
__main_block_desc_0_DATA
:__main_block_desc_0_DATA
包含該 Block 的相關信息。
我們再來結合之前的__main_block_impl_0
結構體定義。-
__main_block_impl_0
結構體(Block 結構體)可以表述爲:
struct __main_block_impl_0 { void *isa; // 用於保存 Block 結構體的實例指針 int Flags; // 標誌位 int Reserved; // 今後版本升級所需的區域大小 void *FuncPtr; // 函數指針 struct __main_block_desc_0* Desc; // Desc:Desc 指針 };
-
__main_block_impl_0
構造函數可以表述爲:
impl.isa = &_NSConcreteStackBlock; // isa 保存 Block 結構體實例 impl.Flags = 0; // 標誌位賦值 impl.FuncPtr = __main_block_func_0; // FuncPtr 保存 Block 結構體的主體部分 Desc = &__main_block_desc_0_DATA; // Desc 保存 Block 結構體的附加信息
-
1.4 Block 實質總結
至此,Block 的實質就要真相大白了。
__main_block_impl_0
結構體(Block 結構體)相當於 Objective-C 類對象的結構體,isa
指針保存的是所屬類的結構體的實例的指針。_NSConcreteStackBlock
相當於 Block 的結構體實例。對象 impl.isa = &_NSConcreteStackBlock;
語句中,將 Block 結構體的指針賦值給其成員變量 isa
,相當於 Block 結構體的成員變量 保存了 Block 結構體的指針,這裏和 Objective-C 中的對象處理方式是一致的。
也就是說明: Block 的實質就是對象。
Block 跟其他所有的 NSObject 一樣,都是對象。果不其然,萬物皆對象,古人誠不欺我。
2. Block 截獲局部變量和特殊區域變量
2.1 Blcok 截獲局部變量的實質
回顧一下上篇文章講解的例子:
// 使用 Blocks 截獲局部變量值
- (void)useBlockInterceptLocalVariables {
int a = 10, b = 20;
void (^myLocalBlock)(void) = ^{
printf("a = %d, b = %d\n",a, b);
};
myLocalBlock(); // 輸出結果:a = 10, b = 20
a = 20;
b = 30;
myLocalBlock(); // 輸出結果:a = 10, b = 20
}
從中可以看到,我們在第一次調用 myLocalBlock();
之後已經重新給變量 a
、變量 b
賦值了,但是第二次調用 myLocalBlock();
的時候,使用的還是之前對應變量的值。
這是因爲 Block 語法的表達式使用的是它之前聲明的局部變量
a
、變量b
。Blocks 中,Block 表達式截獲所使用的局部變量的值,保存了該變量的瞬時值。所以在第二次執行 Block 表達式時,即使已經改變了局部變量a
和b
的值,也不會影響 Block 表達式在執行時所保存的局部變量的瞬時值。
這就是 Blocks 變量截獲局部變量值的特性。
可是,爲什麼 Blocks 變量使用的是局部變量的瞬時值,而不是局部變量的當前值呢?
我們來看一下對應的 C++ 代碼:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int a;
int b;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int _b, int flags=0) : a(_a), b(_b) {
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
int b = __cself->b; // bound by copy
printf("a = %d, b = %d\n",a, b);
}
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 = 10, b = 20;
void (*myLocalBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a, b));
((void (*)(__block_impl *))((__block_impl *)myLocalBlock)->FuncPtr)((__block_impl *)myLocalBlock);
a = 20;
b = 30;
((void (*)(__block_impl *))((__block_impl *)myLocalBlock)->FuncPtr)((__block_impl *)myLocalBlock);
}
-
可以看到
__main_block_impl_0
結構體(Block 結構體)中多了兩個成員變量a
和b
,這兩個變量就是 Block 截獲的局部變量。a
和b
的值來自與__main_block_impl_0
構造函數中傳入的值。struct __main_block_impl_0 { struct __block_impl impl; struct __main_block_desc_0* Desc; int a; // 增加的成員變量 a int b; // 增加的成員變量 b __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int _b, int flags=0) : a(_a), b(_b) { impl.isa = &_NSConcreteStackBlock; impl.Flags = flags; impl.FuncPtr = fp; Desc = desc; } };
-
還可以看出 __main_block_func_0
(保存 Block 主體部分的結構體)中,變量a
、b
的值使用的__cself
獲取的值。
而__cself->a
、__cself->b
是通過值傳遞的方式傳入進來的,而不是通過指針傳遞。這也就說明了a
、b
只是 Block 內部的變量,改變 Block 外部的局部變量值,並不能改變 Block 內部的變量值。static void __main_block_func_0(struct __main_block_impl_0 *__cself) { int a = __cself->a; // bound by copy int b = __cself->b; // bound by copy printf("a = %d, b = %d\n",a, b); }
那麼來總結一下:
在定義 Block 表達式的時候,局部變量使用『值傳遞』的方式傳入 Block 結構體中,並保存爲 Block 的成員變量。
而當外部局部變量發生變化的時候,Block 結構體內部對應的的成員變量的值並沒有發生改變,所以無論調用幾次,Block 表達式結果都沒有發生改變。
如果在 Block 主體部分對外部局部變量進行修改呢?類似下面這樣,是不是就可以將截獲的外部局部變量修改了?
int a = 10, b = 20;
void (^myLocalBlock)(void) = ^{
a = 20;
b = 30;
printf("a = %d, b = %d\n",a, b);
};
myLocalBlock();
很遺憾,編譯直接報錯了。
這種方式也走不通。
由此我們暫時可以得出一個結論:
被截獲的自動變量的值是無法直接修改的。
可是,憑啥不能改變?如果我們非要改變呢,該咋整?
有一個辦法,可以通過 __block
說明符修飾局部變量。
2.2 使用 __block
說明符更改局部變量值
// 使用 __block 說明符修飾,更改局部變量值
- (void)useBlockQualifierChangeLocalVariables {
__block int a = 10, b = 20;
void (^myLocalBlock)(void) = ^{
a = 20;
b = 30;
printf("a = %d, b = %d\n",a, b); // 輸出結果:a = 20, b = 30
};
myLocalBlock();
}
從中我們可以發現:通過 __block
修飾的局部變量,可以在 Block 的主體部分中改變值。
我們來轉換下源碼,分析一下:
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};
struct __Block_byref_b_1 {
void *__isa;
__Block_byref_b_1 *__forwarding;
int __flags;
int __size;
int b;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_a_0 *a; // by ref
__Block_byref_b_1 *b; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, __Block_byref_b_1 *_b, int flags=0) : a(_a->__forwarding), b(_b->__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_a_0 *a = __cself->a; // bound by ref
__Block_byref_b_1 *b = __cself->b; // bound by ref
(a->__forwarding->a) = 20;
(b->__forwarding->b) = 30;
printf("a = %d, b = %d\n",(a->__forwarding->a), (b->__forwarding->b));
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);_Block_object_assign((void*)&dst->b, (void*)src->b, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);_Block_object_dispose((void*)src->b, 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_a_0 a = {(void*)0,(__Block_byref_a_0 *)&a, 0, sizeof(__Block_byref_a_0), 10};
__Block_byref_b_1 b = {(void*)0,(__Block_byref_b_1 *)&b, 0, sizeof(__Block_byref_b_1), 20};
void (*myLocalBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_a_0 *)&a, (__Block_byref_b_1 *)&b, 570425344));
((void (*)(__block_impl *))((__block_impl *)myLocalBlock)->FuncPtr)((__block_impl *)myLocalBlock);
return 0;
}
可以看到,只是加上了一個 __block
,代碼量就增加了很多。
我們從 __main_block_impl_0
開始說起:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_a_0 *a; // by ref
__Block_byref_b_1 *b; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_a_0 *_a, __Block_byref_b_1 *_b, int flags=0) : a(_a->__forwarding), b(_b->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
我們在 __main_block_impl_0
結構體中可以看到: 原 OC 代碼中,被 __block
修飾的局部變量 __block int a
、__block int b
分別變成了 __Block_byref_a_0
、__Block_byref_b_1
類型的結構體指針 a
、結構體指針 b
。這裏使用結構體指針 a
、結構體指針 b
說明 _Block_byref_a_0
、__Block_byref_b_1
類型的結構體並不在 __main_block_impl_0
結構體中,而只是通過指針的形式引用,這是爲了可以在多個不同的 Block 中使用 __block
修飾的變量。
__Block_byref_a_0
、__Block_byref_b_1
類型的結構體聲明如下:
struct __Block_byref_a_0 {
void *__isa;
__Block_byref_a_0 *__forwarding;
int __flags;
int __size;
int a;
};
struct __Block_byref_b_1 {
void *__isa;
__Block_byref_b_1 *__forwarding;
int __flags;
int __size;
int b;
};
拿第一個 __Block_byref_a_0
結構體定義來說明,__Block_byref_a_0
有 5 個部分:
-
__isa
:標識對象類的isa
實例變量 -
__forwarding
:傳入變量的地址 -
__flags
:標誌位 -
__size
:結構體大小 -
a
:存放實變量a
實際的值,相當於原局部變量的成員變量(和之前不加__block修飾符的時候一致)。
再來看一下 main()
函數中,__block int a
、__block int b
的賦值情況。
順便把代碼整理一下,使之簡易一點:
__Block_byref_a_0 a = {
(void*)0,
(__Block_byref_a_0 *)&a,
0,
sizeof(__Block_byref_a_0),
10
};
__Block_byref_b_1 b = {
0,
&b,
0,
sizeof(__Block_byref_b_1),
20
};
還是拿第一個__Block_byref_a_0 a
的賦值來說明。
可以看到 __isa
指針值傳空,__forwarding
指向了局部變量 a
本身的地址,__flags
分配了 0,__size
爲結構體的大小,a
賦值爲 10。下圖用來說明 __forwarding
指針的指向情況。
這下,我們知道 __forwarding
其實就是局部變量 a
本身的地址,那麼我們就可以通過 __forwarding
指針來訪問局部變量,同時也能對其進行修改了。
來看一下 Block 主體部分對應的 __main_block_func_0
結構體來驗證一下。
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_a_0 *a = __cself->a; // bound by ref
__Block_byref_b_1 *b = __cself->b; // bound by ref
(a->__forwarding->a) = 20;
(b->__forwarding->b) = 30;
printf("a = %d, b = %d\n",(a->__forwarding->a), (b->__forwarding->b));
}
可以看到 (a->__forwarding->a) = 20;
和 (b->__forwarding->b) = 30;
是通過指針取值的方式來改變了局部變量的值。這也就解釋了通過 __block
來修飾的變量,在 Block 的主體部分中改變值的原理其實是:通過『指針傳遞』的方式。
2.3 更改特殊區域變量值
除了通過 __block
說明符修飾的這種方式修改局部變量的值之外,還有一些特殊區域的變量,我們也可以在 Block 的內部將其修改。
這些特殊區域的變量包括:靜態局部變量、靜態全局變量、全局變量。
我們還是通過 OC 代碼和 C++ 源碼來說明一下:
- OC 代碼:
int global_val = 10; // 全局變量
static int static_global_val = 20; // 靜態全局變量
int main() {
static int static_val = 30; // 靜態局部變量
void (^myLocalBlock)(void) = ^{
global_val *= 1;
static_global_val *= 2;
static_val *= 3;
printf("static_val = %d, static_global_val = %d, global_val = %d\n",static_val, static_global_val, static_val);
};
myLocalBlock();
return 0;
}
- C++ 代碼:
int global_val = 10;
static int static_global_val = 20;
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int *static_val;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_val, int flags=0) : static_val(_static_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_val = __cself->static_val; // bound by copy
global_val *= 1;
static_global_val *= 2;
(*static_val) *= 3;
printf("static_val = %d, static_global_val = %d, global_val = %d\n",(*static_val), static_global_val, (*static_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() {
static int static_val = 30;
void (*myLocalBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &static_val));
((void (*)(__block_impl *))((__block_impl *)myLocalBlock)->FuncPtr)((__block_impl *)myLocalBlock);
return 0;
}
從中可以看到:
在 __main_block_impl_0
結構體中,將靜態局部變量 static_val
以指針的形式添加爲成員變量,而靜態全局變量 static_global_val
、全局變量 global_val
並沒有添加爲成員變量。
int global_val = 10;
static int static_global_val = 20;
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int *static_val;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_val, int flags=0) : static_val(_static_val) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
再來看一下 Block 主體部分對應的 __main_block_func_0
結構體部分。靜態全局變量 static_global_val
、全局變量 global_val
是直接訪問的,而靜態局部變量 static_val
則是通過『指針傳遞』的方式進行訪問和賦值。
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int *static_val = __cself->static_val; // bound by copy
global_val *= 1;
static_global_val *= 2;
(*static_val) *= 3;
printf("static_val = %d, static_global_val = %d, global_val = %d\n",(*static_val), static_global_val, (*static_val));
}
3. Block 的存儲區域
通過之前對 Block 本質的探索,我們知道了 Block 的本質是 Objective-C 對象。通過上述代碼中 impl.isa = &_NSConcreteStackBlock;
,可以知道該 Block 的類名爲 NSConcreteStackBlock
,根據名稱可以看出,該 Block 是存於棧區中的。而與之相關的,還有 _NSConcreteGlobalBlock
、_NSConcreteMallocBlock
。
3.1 _NSConcreteGlobalBlock
在以下兩種情況下使用 Block 的時候,Block 爲 NSConcreteGlobalBlock
類對象。
- 記述全局變量的地方,使用 Block 語法時;
- Block 語法的表達式中沒有截獲的自動變量時。
NSConcreteGlobalBlock
類的 Block 存儲在『程序的數據區域』。因爲存放在程序的數據區域,所以即使在變量的作用域外,也可以通過指針安全的使用。
- 記述全局變量的地方,使用 Block 語法示例代碼:
void (^myGlobalBlock)(void) = ^{
printf("GlobalBlock\n");
};
int main() {
myGlobalBlock();
return 0;
}
通過對應 C++ 源碼,我們可以發現:Block 結構體的成員變量 isa
賦值爲:impl.isa = &_NSConcreteGlobalBlock;
,說明該 Block 爲 NSConcreteGlobalBlock
類對象。
3.2 _NSConcreteStackBlock
除了 3.1 _NSConcreteGlobalBlock 中提到的兩種情形,其他情形下創建的 Block 都是 NSConcreteStackBlock
對象,平常接觸的 Block 大多屬於 NSConcreteStackBlock
對象。
NSConcreteStackBlock
類的 Block 存儲在『棧區』的。如果其所屬的變量作用域結束,則該 Block 就會被廢棄。如果 Block 使用了 __block
變量,則當 __block
變量的作用域結束,則 __block
變量同樣被廢棄。
3.3 _NSConcreteMallocBlock
爲了解決棧區上的 Block 在變量作用域結束被廢棄這一問題,Block 提供了 『複製』 功能。可以將 Block 對象和 __block
變量從棧區複製到堆區上。當 Block 從棧區複製到堆區後,即使棧區上的變量作用域結束時,堆區上的 Block 和 __block
變量仍然可以繼續存在,也可以繼續使用。
此時,『堆區』上的 Block 爲 NSConcreteMallocBlock
對象,Block 結構體的成員變量 isa 賦值爲:impl.isa = &_NSConcreteMallocBlock;
那麼,什麼時候纔會將 Block 從棧區複製到堆區呢?
這就涉及到了 Block 的自動拷貝和手動拷貝。
3.4 Block 的自動拷貝和手動拷貝
3.4.1 Block 的自動拷貝
在使用 ARC 時,大多數情形下編譯器會自動進行判斷,自動生成將 Block 從棧上覆制到堆上的代碼:
- 將 Block 作爲函數返回值返回時,會自動拷貝;
- 向方法或函數的參數中傳遞 Block 時,使用以下兩種方法的情況下,會進行自動拷貝,否則就需要手動拷貝:
- Cocoa 框架的方法且方法名中含有
usingBlock
等時; -
Grand Central Dispatch(GCD)
的 API。
- Cocoa 框架的方法且方法名中含有
3.4.2 Block 的手動拷貝
我們可以通過『copy 實例方法(即 alloc / new / copy / mutableCopy
)』來對 Block 進行手動拷貝。當我們不確定 Block 是否會被遺棄,需不需要拷貝的時候,直接使用 copy 實例方法即可,不會引起任何的問題。
關於 Block 不同類的拷貝效果總結如下:
Block 類 | 存儲區域 | 拷貝效果 |
---|---|---|
_NSConcreteStackBlock | 棧區 | 從棧拷貝到堆 |
_NSConcreteGlobalBlock | 程序的數據區域 | 不做改變 |
_NSConcreteMallocBlock | 堆區 | 引用計數增加 |
3.5 __block 變量的拷貝
在使用 __block
變量的 Block 從棧複製到堆上時,__block
變量也會受到如下影響:
__block 變量的配置存儲區域 | Block 從棧複製到堆時的影響 |
---|---|
堆區 | 從棧複製到堆,並被 Block 所持有 |
棧區 | 被 Block 所持有 |
當然,如果不再有 Block 引用該 __block
變量,那麼 __block
變量也會被廢除。
4. Block 的循環引用
從上文 2. Block 截獲局部變量和特殊區域變量 中我們知道 Block 會對引用的局部變量進行持有。同樣,如果 Block 也會對引用的對象進行持有(引用計數 + 1),從而會導致相互持有,引起循環引用。
/* —————— retainCycleBlcok.m —————— */
#import <Foundation/Foundation.h>
#import "Person.h"
int main() {
Person *person = [[Person alloc] init];
person.blk = ^{
NSLog(@"%@",person);
};
return 0;
}
/* —————— Person.h —————— */
#import <Foundation/Foundation.h>
typedef void(^myBlock)(void);
@interface Person : NSObject
@property (nonatomic, copy) myBlock blk;
@end
/* —————— Person.m —————— */
#import "Person.h"
@implementation Person
@end
我們將 retainCycleBlcok.m 轉換爲 C++ 代碼來看一下:
節選部分 C++ 代碼:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
Person *person;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, Person *_person, int flags=0) : person(_person) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
Person *person = __cself->person; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_ct_0dyw1pvj6k16t5z8t0j0_ghw0000gn_T_retainCycleBlcok_8957e0_mi_0,person);
}
可以看到 __main_block_impl_0
結構體中增加了成員變量 person
,同時 __main_block_func_0
結構體中也使用了 __cself->person
。
這樣就導致了:person
持有成員變量 myBlock blk
,而 blk
也同時持有成員變量 person
,就造成了循環引用問題。
那麼,如何來解決這個問題呢?
4.1 ARC 下,通過 __weak 修飾符來消除循環引用
在 ARC 下,可聲明附有 __weak
修飾符的變量,並將對象賦值使用。
int main() {
Person *person = [[Person alloc] init];
__weak typeof(person) weakPerson = person;
person.blk = ^{
NSLog(@"%@",weakPerson);
};
return 0;
}
這樣就可以解決循環引用的問題。我們再來轉換爲 C++ 代碼來看看。
這裏需要改下轉換 C++ 指令,因爲使用原指令會報錯:error: cannot create __weak reference because the current deployment target does not support weak references
這裏需要使用
clang -rewrite-objc -fobjc-arc -stdlib=libc++ -mmacosx-version-min=10.7 -fobjc-runtime=macosx-10.7 -Wno-deprecated-declarations retainCycleBlcok.m
命令來轉換。
使用 __weak
修飾後的 Block 示例代碼中,節選的部分 C++ 代碼:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
Person *__weak weakPerson;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, Person *__weak _weakPerson, int flags=0) : weakPerson(_weakPerson) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
Person *__weak weakPerson = __cself->weakPerson; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_ct_0dyw1pvj6k16t5z8t0j0_ghw0000gn_T_retainCycleBlcok_447367_mi_0,weakPerson);
}
可以看到,__main_block_impl_0
使用過了 __weak
對成員變量 person
進行弱引用。
這樣,person
持有成員變量 myBlock blk
,而 blk
對 person
進行弱引用,從而就消除了循環引用。
4.2 MRC 下,通過 __block 修飾符來消除循環引用
MRC 下,是不支持 __weak
修飾符的。我們可以通過 __block
來消除循環引用。
int main() {
Person *person = [[Person alloc] init];
__block typeof(person) blockPerson = person;
person.blk = ^{
NSLog(@"%@", blockPerson);
};
return 0;
}
使用
clang -rewrite-objc -fno-objc-arc -stdlib=libc++ -mmacosx-version-min=10.7 -fobjc-runtime=macosx-10.7 -Wno-deprecated-declarations retainCycleBlcok.m
命令來轉換爲 C++ 代碼。
使用 __block
修飾後的 Block 示例代碼中,節選的部分 C++ 代碼:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_blockPerson_0 *blockPerson; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_blockPerson_0 *_blockPerson, int flags=0) : blockPerson(_blockPerson->__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_blockPerson_0 *blockPerson = __cself->blockPerson; // bound by ref
NSLog((NSString *)&__NSConstantStringImpl__var_folders_ct_0dyw1pvj6k16t5z8t0j0_ghw0000gn_T_retainCycleBlcok_536cd4_mi_0,(blockPerson->__forwarding->blockPerson));
}
可以看到,通過 __block
引用的 blockPerson
,生成了 __Block_byref_blockPerson_0
結構體指針。這裏通過指針的方式來訪問 person
,而沒有對 person
進行強引用,所以不會造成循環引用。
參考資料
- 書籍:『Objective-C 高級編程 iOS 與OS X 多線程和內存管理』
- 博文:《Objective-C 高級編程》乾貨三部曲(二):Blocks篇
至此,Blocks 相關內容已經全部總結完畢,前前後後大概花費了差不多三週的時間。原本只是想簡單寫一下 Blocks 的基本應用,寫着寫着就去翻了下 『Objective-C 高級編程 iOS 與OS X 多線程和內存管理 』中關於 Block 的篇章。也借鑑了大佬關於這本書中對於 Blocks 的理解。然後就有了這篇關於 Blocks 的底層原理部分。
希望大家能夠喜歡。
- 本文作者: 行走少年郎
- 本文鏈接: https://www.jianshu.com/p/ba7ab9522cbc
- 版權聲明: 本文章採用 CC BY-NC-SA 3.0 許可協議。轉載請在文字開頭註明『本文作者』和『本文鏈接』!