Blocks的實現

窗前有月光發表於2018-12-21

前言

Blocks的原理,每當自己對知識體系有一定提升之後,再回過頭來看一下曾經讀過的書籍,會發現對它的理解逐步加深。藉著讀書筆記活動,立個小目標,把Block徹底搞明白,重讀《Objective-C高階程式設計 iOS與OS X多執行緒和記憶體管理》第二章節block原理部分,一方面給自己做個筆記,另一方面加深一下印象。

目錄

  • Block的實質
  • Block捕獲自動變數的值
  • __block的實質
  • Block儲存域
  • __block變數儲存域
  • 截獲物件
  • __block變數和物件
  • Block迴圈引用

1.block實質

block程式碼:

void (^blk)(void) = ^ { 
printf("Block");

};
blk();
複製程式碼

執行xcrun -sdk iphonesimulator clang -rewrite-objc 原始碼檔名就能將含有Block的程式碼轉換為C++的原始碼。我是按照書上的示例,同樣轉換的main.m檔案,轉換完之後這裡就會多出一個main.cpp的檔案,開啟很恐怖,六萬多行...

Blocks的實現

實際上和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;

}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
printf("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)
};
int main(int argc, char * argv[]) {
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結構看上去有點抽象,不過理解起來並不難。

首先先從__main_block_func_0函式開始,因為我們想要執行的回撥看原始碼都是寫在這個函式裡面的,block使用的匿名函式(也就是我們定義的block)實際上被作為簡單的C語言函式(block__main_block_func_0)來處理,該函式的引數__cself相當於OC例項方法中指向物件自身的變數self,即__self為指向Block值的變數。__self與OC裡面的self相同也是一個結構體指標,是__main_block_impl_0結構體的指標,這個結構體宣告如下:

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,也是一個結構體,宣告如下:

struct __block_impl { 
void *isa;
int Flags;
int Reserved;
void *FuncPtr;

};
複製程式碼

先看FuncPrt,這個就是block括號中函式的函式指標,呼叫它就能執行block括號中的函式,實際上在呼叫block的時候就是呼叫的這個函式指標,執行它指向的具體函式實現。第二個成員變數是Desc指標,以下為其__main_block_desc_0結構體宣告:

static struct __main_block_desc_0 { 
size_t reserved;
size_t Block_size;

}複製程式碼

其結構為今後版本升級所需的區域和Block的大小。實際上__main_block_impl_0結構體展開最後就是這樣:

struct __main_block_impl_0 { 
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
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;

}複製程式碼

這就是整個__main_block_impl_0結構體所包含的,既然定義了這個結構體的初始化函式,那在詳細看一下它的初始化過程,實際上該結構體會像下面這樣初始化:

isa = &
_NSConcreteStackBlock;
Flags = 0;
Reserved = 0;FuncPtr = __main_block_func_0;
Desc = &
__main_block_desc_0_DATA;
複製程式碼

__main_block_func_0這不就是上面說到的那個指向函式實現的那個函式指標,也就是說只需要呼叫到結構體裡面的FuncPtr就能呼叫到我們的具體實現了。那這個建構函式在哪裡初始化的,看上面的原始碼是在我們定義block的時候:

void (*blk)(void) = ((void (*)())&
__main_block_impl_0((void *)__main_block_func_0, &
__main_block_desc_0_DATA));
複製程式碼

簡化為:

struct __mian_block_impl_0 tmp = __main_block_impl_0(__main_block_func_0, &
__main_block_desc_0_DATA);
struct __main_block_impl_0 *blk = &
tmp;
複製程式碼

該原始碼將__mian_block_impl_0結構體型別的自動變數,即棧上生成的__mian_block_impl_0結構體例項的指標,賦值給__mian_block_impl_0結構體指標型別的變數blk。聽起來有點繞,實際上就是我們最開始定義的blk__main_block_impl_0結構體指標指向了__main_block_impl_0結構體的例項。

接下來看看__main_block_impl_0結構體例項的構造引數:

__main_block_impl_0(__main_block_func_0, &
__main_block_desc_0_DATA);
複製程式碼

第一個引數為由Block語法轉換的C語言函式指標,第二個引數是作為靜態全域性變數初始化的__main_block_desc_0結構體例項指標,以下為__main_block_desc_0結構體例項的初始化部分程式碼:

static struct __main_block_desc_0 __main_block_desc_0_DATA = { 
0, sizeof(struct __main_block_impl_0)
};
複製程式碼

__main_block_impl_0結構體例項的大小。

接下來看看棧上的__main_block_impl_0結構體例項(即Block)是如何根據這些引數進行初始化的。也就是blk()的具體實現:

((void (*)(__block_impl *))((__block_impl *)blk)->
FuncPtr)((__block_impl *)blk);
複製程式碼

簡化以下:

(*blk->
impl.FuncPtr)(blk);
複製程式碼

FuncPtr正是我們初始化__main_block_desc_0結構體例項時候傳進去的函式指標,這裡使用這個函式指標呼叫了這個函式,正如我們剛才所說的,有block語法轉換的__main_block_func_0函式的指標被賦值成員變數FuncPtr中。blk也是作為引數進行傳遞的,也就是最開始講到的__cself。到此block的初始化和呼叫過程就結束了。

2.Block捕獲自動變數的值

基於上面的例子,額外增加個區域性變數val:

 int val = 0;
void (^blk)(void) = ^{
NSLog(@"%d",val);

};
val = 10;
blk();
複製程式碼

轉換為C++程式碼如下:

struct __main_block_impl_0 { 
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int val;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _val, int flags=0) : val(_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 val = __cself->
val;
// bound by copy NSLog((NSString *)&
__NSConstantStringImpl__var_folders_p6_239crx8x16s8vby9bfclq5d40000gn_T_main_057be8_mi_0,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(int argc, char * argv[]) {
int val = 0;
void (*blk)(void) = ((void (*)())&
__main_block_impl_0((void *)__main_block_func_0, &
__main_block_desc_0_DATA, val));
val = 10;
((void (*)(__block_impl *))((__block_impl *)blk)->
FuncPtr)((__block_impl *)blk);

}複製程式碼

這與上一節轉換的程式碼稍有差異,自動變數被作為了成員變數追加到了__main_block_impl_0結構體中。在__main_block_impl_0結構體中宣告的成員變數型別與自動變數型別完全相同(block語法表示式中,沒有使用的自動變數不會被追加,也就是如果變數沒有在block內被使用,是不會被捕獲到的)。

另外__main_block_impl_0結構體的建構函式與上一篇也有差異:

__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _val, int flags=0) : val(_val) { 
impl.isa = &
_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;

}複製程式碼

在初始化__main_block_impl_0結構體例項時,自動變數val被以引數的形式傳遞到了結構體裡面,就是在我們定義block的時候,捕獲的自動變數會被用來初始化這個結構體:

void (*blk)(void) = ((void (*)())&
__main_block_impl_0((void *)__main_block_func_0, &
__main_block_desc_0_DATA, val));
複製程式碼

實際上帶有這種自動變數的block會像下面這樣初始化:

impl.isa = &
_NSConcreteStackBlock;
impl.Flags = 0;
impl.FuncPtr = __main_block_func_0;
Desc = &
__main_block_desc_0_DATA;
val = 0;
複製程式碼

由此可以看到,在__main_block_impl_0結構體被初始化的時候,變數val的值被捕獲到了並且賦值給了__main_block_impl_0結構體裡面的_val成員變數,其實是值的捕獲,並非記憶體地址,所以我們在外部無法修改。

static void __main_block_func_0(struct __main_block_impl_0 *__cself) { 
int val = __cself->
val;
// bound by copy NSLog((NSString *)&
__NSConstantStringImpl__var_folders_p6_239crx8x16s8vby9bfclq5d40000gn_T_main_057be8_mi_0,val);

}複製程式碼

