下面我將經過一個簡單的例子,結合源代碼進行介紹api
int main(int argc, const char * argv[]) {
void (^blk)(void) = ^{ printf("Hello Block\n"); };
blk();
return 0;
}
複製代碼
使用clang -rewrite-objc main.m
,咱們能夠將 Objc 的源碼轉成 Cpp 的相關源碼:bash
int main(int argc, const char * argv[]) {
// Block 的建立
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;
}
複製代碼
由上面的源碼,咱們能猜測到:數據結構
__main_block_impl_0
結構體FuncPtr
函數指針的調用從這裏爲切入點看看上面提到的都是啥函數
Block 的真身:ui
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
// 省略了構造函數
};
複製代碼
__main_block_impl_0
名字的命名規則: __所在函數_block_impl_序號
__main_block_impl_0
的主要數據:this
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
複製代碼
isa
指針: 體現了 Block 是 Objc 對象的本質。FuncPtr
指針: 其實就是一個函數指針,指向所謂的匿名函數。__main_block_desc_0
中放着 Block 的描述信息spa
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)
};
複製代碼
__main_block_impl_0
即 Block 建立時候使用到了__main_block_func_0
正是下面的函數:3d
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("Hello Block\n");
}
複製代碼
^{ printf("Hello Block\n"); }
十分類似,由此可看出: 經過 Blocks 使用的匿名函數實際上被做爲簡單的 C 語言函數來處理main
)和該 Block 語法在函數出現的順序值(此處爲 0)來命名的。__cself
至關於 C++ 實例方法中指向實例自身的變量this
,或是 Objective-C 實例方法中指向對象自身的變量self
,即參數__cself
爲指向 Block 的變量。(*blk->impl.FuncPtr)(blk);
中的blk
就是__cself
介紹了基本的數據結構,下面到回到一開始的main
函數,看看 Block 具體的使用指針
void (*blk)(void) =
(void (*)(void))&__main_block_impl_0(
(void *)__main_block_func_0, &__main_block_desc_0_DATA);
/** 去掉轉換的部分
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
__main_block_func_0
的函數指針建立一個__main_block_impl_0
結構體,咱們用的時候是拿到了這個結構體的指針。((void (*)(struct __block_impl *))(
(struct __block_impl *)blk)->FuncPtr)((struct __block_impl *)blk);
/** 去掉轉換的部分
(*blk->impl.FuncPtr)(blk);
*/
複製代碼
__main_block_impl_0
中的函數指針FuncPtr
(blk)
這裏是傳入本身,就是給_cself
傳參從 Block 中的簡單實現中,咱們從
isa
中發現 Block 的本質是 Objc 對象,是對象就有不一樣類型的類。所以,Block 固然有不一樣的類型code
在 Apple 的libclosure-73
中的data.c
上可見,isa
可指向:
void * _NSConcreteStackBlock[32] = { 0 }; // 棧上建立的block
void * _NSConcreteMallocBlock[32] = { 0 }; // 堆上建立的block
void * _NSConcreteAutoBlock[32] = { 0 };
void * _NSConcreteFinalizingBlock[32] = { 0 };
void * _NSConcreteGlobalBlock[32] = { 0 }; // 做爲全局變量的block
void * _NSConcreteWeakBlockVariable[32] = { 0 };
複製代碼
其中咱們最多見的是:
Block的類型 | 名稱 | 行爲 | 存儲位置 |
---|---|---|---|
_NSConcreteStackBlock | 棧Block | 捕獲了局部變量 | 棧 |
_NSConcreteMallocBlock | 堆Block | 對棧Block調用copy所得 | 堆 |
_NSConcreteGlobalBlock | 全局Block | 定義在全局變量中 | 常量區(數據段) |
PS: 內存五大區:棧、堆、靜態區(BSS 段)、常量區(數據段)、代碼段
對象有copy
操做,Block 也有copy
操做。不一樣類型的 Block 調用copy
操做,也會產生不一樣的複製效果:
Block的類型 | 副本源的配置存儲域 | 複製效果 |
---|---|---|
_NSConcreteStackBlock | 棧 | 從棧複製到堆 |
_NSConcreteGlobalBlock | 常量區(數據段) | 什麼也不作 |
_NSConcreteMallocBlock | 堆 | 引用計數增長 |
copy
實例方法編譯器自動調用_Block_copy
函數狀況
id
或 Block 類型成員變量)PS: 在 ARC 環境下,聲明的 Block 屬性用copy
或strong
修飾的效果是同樣的,但在 MRC 環境下用 copy 修飾。
以全局變量、靜態全局變量、局部變量、靜態局部變量爲例:
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;
}
複製代碼
轉換後「匿名函數」對應的代碼:
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));
}
複製代碼
__main_block_impl_0
中存在val
實例,所以對於局部變量,Block 只是單純的複製建立時候局部變量的瞬時值,咱們可使用值,但不能修改值。struct __main_block_impl_0 {
// ...
int val; // 值傳遞
// ...
};
複製代碼
__main_block_impl_0
中存在static_val
指針,所以 Block 是在建立的時候獲取靜態局部變量的指針值。struct __main_block_impl_0 {
// ...
int *static_val; // 指針傳遞
// ...
};
複製代碼
模仿基礎類型變量,實例化四個不同的SCPeople
變量:
int main(int argc, const char * argv[]) {
// 省略初始化
[globalPeople introduce];
[staticGlobalPeople introduce];
[people introduce];
[staticPeople introduce];
return 0;
}
複製代碼
轉換後"匿名函數"對應的代碼:
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 轉換
[globalPeople introduce];
[staticGlobalPeople introduce];
[people introduce];
[*staticPeople introduce];
}
複製代碼
__main_block_impl_0
中存在people
指針實例,所以 Block 獲取的是指針瞬間值,咱們能夠在 Block 中經過指針能夠操做對象,可是不能改變指針的值。struct __main_block_impl_0 {
// ...
SCPeople *people;
// ...
};
複製代碼
__main_block_impl_0
中存在staticPeople
指針的指針,所以 Block 是在建立的時候獲取靜態局部對象的指針值(即指針的指針)。struct __main_block_impl_0 {
// ...
SCPeople **staticPeople;
// ...
};
複製代碼
經過對基礎類型、對象類型與四種不一樣的變量進行排列組合的小 Demo,不可貴出下面的規則:
變量類型 | 是否捕獲到 Block 內部 | 訪問方式 |
---|---|---|
全局變量 | 否 | 直接訪問 |
靜態全局變量 | 否 | 直接訪問 |
局部變量 | 是 | 值訪問 |
靜態局部變量 | 是 | 指針訪問 |
PS:
上面的篇幅經過底層實現,向你們介紹了 Block 這個所謂"匿名函數"是如何捕獲變量的,可是一些時候咱們須要修改 Block 中捕獲的變量:
全局變量與靜態全局變量的做用域都是全局的,天然在 Block 內外的變量操做都是同樣的。
在上面變量捕獲的章節中,咱們得知 Block 捕獲的是靜態局部變量的指針值,所以咱們能夠在 Block 內部改變靜態局部變量的值(底層是經過指針來進行操做的)。
使用
__block
修飾符來指定咱們想改變的局部變量,達到在 Block 中修改的須要。
咱們用一樣的方式,經過底層實現認識一下__block
,舉一個🌰:
__block int val = 0;
void (^blk)(void) = ^{ val = 1; };
blk();
複製代碼
通過轉換的代碼中出現了和單純捕獲局部變量不一樣的代碼:
__Block_byref_val_0
結構體struct __Block_byref_val_0 {
void *__isa; // 一個 Objc 對象的體現
__Block_byref_val_0 *__forwarding; // 指向該實例自身的指針
int __flags;
int __size;
int val; // 原局部變量
};
複製代碼
__block
修飾的變量包裝成一個 Objc 對象。val
轉換成__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
// ...
};
複製代碼
__main_block_impl_0
結構體實例持有指向__block
變量的__Block_byref_val_0
結構體實例的指針。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
是__main_block_impl_0
中的val
,這個val
經過__block int val
的地址初始化val
是__Block_byref_val_0
中的val
,正是__block int val
的val
__forwarding
在這裏只是單純指向了本身而已
上面的"棧Blcok"中__forwarding
在這裏只是單純指向本身,可是在當"棧Blcok"複製變成"堆Block"後,__forwarding
就有他的存在乎義了:
__block
修飾符不能用於修飾全局變量、靜態變量。
衆所周知,對象其實也是使用一個指針指向對象的存儲空間,咱們的對象值其實也是指針值。雖然是看似對象類型的捕獲與基礎類型的指針類型捕獲差很少,可是捕獲對象的轉換代碼比基礎指針類型的轉換代碼要多。(__block
變量也會變成一個對象,所以下面的內容也適用於__block
修飾局部變量的狀況)。多出來的部分是與內存管理相關的copy
函數與dispose
函數:
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*/);
}
複製代碼
這兩個函數在 Block 數據結構存在於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*);
}; // 省略了初始化好的結構體
複製代碼
函數 | 調用時機 |
---|---|
copy 函數 | 棧上的 Block 複製到堆時 |
dispose 函數 | 堆上的 Block 被廢棄時 |
copy
函數中的_Block_object_assign
函數至關於內存管理中的retain
函數,將對象賦值在對象類型的結構體成員變量中。dispose
函數中的_Block_object_dispose
函數至關於內存管理中的release
函數,釋放賦值在對象類型的結構體變量中的對象。copy
和dispose
並配合 Objc 運行時庫對其的調用能夠實現內存管理當 Block 內部訪問了對象類型的局部變量時:
copy
到堆上時: Block 會調用內部的copy
函數,copy
函數內部會調用_Block_object_assign
函數,_Block_object_assign
函數會根據局部變量的修飾符(__strong
、__weak
、__unsafe_unretained
)做出相應的內存管理操做。(注意: 多個 Block 對同一個對象進行強引用的時,堆上只會存在一個該對象)dispose
函數,dispose
函數內部會調用_Block_object_dispose
函數,_Block_object_dispose
函數會自動release
引用的局部變量。(注意: 直到被引用的對象的引用計數爲 0,這個堆上的該對象纔會真正釋放)PS: 對於__block
變量,Block 永遠都是對__Block_byref_局部變量名_0
進行強引用。若是__block
修飾符背後還有其餘修飾符,那麼這些修飾符是用於修飾__Block_byref_局部變量名_0
中的局部變量
的。
現象: Block 中使用的賦值給附有
__strong
修飾符的局部變量的對象和複製到堆上的__block
變量因爲被堆的 Block 所持有,於是可超出其變量做用域而存在。
因爲 Block 內部能強引用捕獲的對象,所以當該 Block 被對象強引用的時候就是注意如下的引用循環問題了:
弱引用持有:使用__weak
或__unsafe_unretained
捕獲對象解決
weak
修飾的指針變量,在指向的內存地址銷燬後,會在 Runtime 的機制下,自動置爲nil
。_unsafe_unretained
不會置爲nil
,容易出現懸垂指針,發生崩潰。可是_unsafe_unretained
比__weak
效率高。使用__block
變量:使用__block
修飾對象,在 block 內部用完該對象後,將__block
變量置爲nil
便可。雖然能控制對象的持有期間,而且能將其餘對象賦值在__block
變量中,可是必須執行該 block。(意味着這個對象的生命週期徹底歸咱們控制)
__unsafe_unretained
捕獲對象__block
修飾對象,無需手動將對象置爲nil
,由於底層_Block_object_assign
函數在 MRC 環境下對 block 內部的對象不會進行retain
操做。ARC 無效時,須要手動將 Block 從棧複製到堆,也須要手動釋放 Block
retain
實例方法是不起做用的copy
實例方式(引用計數+1),將其配置在堆上,纔可繼續使用retain
實例方法release
實例方法便可。Block_copy
和Block_release
代替copy
和release
。