定義
- Block 是 C 語言的擴充功能
- Block 是帶有自動變量(局部變量)的匿名函數(shù)
本質(zhì)
- Block 是一個 Objc 對象
底層實現(xiàn)
下面我將通過一個簡單的例子,結(jié)合源代碼進行介紹
int main(int argc, const char * argv[]) {
void (^blk)(void) = ^{ printf("Hello Block\n"); };
blk();
return 0;
}
使用clang -rewrite-objc main.m,我們可以將 Objc 的源碼轉(zhuǎn)成 Cpp 的相關(guān)源碼:
int main(int argc, const char * argv[]) {
// Block 的創(chuàng)建
void (*blk)(void) =
(void (*)(void))&__main_block_impl_0(
(void *)__main_block_func_0, &__main_block_desc_0_DATA);
// Block 的使用
((void (*)(struct __block_impl *))(
(struct __block_impl *)blk)->FuncPtr)((struct __block_impl *)blk);
return 0;
}
由上面的源碼,我們能猜想到:
- Block 的創(chuàng)建涉及
__main_block_impl_0結(jié)構(gòu)體 - Block 的涉及到了
FuncPtr函數(shù)指針的調(diào)用
從這里為切入點看看上面提到的都是啥
Block 的數(shù)據(jù)結(jié)構(gòu)
Block 的真身:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
// 省略了構(gòu)造函數(shù)
};
- Block 其實不是一個匿名函數(shù),他是一個結(jié)構(gòu)體
-
__main_block_impl_0名字的命名規(guī)則:__所在函數(shù)_block_impl_序號
impl 變量的數(shù)據(jù)結(jié)構(gòu)
__main_block_impl_0的主要數(shù)據(jù):
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
-
isa指針: 體現(xiàn)了 Block 是 Objc 對象的本質(zhì)。 -
FuncPtr指針: 其實就是一個函數(shù)指針,指向所謂的匿名函數(shù)。
Desc 變量的數(shù)據(jù)結(jié)構(gòu)
__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)
};
"匿名函數(shù)"
__main_block_impl_0即 Block 創(chuàng)建時候使用到了__main_block_func_0正是下面的函數(shù):
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("Hello Block\n");
}
- 這部分和
^{ printf("Hello Block\n"); }十分相似,由此可看出: 通過 Blocks 使用的匿名函數(shù)實際上被作為簡單的 C 語言函數(shù)來處理 - 函數(shù)名是根據(jù) Block 語法所屬的函數(shù)名(此處
main)和該 Block 語法在函數(shù)出現(xiàn)的順序值(此處為 0)來命名的。 - 函數(shù)的參數(shù)
__cself相當(dāng)于 C++ 實例方法中指向?qū)嵗陨淼淖兞?code>this,或是 Objective-C 實例方法中指向?qū)ο笞陨淼淖兞?code>self,即參數(shù)__cself為指向 Block 的變量。 - 上面的
(*blk->impl.FuncPtr)(blk);中的blk就是__cself
介紹了基本的數(shù)據(jù)結(jié)構(gòu),下面到回到一開始的main函數(shù),看看 Block 具體的使用
Block 的創(chuàng)建
void (*blk)(void) =
(void (*)(void))&__main_block_impl_0(
(void *)__main_block_func_0, &__main_block_desc_0_DATA);
/** 去掉轉(zhuǎn)換的部分
struct __main_block_impl_0 tmp =
__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA);
struct __main_block_impl_0 *blk = &tmp;
*/
-
void (^blk)(void)就是是一個struct __main_block_impl_0 *blk - Block 表達式的其實就是通過所謂的匿名函數(shù)
__main_block_func_0的函數(shù)指針創(chuàng)建一個__main_block_impl_0結(jié)構(gòu)體,我們用的時候是拿到了這個結(jié)構(gòu)體的指針。
Block 的使用
((void (*)(struct __block_impl *))(
(struct __block_impl *)blk)->FuncPtr)((struct __block_impl *)blk);
/** 去掉轉(zhuǎn)換的部分
(*blk->impl.FuncPtr)(blk);
*/
- Block 真正的使用方法就是使用
__main_block_impl_0中的函數(shù)指針FuncPtr -
(blk)這里是傳入自己,就是給_cself傳參
Block 的類型
從 Block 中的簡單實現(xiàn)中,我們從
isa中發(fā)現(xiàn) Block 的本質(zhì)是 Objc 對象,是對象就有不同類型的類。因此,Block 當(dāng)然有不同的類型
在 Apple 的libclosure-73中的data.c上可見,isa可指向:
void * _NSConcreteStackBlock[32] = { 0 }; // 棧上創(chuàng)建的block
void * _NSConcreteMallocBlock[32] = { 0 }; // 堆上創(chuàng)建的block
void * _NSConcreteAutoBlock[32] = { 0 };
void * _NSConcreteFinalizingBlock[32] = { 0 };
void * _NSConcreteGlobalBlock[32] = { 0 }; // 作為全局變量的block
void * _NSConcreteWeakBlockVariable[32] = { 0 };
其中我們最常見的是:
| Block的類型 | 名稱 | 行為 | 存儲位置 |
|---|---|---|---|
| _NSConcreteStackBlock | 棧Block | 捕獲了局部變量 | 棧 |
| _NSConcreteMallocBlock | 堆Block | 對棧Block調(diào)用copy所得 | 堆 |
| _NSConcreteGlobalBlock | 全局Block | 定義在全局變量中 | 常量區(qū)(數(shù)據(jù)段) |
PS: 內(nèi)存五大區(qū):棧、堆、靜態(tài)區(qū)(BSS 段)、常量區(qū)(數(shù)據(jù)段)、代碼段
關(guān)于 copy 操作
對象有copy操作,Block 也有copy操作。不同類型的 Block 調(diào)用copy操作,也會產(chǎn)生不同的復(fù)制效果:
| Block的類型 | 副本源的配置存儲域 | 復(fù)制效果 |
|---|---|---|
| _NSConcreteStackBlock | 棧 | 從棧復(fù)制到堆 |
| _NSConcreteGlobalBlock | 常量區(qū)(數(shù)據(jù)段) | 什么也不做 |
| _NSConcreteMallocBlock | 堆 | 引用計數(shù)增加 |
棧上的 Block 復(fù)制到堆上的時機
- 調(diào)用 Block 的
copy實例方法
編譯器自動調(diào)用_Block_copy函數(shù)情況
- Block 作為函數(shù)返回值返時
- 將 Block 賦值給 __strong 指針(
id或 Block 類型成員變量) - 在 Apple 的 Cocoa、GCD 等 api 中傳遞 Block 時
PS: 在 ARC 環(huán)境下,聲明的 Block 屬性用copy或strong修飾的效果是一樣的,但在 MRC 環(huán)境下用 copy 修飾。
捕獲變量
基礎(chǔ)類型變量
以全局變量、靜態(tài)全局變量、局部變量、靜態(tài)局部變量為例:
int global_val = 1;
static int static_global_val = 2;
int main(int argc, const char * argv[]) {
int val = 3;
static int static_val = 4;
void (^blk)(void) = ^{
printf("global_val is %d\n", global_val);
printf("static_global_val is %d\n", static_global_val);
printf("val is %d\n", val);
printf("static_val is %d\n", static_val);
};
blk();
return 0;
}
轉(zhuǎn)換后“匿名函數(shù)”對應(yīng)的代碼:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int val = __cself->val; // bound by copy
int *static_val = __cself->static_val; // bound by copy
printf("global_val is %d\n", global_val);
printf("static_global_val is %d\n", static_global_val);
printf("val is %d\n", val);
printf("static_val is %d\n", (*static_val));
}
- 全局變量、靜態(tài)全局變量: 作用域為全局,因此在 Block 中是直接訪問的。
-
局部變量: 生成的
__main_block_impl_0中存在val實例,因此對于局部變量,Block 只是單純的復(fù)制創(chuàng)建時候局部變量的瞬時值,我們可以使用值,但不能修改值。
struct __main_block_impl_0 {
// ...
int val; // 值傳遞
// ...
};
-
靜態(tài)局部變量: 生成的
__main_block_impl_0中存在static_val指針,因此 Block 是在創(chuàng)建的時候獲取靜態(tài)局部變量的指針值。
struct __main_block_impl_0 {
// ...
int *static_val; // 指針傳遞
// ...
};
對象類型變量
模仿基礎(chǔ)類型變量,實例化四個不一樣的SCPeople變量:
int main(int argc, const char * argv[]) {
// 省略初始化
[globalPeople introduce];
[staticGlobalPeople introduce];
[people introduce];
[staticPeople introduce];
return 0;
}
轉(zhuǎn)換后"匿名函數(shù)"對應(yīng)的代碼:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
SCPeople *people = __cself->people; // bound by copy
SCPeople **staticPeople = __cself->staticPeople; // bound by copy
// 省略 objc_msgSend 轉(zhuǎn)換
[globalPeople introduce];
[staticGlobalPeople introduce];
[people introduce];
[*staticPeople introduce];
}
- 全局對象、靜態(tài)全局對象: 作用域依然是全局,因此在 Block 中是直接訪問的。
-
局部對象: 生成的
__main_block_impl_0中存在people指針實例,因此 Block 獲取的是指針?biāo)查g值,我們可以在 Block 中通過指針可以操作對象,但是不能改變指針的值。
struct __main_block_impl_0 {
// ...
SCPeople *people;
// ...
};
-
靜態(tài)局部對象: 生成的
__main_block_impl_0中存在staticPeople指針的指針,因此 Block 是在創(chuàng)建的時候獲取靜態(tài)局部對象的指針值(即指針的指針)。
struct __main_block_impl_0 {
// ...
SCPeople **staticPeople;
// ...
};
小結(jié)
通過對基礎(chǔ)類型、對象類型與四種不同的變量進行排列組合的小 Demo,不難得出下面的規(guī)則:
| 變量類型 | 是否捕獲到 Block 內(nèi)部 | 訪問方式 |
|---|---|---|
| 全局變量 | 否 | 直接訪問 |
| 靜態(tài)全局變量 | 否 | 直接訪問 |
| 局部變量 | 是 | 值訪問 |
| 靜態(tài)局部變量 | 是 | 指針訪問 |
PS:
- 基礎(chǔ)類型和對象指針類型其實是一樣的,只不過指針的指針看起來比較繞而已。
- 全局變量與靜態(tài)全局變量的存儲方式、生命周期是相同的。但是作用域不同,全局變量在所有文件中都可以訪問到,而靜態(tài)全局變量只能在其申明的文件中才能訪問到。
變量修改
上面的篇幅通過底層實現(xiàn),向大家介紹了 Block 這個所謂"匿名函數(shù)"是如何捕獲變量的,但是一些時候我們需要修改 Block 中捕獲的變量:
修改全局變量或靜態(tài)全局變量
全局變量與靜態(tài)全局變量的作用域都是全局的,自然在 Block 內(nèi)外的變量操作都是一樣的。
修改靜態(tài)局部變量
在上面變量捕獲的章節(jié)中,我們得知 Block 捕獲的是靜態(tài)局部變量的指針值,因此我們可以在 Block 內(nèi)部改變靜態(tài)局部變量的值(底層是通過指針來進行操作的)。
修改局部變量
使用
__block修飾符來指定我們想改變的局部變量,達到在 Block 中修改的需要。
我們用同樣的方式,通過底層實現(xiàn)認識一下__block,舉一個??:
__block int val = 0;
void (^blk)(void) = ^{ val = 1; };
blk();
經(jīng)過轉(zhuǎn)換的代碼中出現(xiàn)了和單純捕獲局部變量不同的代碼:
__Block_byref_val_0結(jié)構(gòu)體
struct __Block_byref_val_0 {
void *__isa; // 一個 Objc 對象的體現(xiàn)
__Block_byref_val_0 *__forwarding; // 指向該實例自身的指針
int __flags;
int __size;
int val; // 原局部變量
};
- 編譯器會將
__block修飾的變量包裝成一個 Objc 對象。
val轉(zhuǎn)換成__Block_byref_val_0
__attribute__((__blocks__(byref))) __Block_byref_val_0 val = {
(void*)0,
(__Block_byref_val_0 *)&val,
0,
sizeof(__Block_byref_val_0),
0
};
__main_block_impl_0捕獲的變量
struct __main_block_impl_0 {
// ...
__Block_byref_val_0 *val; // by ref
// ...
};
- Block的
__main_block_impl_0結(jié)構(gòu)體實例持有指向__block變量的__Block_byref_val_0結(jié)構(gòu)體實例的指針。 - 這個捕獲方式和捕獲靜態(tài)局部變量相似,都是指針傳遞
"匿名函數(shù)"的操作
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
__Block_byref_val_0 *val = __cself->val; // bound by ref
(val->__forwarding->val) = 1;
}
(val->__forwarding->val) 解釋
- 左邊的
val是__main_block_impl_0中的val,這個val通過__block int val的地址初始化 - 右邊的
val是__Block_byref_val_0中的val,正是__block int val的val -
__forwarding在這里只是單純指向了自己而已
val->__forwarding->val
__forwarding 的存在意義
上面的"棧Blcok"中__forwarding在這里只是單純指向自己,但是在當(dāng)"棧Blcok"復(fù)制變成"堆Block"后,__forwarding就有他的存在意義了:
PS:
__block修飾符不能用于修飾全局變量、靜態(tài)變量。
內(nèi)存管理
Block 與對象類型
copy & dispose
眾所周知,對象其實也是使用一個指針指向?qū)ο蟮拇鎯臻g,我們的對象值其實也是指針值。雖然是看似對象類型的捕獲與基礎(chǔ)類型的指針類型捕獲差不多,但是捕獲對象的轉(zhuǎn)換代碼比基礎(chǔ)指針類型的轉(zhuǎn)換代碼要多。(__block變量也會變成一個對象,因此下面的內(nèi)容也適用于__block修飾局部變量的情況)。多出來的部分是與內(nèi)存管理相關(guān)的copy函數(shù)與dispose函數(shù):
底層實現(xiàn)
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
_Block_object_assign((void*)&dst->people, (void*)src->people, 3/*BLOCK_FIELD_IS_OBJECT*/);
_Block_object_assign((void*)&dst->staticPeople, (void*)src->staticPeople, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {
_Block_object_dispose((void*)src->people, 3/*BLOCK_FIELD_IS_OBJECT*/);
_Block_object_dispose((void*)src->staticPeople, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
這兩個函數(shù)在 Block 數(shù)據(jù)結(jié)構(gòu)存在于Desc變量中:
static struct __main_block_desc_0 {
// ...
void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
void (*dispose)(struct __main_block_impl_0*);
}; // 省略了初始化好的結(jié)構(gòu)體
函數(shù)調(diào)用時機
| 函數(shù) | 調(diào)用時機 |
|---|---|
| copy 函數(shù) | 棧上的 Block 復(fù)制到堆時 |
| dispose 函數(shù) | 堆上的 Block 被廢棄時 |
函數(shù)意義
-
copy函數(shù)中的_Block_object_assign函數(shù)相當(dāng)于內(nèi)存管理中的retain函數(shù),將對象賦值在對象類型的結(jié)構(gòu)體成員變量中。 -
dispose函數(shù)中的_Block_object_dispose函數(shù)相當(dāng)于內(nèi)存管理中的release函數(shù),釋放賦值在對象類型的結(jié)構(gòu)體變量中的對象。 - 通過
copy和dispose并配合 Objc 運行時庫對其的調(diào)用可以實現(xiàn)內(nèi)存管理
※ 例子
當(dāng) Block 內(nèi)部訪問了對象類型的局部變量時:
- 當(dāng) Block 存儲在棧上時: Block 不會對局部變量產(chǎn)生強引用。
-
當(dāng) Block 被
copy到堆上時: Block 會調(diào)用內(nèi)部的copy函數(shù),copy函數(shù)內(nèi)部會調(diào)用_Block_object_assign函數(shù),_Block_object_assign函數(shù)會根據(jù)局部變量的修飾符(__strong、__weak、__unsafe_unretained)作出相應(yīng)的內(nèi)存管理操作。(注意: 多個 Block 對同一個對象進行強引用的時,堆上只會存在一個該對象) -
當(dāng) Block 從堆上被移除時: Block 會調(diào)用內(nèi)部的
dispose函數(shù),dispose函數(shù)內(nèi)部會調(diào)用_Block_object_dispose函數(shù),_Block_object_dispose函數(shù)會自動release引用的局部變量。(注意: 直到被引用的對象的引用計數(shù)為 0,這個堆上的該對象才會真正釋放)
PS: 對于__block變量,Block 永遠都是對__Block_byref_局部變量名_0進行強引用。如果__block修飾符背后還有其他修飾符,那么這些修飾符是用于修飾__Block_byref_局部變量名_0中的局部變量的。
現(xiàn)象: Block 中使用的賦值給附有
__strong修飾符的局部變量的對象和復(fù)制到堆上的__block變量由于被堆的 Block 所持有,因而可超出其變量作用域而存在。
循環(huán)引用
由于 Block 內(nèi)部能強引用捕獲的對象,因此當(dāng)該 Block 被對象強引用的時候就是注意以下的引用循環(huán)問題了:
ARC 環(huán)境下解決方案
-
弱引用持有:使用
__weak或__unsafe_unretained捕獲對象解決
弱引用持有-
weak修飾的指針變量,在指向的內(nèi)存地址銷毀后,會在 Runtime 的機制下,自動置為nil。 -
_unsafe_unretained不會置為nil,容易出現(xiàn)懸垂指針,發(fā)生崩潰。但是_unsafe_unretained比__weak效率高。
-
-
使用
__block變量:使用__block修飾對象,在 block 內(nèi)部用完該對象后,將__block變量置為nil即可。雖然能控制對象的持有期間,并且能將其他對象賦值在__block變量中,但是必須執(zhí)行該 block。(意味著這個對象的生命周期完全歸我們控制)
使用`__block`變量
MRC 環(huán)境下解決方案
- 弱引用持有:使用
__unsafe_unretained捕獲對象 - 直接使用
__block修飾對象,無需手動將對象置為nil,因為底層_Block_object_assign函數(shù)在 MRC 環(huán)境下對 block 內(nèi)部的對象不會進行retain操作。
MRC 下的 Block
ARC 無效時,需要手動將 Block 從棧復(fù)制到堆,也需要手動釋放 Block
- 對于棧上的 Block 調(diào)用
retain實例方法是不起作用的 - 對于棧上的 Block 需要調(diào)用一次
copy實例方式(引用計數(shù)+1),將其配置在堆上,才可繼續(xù)使用retain實例方法 - 需要減少引用的時候,只需調(diào)用
release實例方法即可。 - 對于在 C 語言中使用 Block,需要使用
Block_copy和Block_release代替copy和release。