__cself->
val
__cself上一篇已經講過了它指向的就是這個block物件__cself->
val
就是訪問的__main_block_impl_0的成員變數_val,而自動變數的值又賦給了_val,所以我們在外部改變自動變數的值在block內部是不會生效的。

3.__block的實質

我們如果想要修改block截獲的自動變數的值,靜態全域性變數,全域性變數和靜態變數,block內是不會捕獲到他們的的,所以這類變數在block內部,是可以進行改寫值的。那麼他們具體在程式碼層面上是怎麼做的還是通過上面的命令看一下原始碼:

int global_var = 1;
static int static_global_var = 2;
int main(int argc, char * argv[]) {
static int static_var = 3;
void (^blk)(void) = ^{
global_var *= 1;
static_global_var *= 2;
static_var *= 3;

};
blk();
return 0;

}複製程式碼

經過轉換後的原始碼:

int global_var = 1;
static int static_global_var = 2;
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int *static_var;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_var, int flags=0) : static_var(_static_var) {
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_var = __cself->
static_var;
// bound by copy global_var *= 1;
static_global_var *= 2;
(*static_var) *= 3;

}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 argc, char * argv[]) {
static int static_var = 3;
void (*blk)(void) = ((void (*)())&
__main_block_impl_0((void *)__main_block_func_0, &
__main_block_desc_0_DATA, &
static_var));
((void (*)(__block_impl *))((__block_impl *)blk)->
FuncPtr)((__block_impl *)blk);
return 0;

}複製程式碼

對靜態全域性變數static_global_var和全域性變數global_var的訪問與轉換前完全相同,那麼靜態區域性變數是如何轉換的呢:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) { 
int *static_var = __cself->
static_var;
// bound by copy global_var *= 1;
static_global_var *= 2;
(*static_var) *= 3;

}複製程式碼

通過int *static_var = __cself->
static_var
能夠看出,實際上是將靜態區域性變數static_var的指標傳遞給了__main_block_impl_0結構體,這也是超出變數作用域去使用變數的一種方法,那就是通過指標去訪問。那為什麼在區域性變數不這麼使用呢,這個後面再說,我們現在只需要知道static修飾的區域性變數是可以在block內部進行值的改變的。迴歸主題,那麼自動變數我們是怎麼去修改它的值的,就是通過__block進行修飾,看下程式碼:

int main(int argc, char * argv[]) { 
__block int var = 10;
void (^blk)(void) = ^{
var = 1;

};
blk();
return 0;

}複製程式碼

轉換後如下:

struct __Block_byref_var_0 { 
void *__isa;
__Block_byref_var_0 *__forwarding;
int __flags;
int __size;
int var;

};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_var_0 *var;
// by ref __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_var_0 *_var, int flags=0) : var(_var->
__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_var_0 *var = __cself->
var;
// bound by ref (var->
__forwarding->
var) = 1;

}static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&
dst->
var, (void*)src->
var, 8/*BLOCK_FIELD_IS_BYREF*/);

}static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->
var, 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(int argc, char * argv[]) {
__attribute__((__blocks__(byref))) __Block_byref_var_0 var = {(void*)0,(__Block_byref_var_0 *)&
var, 0, sizeof(__Block_byref_var_0), 10
};
void (*blk)(void) = ((void (*)())&
__main_block_impl_0((void *)__main_block_func_0, &
__main_block_desc_0_DATA, (__Block_byref_var_0 *)&
var, 570425344));
((void (*)(__block_impl *))((__block_impl *)blk)->
FuncPtr)((__block_impl *)blk);
return 0;

}複製程式碼

不難發現多了一個__Block_byref_var_0結構體例項,它也正是__block的實現。該結構體中的成員變數var就相當於block外面的自動變數的成員變數。然後我們再看一下block是怎麼給這個成員變數進行賦值操作的:

static void __main_block_func_0(struct __main_block_impl_0 *__cself) { 
__Block_byref_var_0 *var = __cself->
var;
// bound by ref (var->
__forwarding->
var) = 1;

}複製程式碼

剛剛在block中給靜態變數賦值的時候,使用了指向該靜態變數的指標,但是用__block修飾的時候,實際上能夠看到__Block_byref_var_0結構體中也就是__block有一個成員變數__Block_byref_var_0 *__forwarding,是一個指向該例項自身的指標,通過成員變數__forwarding就能訪問到它自身的var,那麼究竟為什麼要通過這個指向自身的__forwarding來訪問成員變數var下一節會說,我們先知道它就是使用這種方式來訪問這個自動變數的。實際上我們為什麼能訪問到這個成員變數var,是因為在給自動變數定義為__block型別的時候,就會初始化一個__Block_byref_var_0型別的結構體,並且預設將該變數初始化為10(因為我們給var初始化的10),相當於持有了原自動變數的成員變數。然後在初始化__main_block_impl_0結構體的時候就將這個block結構體作為引數傳遞了過去,這樣__cself->
var
實際上就是我們剛才說的初始化的block的結構體,var->
__forwarding->
var
就是訪問了這個block的結構體的__forwarding成員變數,__forwarding成員變數指向的又是自身,所以__forwarding->
var
返回的就是自身的成員變數var,這樣整個流程就走通了,具體為什麼要有個_forwarding我們繼續往下看。

4.Block儲存域

通過上面的分析,現在出現了幾個問題需要解釋:

1.為什麼要有個_forwarding?(後面說)

2.BLock作用域在棧上,超出變數作用域是不是就銷燬了?

上面分析的block和__block都是結構體型別的自動變數,在棧上生成,稱為“棧塊”,實際上還存在兩種block,“堆塊”“全域性塊”。全域性塊與全域性變數一樣,設定在程式的.data資料區域,堆塊顧名思義,分配在堆上,型別分別如下:

  • _NSConcreteGlobalBlock
  • _NSConcreteStackBlock
  • _NSConcreteMallocBlock

有兩種情況,是預設會分配在資料區域上的:

  • 1.記述全域性變數的地方有block語法時。
  • 2.block語法的表示式中不使用截獲的自動變數的值。

除此之外的Block語法生成的Block為設定在棧上的_NSConcreteStackBlock類物件。配置在全域性變數上的Block從變數作用域外也可以通過指標安全的使用,但設定在棧上的Block,如果所屬的變數作用域結束,該Block就被廢棄。由於__block也配置在棧上,同樣的__block變數也會被廢棄。Blocks提供了將block和__block變數從棧上覆制到堆上的方法來解決這個問題。這樣即使block語法記述的變數作用域結束,堆上的block還可以繼續存在(原文解釋)。大概意思就是有些情況下,編譯器會預設對棧block生成一個copy到堆上的操作。大多數情況下,編譯器會適當的進行判斷是否會將棧塊拷貝到堆上,有一種情況除外:

  • 向方法或函式的引數中傳遞Block。

就是說block作為引數傳遞的時候是需要我們手動執行copy的,編譯器不會自動執行copy。儘管這樣,還是有兩種情況是不需要我們手動實現,因為他們函式內部已經實現了copy操作:

  • Cocoa框架的方法切方法中含有usingBlock。
  • GCD的API。

舉個例子,把書上面的例子自己手動實現了一下:

- (void)viewDidLoad { 
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib. id object = [self getBlockArray];
typedef void(^blk_t)(void);
blk_t blk = (blk_t)[object objectAtIndex:0];
blk();

}- (id)getBlockArray {
int var = 10;
return [[NSArray alloc] initWithObjects: ^{NSLog(@"blk0:%d",var);

}, ^{NSLog(@"blk1:%d",var);

}, nil];

}複製程式碼

在執行blk()的時候程式異常崩潰了。因為getBlockArray函式執行結束的時候,在棧上建立的block被廢棄了,這個時候編譯器並沒有自動執行copy操作,需要我們手動實現。為什麼編譯器不對所有的棧塊都執行copy到堆上,書上明確說明了:block從棧複製到堆上是相當消耗CPU的,將block設定在棧上也能夠使用時,將block複製到堆上只是在浪費CPU資源。所以這種情況下對block執行copy就可以了:

- (id)getBlockArray { 
int var = 10;
return [[NSArray alloc] initWithObjects: [^{NSLog(@"blk0:%d",var);

} copy], [^{NSLog(@"blk1:%d",var);

} copy], nil];

}複製程式碼
2018-12-24 12:33:33.526163+0800 Blocks-捕獲變數的值[54592:3223484] blk0:10複製程式碼

文中還提到了如果多次呼叫copy會不會有問題,答案當然是沒有問題,在ARC下是不用擔心多次copy引起記憶體問題。

還有一個_forwarding的問題沒有說,至少現在已經知道我們設定在棧上的block因為執行了copy操作到了堆上,所以我們無需關心它會超出作用域而被釋放的問題了,那麼_forwarding繼續往下看。

5.__block變數儲存域

如果在Block中使用了__block變數,那麼該Block從棧複製到堆時,所使用的__block也會被複制到堆上,並且會被Block持有。每一個使用了當前__block變數的Block被複制到堆上時都會對這個__block引用計數+1,如果配置在堆上的Block被廢棄,相應的它所使用的__block引用計數會-1,直到所有的Block被釋放那麼此__block也會隨之釋放。

也就是說,除了上面說到的兩種情況,我們其餘的Block基本都會複製到堆上,也就是說我們使用的__block也會相應的跟著複製到堆上,像OC物件一樣,擁有引用計數。那麼我們再分析一下之前遺留的問題,_forwarding是幹嘛的,當__block被複制到堆上的時候,棧上面的__block結構體裡面的_forwarding成員變數就會指向堆裡面的__block結構體例項,此時堆上面的__block變數的_forwarding會指向自己本身。也就如下圖這個樣子:

Blocks的實現

回顧一下上面__block的實質舉過的例子,我們在用__block修飾自動變數的時候,在func函式裡面修改此變數值的時候,通過(var->
__forwarding->
var) = 1;
這種方式去改變的,var->
__forwarding
實際上訪問的是堆上的__block結構體,var->
__forwarding->
var
就是堆裡面結構體的var成員變數。這樣就算是棧上面的__block被釋放了,我們還可以去訪問堆裡面的var,這也是為什麼自動變數不像static靜態變數那樣通過指標去訪問了,因為自動變數在作用域結束之後就會被釋放了,拷貝到堆上,作用域結束堆上面還會有其相應拷貝,這份拷貝只有在使用了它的Block釋放之後才會釋放。

6.截獲物件

前面分析了__block修飾的自動變數超出作用域也能使用的原理,實際上對於物件型別,Block對其捕獲之後在處理上和__block很像,那麼具體使用__block對變數捕獲之後當Block和__block被拷貝到堆上和他們被釋放這兩個過程具體做了什麼之前也沒有詳細講到,通過捕獲物件的學習,也可以對前面做個總結和思考。直接看下書上面的示例程式碼:

typedef void(^blk_t)(id);
blk_t blk;
{
id array = [[NSMutableArray alloc] init];
blk = [^(id obj){
[array addObject:obj];
NSLog(@"array count : %ld",[array count]);

} copy];

} blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
複製程式碼
2018-12-25 12:25:06.678625+0800 Blocks-捕獲變數的值[56349:3341197] array count : 12018-12-25 12:25:06.679199+0800 Blocks-捕獲變數的值[56349:3341197] array count : 22018-12-25 12:25:06.679210+0800 Blocks-捕獲變數的值[56349:3341197] array count : 3複製程式碼

按理來說array在超出變數作用域的時候會被廢棄,但是根據列印結果來看一切正常。也就是說array在超出變數作用域後依然存在。通過轉換的原始碼如下:

Block結構體部分:

struct __main_block_impl_0 { 
struct __block_impl impl;
struct __main_block_desc_0* Desc;
id array;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, id _array, int flags=0) : array(_array) {
impl.isa = &
_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;

}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself, id obj) {
id array = __cself->
array;
// bound by copy ((void (*)(id, SEL, ObjectType _Nonnull))(void *)objc_msgSend)((id)array, sel_registerName("addObject:"), (id)obj);
NSLog((NSString *)&
__NSConstantStringImpl__var_folders_p6_239crx8x16s8vby9bfclq5d40000gn_T_main_1dc794_mi_0,((NSUInteger (*)(id, SEL))(void *)objc_msgSend)((id)array, sel_registerName("count")));

} static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
_Block_object_assign((void*)&
dst->
array, (void*)src->
array, 3/*BLOCK_FIELD_IS_OBJECT*/);

}static void __main_block_dispose_0(struct __main_block_impl_0*src) {
_Block_object_dispose((void*)src->
array, 3/*BLOCK_FIELD_IS_OBJECT*/);

}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
};
複製程式碼

使用Block部分:

typedef void(*blk_t)(id);
blk_t blk;
{
id array = ((NSMutableArray *(*)(id, SEL))(void *)objc_msgSend)((id)((NSMutableArray *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSMutableArray"), sel_registerName("alloc")), sel_registerName("init"));
blk = (blk_t)((id (*)(id, SEL))(void *)objc_msgSend)((id)((void (*)(id))&
__main_block_impl_0((void *)__main_block_func_0, &
__main_block_desc_0_DATA, array, 570425344)), sel_registerName("copy"));

} ((void (*)(__block_impl *, id))((__block_impl *)blk)->
FuncPtr)((__block_impl *)blk, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init")));
((void (*)(__block_impl *, id))((__block_impl *)blk)->
FuncPtr)((__block_impl *)blk, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init")));
((void (*)(__block_impl *, id))((__block_impl *)blk)->
FuncPtr)((__block_impl *)blk, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init")));
複製程式碼

這一塊我自己測試了一下暫時有點疑問。因為按照書上的示例來看,array前是有__strong修飾的,但是從我轉換的原始碼來看並未看到__strong修飾符。是否是說如果沒有使用weak修飾預設為strong?我先預設這個id array是被__strong修飾的。文中講到,C語言結構體不能附有__strong修飾符的變數,因為編譯器不知道應何時進行C語言結構體的初始化和廢棄操作,不能很好的管理記憶體。但是它能很好的把握Block從棧複製到堆和把Block從堆上廢棄的時機,因此就算Block結構體中含有OC修飾符的變數也一樣能夠跟隨者Block的廢棄而廢棄。

因為Block結構體中含有__strong修飾符的物件,所以需要對它進行管理,和之前的Block原始碼對比,在struct __main_block_desc_0結構體中多了兩個函式指標:

  • void (copy)(struct __main_block_impl_0, struct __main_block_impl_0*);
  • void (dispose)(struct __main_block_impl_0);

這其實在分析__block原理的時候就有了,實際上他們用處是一樣的,都是用來管理Block記憶體用的。

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) { 
_Block_object_assign((void*)&
dst->
array, (void*)src->
array, 3/*BLOCK_FIELD_IS_OBJECT*/);

}複製程式碼

_Block_object_assign函式相當於呼叫了retain函式,將物件賦值在物件型別的結構體成員變數中。

static void __main_block_dispose_0(struct __main_block_impl_0*src) { 
_Block_object_dispose((void*)src->
array, 3/*BLOCK_FIELD_IS_OBJECT*/);

}複製程式碼

_Block_object_dispose函式相當於呼叫了release例項方法的函式,釋放賦值在物件型別的結構體成員變數中的物件。但是從轉換的原始碼來看,__main_block_copy_0__main_block_dispose_0函式指標都沒有被呼叫,那麼它們是在什麼時候觸發的?

  • 棧上的Block複製到堆時 –>
    觸發copy函式。
  • 堆上的Block被廢棄時 –>
    觸發dispose函式。

Block被廢棄上面說了就是沒有物件強引用它就會被回收了,就會呼叫dispose方法。那麼什麼時候棧上的Block會複製到堆上呢?

  • 1.呼叫Block的copy例項方法。
  • 2.Block作為函式返回值返回時。
  • 3.將Block賦值給附有__strong修飾符id型別的類或Block型別成員變數時。
  • 4.在方法名中含有usingBlock的Cocoa框架方法或GCD的API中傳遞Block時。

這樣通過__strong修飾符修飾的自動變數,就能夠在作用域外使用了。

在前面使用__block的時候實際上這兩個函式就已經用到了,略微有點不同之處:

  • 截獲物件時 –>
    BLOCK_FIELD_IS_OBJECT
  • __block變數時 –>
    BLOCK_FIELD_IS_BYREF

通過這兩個引數用來區分是Block捕獲的是物件型別還是__block變數。除此之外他們在copy和dispose時都是一樣的,都是被Block持有和釋放。

7.__block變數和物件

對於Block截獲__block修飾的變數還是直接截獲物件的處理過程,上面都已經分析完了,包括它們關於記憶體的處理也都清晰了,唯獨使用__block修飾id型別的自動變數還沒有說,實際上__block說明符可以指定任何型別的自動變數,當然包括物件型別。還是按照書上面的例子看下程式碼:

__block id obj = [[NSObject alloc] init];
複製程式碼

等同與

__block id __strong obj = [[NSObject alloc] init];
複製程式碼

通過clang轉換如下:

/* __block結構體部分*/struct __Block_byref_obj_0 { 
void *__isa;
__Block_byref_obj_0 *__forwarding;
int __flags;
int __size;
void (*__Block_byref_id_object_copy)(void*, void*);
void (*__Block_byref_id_object_dispose)(void*);
id obj;

};
static void __Block_byref_id_object_copy_131(void *dst, void *src) {
_Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);

}static void __Block_byref_id_object_dispose_131(void *src) {
_Block_object_dispose(*(void * *) ((char*)src + 40), 131);

}複製程式碼
/*__block變數宣告部分*/__attribute__((__blocks__(byref))) __Block_byref_obj_0 obj = { 
(void*)0,(__Block_byref_obj_0 *)&
obj, 33554432, sizeof(__Block_byref_obj_0), __Block_byref_id_object_copy_131, __Block_byref_id_object_dispose_131, ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"))
};
複製程式碼

這裡出現了上一節講到的_Block_object_assign_Block_object_dispose函式。實際上編譯器預設這個obj為__strong型別,當Block從棧複製到堆上時,使用_Block_object_assign函式持有Block截獲的物件,當堆上的Block被廢棄時,使用_Block_object_dispose函式釋放Block截獲的物件。這說明使用__block修飾的__strong型別的物件,當__block變數從棧複製到堆上並且在堆上繼續存在,那麼該物件就會繼續處於被持有狀態。這與Block中使用賦值給附有__strong修飾符的物件型別自動變數的物件相同。

那麼除了__strong,如果用__weak修飾呢?

__weak修飾的物件,就算是使用了__block修飾,一樣還是會被釋放掉,實際上書上的原始碼也是給了我們這樣一個結論,只不過物件會自動置為nil。而使用__unsafe_unretained修飾時,注意野指標問題。

8.Block迴圈引用

避免迴圈引用,根據Block的用途可以選擇使用__block變數,__weak修飾符和__unsafe_unretained修飾符來避免迴圈引用。

__weak和__unsafe_unretained修飾弱引用,不用考慮釋放問題。

__block修飾,屬於強引用,需要在Block內對修飾的物件置nil,為避免迴圈引用必須執行Block,但是__block變數可以控制物件的持有時間。

來源:https://juejin.im/post/5c1b97c2e51d4525e00de6c0

相關文章