主要內容:
1.分析Block捕獲外部變量的過程
2.理解Block修改外部變量的限制
3.分析__block存儲域類說明符的原理
4.理解__block變量的存儲域
5.探究Block對對象的捕獲過程
6.Block的循環引用問題
一、分析Block捕獲外部變量的過程
爲了保證block內部能夠正常訪問外部的變量,Block有一個變量捕獲機制,即Block語法表達式所使用變量可以被保存到Block的結構體實例(Block自身)中。
關於捕獲,Block對不同的外部變量的處理有所不同,根據OC中使用變量的分類,大概包括以下幾種情況:
- 函數參數(這裏研究Block捕獲,所以此處不涉及)
- 自動變量(常簡稱,局部變量)
- 靜態局部變量(常簡稱,靜態變量)
- 靜態全局變量
- 全局變量
那麼,現在對Block捕獲外部變量的四種情況進行測試,相關代碼如下:
#import <Foundation/Foundation.h>
//使用如下的命令,可將OC代碼編譯爲C++代碼
//clang -rewrite-objc main.m
int global_val = 1; //全局變量
static int static_global_val = 1; //靜態全局變量
int main(int argc, char * argv[]) {
int val = 1; //自動變量
static int static_val = 1; //局部靜態變量
void (^myBlock)(void) = ^{
global_val ++;
static_global_val ++;
static_val ++;
//val++//直接修改會報錯(Variable is not assignable (missing __block type specifier)
NSLog(@"\nBlock內:\nglobal_val = %d,\nstatic_global_val = %d,\nval = %d,\nstatic_val= %d",global_val,static_global_val,val,static_val);
};
global_val ++;
static_global_val ++;
val ++;
static_val ++;
NSLog(@"\nBlock外:\nglobal_val = %d,\nstatic_global_val = %d,\nval = %d,\nstatic_val= %d",global_val,static_global_val,val,static_val);
myBlock();
return 0;
}
運行的結果如下:
Block外:
global_val = 2,
static_global_val = 2,
val = 2,
static_val= 2
Block內:
global_val = 3,
static_global_val = 3,
val = 1,
static_val= 3
觀察代碼運行結果,我們會發現四種情況下,只有靜態局部變量、靜態全局變量、全局變量可以在Block裏被修改,而且直接修改自動變量就會報錯;所以此時需要考慮以下兩個問題:
1.爲什麼在Block裏不允許更改自動變量?
2.Block捕獲不同的變量並修改時,有什麼區別嗎?
現在將上述代碼轉化爲C++源碼來具體分析,轉換後的代碼如下:
int global_val = 1;
static int static_global_val = 1;
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int *static_val; //對應靜態局部變量
int val; //對應自動變量
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_val, int _val, int flags=0) : static_val(_static_val), 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 *static_val = __cself->static_val; // bound by copy
int val = __cself->val; // bound by copy
global_val ++;
static_global_val ++;
(*static_val) ++;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_3f_crl5bnj956d806cp7d3ctqhm0000gn_T_main_78fd5a_mi_0,global_val,static_global_val,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(int argc, char * argv[]) {
int val = 1;
static int static_val = 1;
void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, &static_val, val));
global_val ++;
static_global_val ++;
val ++;
static_val ++;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_3f_crl5bnj956d806cp7d3ctqhm0000gn_T_main_78fd5a_mi_1,global_val,static_global_val,val,static_val);
((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
return 0;
}
在代碼分析之前,我們有必要對程序中的內存區域劃分有所瞭解,其大致的分類如下:
內存區域 | 具體說明 |
---|---|
棧區 | 存放局部變量的值,系統自動分配和釋放; 特點:容量小,速度快,有序 |
堆區 | 存放通過malloc系列函數或new操作符分配的內存,如對象; 一般由程序員分配和釋放,如果不釋放,則出現內存泄露; 特點:容量大,速度慢,無序; |
靜態區 | 存放全局變量和靜態變量(包括靜態局部變量和靜態全局變量); 當程序結束時,系統回收; |
常量區 | 存放常量的內存區域; 程序結束時,系統回收; |
代碼區 | 存放二進制代碼的區域 |
瞭解了這些之後,我們再來具體分析代碼和執行結果:
1.全局變量和靜態全局變量
這兩種變量都存儲在靜態區,在任何時候都可以訪問,所以Block無所謂捕獲,而是採用了直接訪問的方式成功的修改了它們的值;這一點從Block對應的構造函數中就可以看出來:
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int *_static_var, int _var, int flags=0) : static_var(_static_var), var(_var);
我們看到,用於創建Block的構造函數使用到了靜態局部變量和自動變量作爲參數,並沒有涉及到全局變量和靜態全局變量。而且我們也在Block的結構體中只發現了對應的靜態變量和自動變量的屬性,這進一步說明Block是直接使用全局變量和靜態全局變量,而非捕獲;
int *static_val; //對應靜態局部變量
int val; //對應自動變量
2.自動變量與靜態局部變量
雖然自動變量與靜態局部變量都被Block捕獲,但是隻有靜態局部變量纔可以被修改成功;通過Block中對應的函數__main_block_func_0
,可以觀察到Block對外部變量的修改過程,相關代碼如下:
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int *static_var = __cself->static_var; // bound by copy
int var = __cself->var; // bound by copy
global_var ++;
static_global_var ++;
(*static_var) ++;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_3f_crl5bnj956d806cp7d3ctqhm0000gn_T_TestBlock_b539f1_mi_0,global_var,static_global_var,var,(*static_var));
}
我們發現,Block爲了訪問到對應的自動變量和靜態局部變量都使用了__cself
,這些操作其實都是針對Block自身屬性的,但不同的是:
外部靜態局部變量,由於是指針傳遞,所以修改的是同一個變量,可以修改成功;
外部自動變量,由於是值傳遞,所以即使修改成功,也無法改變外部自動變量的值;
因此,也許是出於安全的目的,在編譯階段我們就會收到錯誤提示:Block不能修改其捕獲的外部自動變量,即
Variable is not assignable(missing __block type specifier)
這裏還有兩個問題值得我們思考:
1.爲什麼靜態局部變量的存儲域也在靜態區,卻不可以像全局變量一樣直接修改呢?
關鍵原因還是"局部"兩個字,我們看到C++代碼中的函數__main_block_func_0
被設置在了包含Block語法的函數(main
函數,靜態局部變量在此處聲明定義)之外,所以__main_block_func_0
和靜態局部變量和作用域不同,自然不能像全局變量一樣隨時訪問它,所以採用捕獲和指針傳遞的方式來修改靜態變量;
2.爲什麼自動變量不能像靜態變量一樣指針傳遞呢?
其實,這主要還是因爲自動變量和靜態變量的存儲域的不同,自動變量存在棧上被銷燬的時間不定,這很有可能導致Block執行的時候自動變量已經被銷燬,那麼此時訪問被銷燬的地址就會產生野指針錯誤。
二、理解Block修改外部變量的限制
通過以上的代碼示例,我們可以將Block修改外部變量成功的情況分爲兩種:
第一種:Block直接訪問全局性的變量,如全局變量、靜態全局變量;
第二種:Block間接訪問靜態局部變量,捕獲外部變量並使用指針傳遞的方式;
Block中不允許修改外部變量的值的問題,變成了不允許修改自動變量的值的問題;但這也並非最終答案,其實最根本的原因還是Block不允許修改棧中指針的內容;
下面的一段代碼,可以從側面來驗證我們的想法:
#import <Foundation/Foundation.h>
int main(int argc, const char * argv[]) {
NSMutableString *mStr = @"mStr".mutableCopy;
void (^myBlock)(void) = ^{
//mStr = @"newMstr".mutableCopy; //代碼1:直接修改了mStr指針內容;
[mStr appendString:@"-ExtraStr"]; //代碼2:修改mStr指向的堆中內容;
NSLog(@"Block內:mStr:%@",mStr);
};
NSLog(@"Block外:%@",mStr);
myBlock();
return 0;
}
//打印結果:
//Block外:mStr
//Block內:mStr:mStr-ExtraStr
上述代碼是操作一個自動變量的可變字符串,經過測試mStr
不可以直接賦值,卻可以通過appendString
修改字符串,這其中的原因是什麼呢?
首先還是將代碼轉化爲C++源碼,具體如下:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
NSMutableString *mStr;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSMutableString *_mStr, int flags=0) : mStr(_mStr) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself){
NSMutableString *mStr = __cself->mStr; // bound by copy
((void (*)(id, SEL, NSString * _Nonnull))(void *)objc_msgSend)((id)mStr, sel_registerName("appendString:"), (NSString *)&__NSConstantStringImpl__var_folders_3f_crl5bnj956d806cp7d3ctqhm0000gn_T_main_fe0cca_mi_1);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_3f_crl5bnj956d806cp7d3ctqhm0000gn_T_main_fe0cca_mi_2,mStr);
}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->mStr, (void*)src->mStr, 3/*BLOCK_FIELD_IS_OBJECT*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->mStr, 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};
int main(int argc, const char * argv[]) {
NSMutableString *mStr = ((id (*)(id, SEL))(void *)objc_msgSend)((id)&__NSConstantStringImpl__var_folders_3f_crl5bnj956d806cp7d3ctqhm0000gn_T_main_fe0cca_mi_0, sel_registerName("mutableCopy"));
void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, mStr, 570425344));
NSLog((NSString *)&__NSConstantStringImpl__var_folders_3f_crl5bnj956d806cp7d3ctqhm0000gn_T_main_fe0cca_mi_3,mStr);
((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
return 0;
}
作爲對象的字符串會涉及到釋放的問題,所以此處轉換後的源碼與基本類型有所區別(但不影響此處分析,後續會講到),我們發現Block捕獲了mStr
,而且採用了指針傳遞的方式,這與上面的靜態局部變量被捕獲的方式很相似,但是mStr
依然不可以直接賦值新的字符串,其實弄清楚問題的關鍵是理解下面這句代碼究做了什麼?
mStr = @"newMstr".mutableCopy;
這句代碼的含義可以歸納爲:@"mStr".mutableCopy
創建了新的字符串對象,並將新對象的地址返回,最後又賦值給了mStr
;可我們知道mStr
指針是在棧上的,它隨時可能被釋放,直接修改就有可能造成野指針錯誤,這剛好對應了先前自動變量不可修改的問題;
但通過appendString
爲什麼又可以修改字符串呢?這主要因爲mStr
通過指針傳遞被Block捕獲後,Block只是藉助其內部的指針(和mStr
同名,且指向同一個地址),找到了可變字符串的位置,向這塊內存追加新的內容,但是並未改變mStr
的內存地址;
重要總結:Block修改外部變量的限制,其實是指Block不允許修改棧中指針的內容;
三、理解__block存儲域類說明符的原理
通過以上的分析,我們可以將Block理解爲"可以帶有自動變量值的匿名函數",但由於存儲域的關係,Block並不能直接修改捕獲的自動變量。爲了解決這個問題,總結起來有兩種方案:
1.使用存儲域在靜態區的變量(如全局變量、靜態全局變量、靜態局部變量);
2.使用存儲域類說明符__block;
第一種方案我們已經分析過了,現在重點來理解__block存儲域說明符
的用法,其實C語言中的還有許多其他存儲域類說明符,如:
typedef
extern
static
auto
register
__block說明符
就類似於static、auto、register
,它們可以用於指定變量值設置到哪個存儲域中。例如,auto
表示自動變量存儲在棧中(默認),static
表示靜態變量存儲在數據區中。
下面我們來實際使用__block
,使用它來修改被Block捕獲的自動變量,具體的代碼如下:
//__block存儲域修飾符
int main(int argc, const char * argv[]) {
__block int val = 10;
void (^myBlock)(void) = ^{ val = 20;};
val = 30;
myBlock();
NSLog(@"val: %@",val);
return 0;
}
此處代碼在Block中修改自動變量卻沒有像之前那樣報錯,說明__block說明符
是有效的,爲了探究其中原理,現在我們再次把上述代碼轉換C++代碼,具體如下:
struct __Block_byref_val_0 {
void *__isa;
__Block_byref_val_0 *__forwarding;
int __flags;
int __size;
int val;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
__Block_byref_val_0 *val; // by ref
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_val_0 *_val, int flags=0) : val(_val->__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_val_0 *val = __cself->val; // bound by ref
(val->__forwarding->val) = 20;}
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->val, (void*)src->val, 8/*BLOCK_FIELD_IS_BYREF*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->val, 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, const char * argv[]) {
__attribute__((__blocks__(byref))) __Block_byref_val_0 val = {(void*)0,(__Block_byref_val_0 *)&val, 0, sizeof(__Block_byref_val_0), 10};
void (*myBlock)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_val_0 *)&val, 570425344));
(val.__forwarding->val) = 30;
((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
NSLog((NSString *)&__NSConstantStringImpl__var_folders_wd_fhcn9bn91v56nlzv9mt5z8ym0000gn_T_main_a9f88e_mi_0,(val.__forwarding->val));
return 0;
}
分析代碼,我們會發現__block變量
的初始化已經發生了根本的變化,此時的自動變量val
對應的是C++源碼中的__Block_byref_val_0
結構體。該結構體包含了五個成員變量,具體定義如下:
struct __Block_byref_val_0 {
void *__isa; //isa指針
__Block_byref_val_0 *__forwarding; //初始化傳遞的是自身結構體實例的指針
int __flags; //標記flag
int __size; //大小
int val; //對應原自動變量val的值
};
我們看到__block變量val
的初始值爲10,而這個值也出現在了調用__Block_byref_val_0
結構體構造方法的時候,總結__block變量
被捕獲的過程如下:
1.自動變量__block int varl
被封裝爲__Block_byref_val_0
結構體;
2.__Block_byref_val_0
結構體包含一個與__block變量
同名的成員變量val
,對應外部自動變量的值;
3.__Block_byref_val_0
結構體包含一個__forwarding
指針,初始化傳遞的是自己的地址;
4.在Block初始化的過程中,調用__main_block_impl_0
結構體構造函數時,會將__block變量
的__Block_byref_val_0
結構體實例的指針作爲參數;
接下來分析給__block變量
賦值的代碼,轉換後的源碼如下:
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) = 20;
}
在這裏,我們看到函數首先通過cself->val
拿到了對應__block變量
的結構體實例,然後又通過__Block_byref_val_0
結構體實例的成員變量__forwarding
,最終訪問到了結構體成員變量val
;具體過程如下圖所示:
分析當前情況,我就會發現這裏有兩個很關鍵問題:
1.爲什麼要使用多餘的__forwarding
指針來間接訪問變量?
2.當前__block
說明符的作用僅僅體現在:將__block變量
封裝爲__Block_byref_val_0
結構體;這並未從根本上改變自動變量的性質,自動變量究竟是如何被修改的呢?
爲了理解上述問題,我們首先應該對下面的代碼有一個更加清晰的瞭解:
void (^myBlock)(void) = ^{ val = 10;};
代碼中創建後的Block直接賦值給了強指針,這其實滿足了ARC環境下編輯器對Block的優化:編譯器會自動將Block從棧拷貝到堆上,而Block中的用到的__block變量也會被一併拷貝,並且被堆上的Block持有。這樣即使Block語法所在的作用域結束,堆上的Block和__block變量
依然繼續存在,自然也就不存在自動變量創建在棧上被釋放的問題了,藉助圖示理解如下:
另外,當__block
變量結構體實例在從棧上被拷貝到堆上時,會將成員變量的__forwarding
的值替換爲複製目標堆上的__block
變量結構體實例的地址。通過這種功能,無論是在Block語法中、Block語法外使用__block
變量,還是__block
變量配置在棧上或堆上,都可以順利訪問同__block
變量。這就是__forwarding
指針存在的意義。使用圖示理解如下:
重要總結:__block
修飾的自動變量被封裝爲結構體,作爲一個對象隨着Block被拷貝到了堆上,解決了自動變量容易因作用域結束而釋放的問題。而__block
變量結構體中的__forwarding
則保證了無論在棧上還是堆上訪問的都是同一個__block變量
;我們能夠成功修改__block
變量的值,其實是修改了堆上被Block持有的__block
變量的內部成員變量val。
其他問題:
1.ARC存在編譯器的自動優化,自動拷貝Block的情況還包含了很多種,這裏只是其中一種情況,上篇已分析過;
2.上述代碼中,__block
說明符將基本類型的數據封裝爲結構體類型(其中包含了isa指針),這其實就說明__block
變量已經是作爲了一個對象在使用,而對象類型被Block捕獲之後都會涉及一些釋放的問題,所以源碼也出現了許多與對象釋放相關的函數如:__main_block_copy_0
、__main_block_dispose_0
等。這個問題後續會詳細分析;
四、__block變量的存儲域
Block的存儲域通常涉及到拷貝的操作,那麼對於__block變量又是如何處理的呢?使用__block變量的Block從棧上拷貝到堆上時,__block變量也會受到影響。
1.單個Block中使用__block變量
若一個Block中使用__block
變量,則當該Block從棧拷貝到堆上時,使用的所有__block
變量也全部被從棧上拷貝到堆上。使用圖示理解如下:
2.多個Block使用__block變量
多個Block使用__block
變量時,任何一個Block從棧上拷貝到堆上,__block
變量就會一併從棧上拷貝到堆上並被該Block所持有。當剩下的Block從棧拷貝到堆上時,被拷貝的Block持有__block
變量,並增加__block
變量的引用計數。使用圖示理解如下:
3.__block變量的釋放
如果拷貝到堆上的Block被釋放,那麼它使用的__block
變量的引用計數會減一,如果引用計數爲0就會被釋放。使用圖示理解如下:
**重要總結:**無論是對基本類型還是對象使用__block
修飾符,從轉化後的源碼來看,它們都會被轉化爲對應的結構體實例來使用,具有引用類型數據的特性。因此__block
變量隨着Block被拷貝到堆上後,它們的內存管理與普通的OC對象引用計數內存管理模式完全相同。
五、理解Block對對象的捕獲
仔細觀察之前的源碼我們就會發現,Block捕獲對象類型和__block
類型的變量(在底層被封裝爲結構體,也屬於對象)明顯比基本類型要複雜多,其實這裏主要是因爲對象類型還要涉及到釋放的問題。下面的代碼演示了Block對對象的捕獲的過程,具體如下:
typedef void(^AddBlock)(NSString *); //定義一種攜帶字符串參數的Block
int main(int argc, const char * argv[]) {
AddBlock blk = nil;
{
NSMutableArray *mArr = @[].mutableCopy;
blk = ^(NSString *string){
[mArr addObject:string];
NSLog(@"mArr count = %ld",[mArr count]);
};
}//NSMutableArray所在的作用域結束
blk(@"A");
blk(@"B");
blk(@"C");
return 0;
}
//打印結果:
mArr count = 1
mArr count = 2
mArr count = 3
分析代碼:當前爲ARC環境下,編譯器自動對訪問了自動變量的mArr
的blk
進行了拷貝;所以mArr
離開其所在的作用域結束時並沒有被釋放。雖然mArr
指針已經不能使用,但是blk
依然保留有對mArr
的引用可以找到這塊內存。所以代碼也是運行正常的;
現在查看編譯器轉換後的源碼如下:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
NSMutableArray *mArr;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSMutableArray *_mArr, int flags=0) : mArr(_mArr) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->mArr, (void*)src->mArr, 3/*BLOCK_FIELD_IS_OBJECT*/);}
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->mArr, 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捕獲對象類型的變量時,此處的__main_block_desc_0
結構體中多了copy
與dispose
兩個成員變量,而且它們的初始化分別使用了__main_block_copy_0
和__main_block_dispose_0
的函數指針;
這裏主要的原因是,在Objective-C中,C語言結構體不能含有__strong、__weak
修飾符的變量,因爲編譯器不知道應該如何進行C語言結構的初始化和廢棄操作,不能很好地管理內存。但是OC的運行時庫能夠準確把握Block從棧複製到堆以及堆上Block被廢棄的時機,所以這裏纔會增加與內存管理相關的變量和函數。
1.__main_block_copy_0函數
結構體__main_block_desc_0
中的copy
成員變量對應了__main_block_copy_0
函數。
當Block從棧上拷貝到堆上時,__main_block_copy_0
函數會被調用,然後再調用其內部的_Block_object_assign
函數。_Block_object_assign
函數就相當於retain操作,會自動根據__main_block_impl_0
結構體內部的mArr
是什麼類型的指針,對mArr
對象產生強引用或者弱引用。如果mArr
指針是__strong
類型,則爲強引用,引用計數+1,如果mArr
指針是__weak
類型,則爲弱引用,引用計數不變。
2.__main_block_dispose_0函數
結構體__main_block_desc_0
中的dispose
成員變量對應了__main_block_dispose_0
函數。
當Block被廢棄時,__main_block_dispose_0
函數會被調用,__main_block_dispose_0
函數就相當於release操作,將mArr
對象的引用計數減1,如果此時引用計數爲0,那麼遵循引用計數的規則mArr
也就被釋放了。
3.Block捕獲對象與__block變量的區別
其實Block捕獲對象與__block
變量後,對於它們的內存管理的方式相同,也都是使用copy函數持有和disposde函數釋放;兩者體現在源碼上的不同,我們可以觀察下面的函數:
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->mArr, (void*)src->mArr, 3/*BLOCK_FIELD_IS_OBJECT*/);}
_Block_object_assign
函數中的最後一個參數用於區分Block捕獲的是對象還是__block
變量。
對象變量 | __block變量 |
---|---|
BLOCK_FIELD_IS_OBJECT | BLOCK_FIELD_IS_BYREF |
六、Block的循環引用問題
Block在從棧拷貝到堆上時,如果其中捕獲了強類型的對象,該對象就會被Block所持有。這樣很容易就會引起循環引用,我們來看下面的代碼:
typedef void(^MyBlock)(void);
@interface MyObject : NSObject
@property(nonatomic,copy) MyBlock block;
@end
@implementation MyObject
- (instancetype)init {
self = [super init];
return self;
}
- (void)dealloc {
NSLog(@"MyObject dealloc!");
}
@end
int main(int argc, char * argv[]) {
@autoreleasepool {
MyObject *myObject = [[MyObject alloc] init];
myObject.block = ^{
//Capturing 'myObject' strongly in this block is likely to lead to a retain cycle
NSLog(@"捕獲對象:%@", myObject );
};
}
NSLog(@"myObject的作用域結束了");
return 0;
}
不僅編譯器給出了內存泄漏的警告,而且測試結果也證實了MyObject的dealloc
實例方法並沒有執行,這裏發生了循環引用。原因就在與myObject
的block
在被自動拷貝到堆上的過程中持有了myObject
,而myObject
本身就持有了block
,所以兩者相互持有就產生了問題。
現在就來總結類似情況下的Block循環引用的處理方法,可分爲ARC和MRC兩種情況:
1.解決ARC環境下的循環引用問題
方法1:使用弱引用修飾符__weak、和__unsafe_unretained修飾符;
使用__weak解決上述問題,需要改進的代碼如下:
int main(int argc, char * argv[]) {
@autoreleasepool {
MyObject *myObject = [[MyObject alloc] init];
__weak typeof(myObject) weakObject = myObject;
myObject.block = ^{
NSLog(@"捕獲對象:%@", weakObject );
};
}
NSLog(@"myObject的作用域結束了");
return 0;
}
上述代碼使用弱引用修飾符__weak ,在block內部對 myObject
設置爲弱引用,弱引用不會導致Block捕獲對象的引用計數增加(這在上述分析中已經講過)。
注意__weak
和__unsafe_unretained
的區別:
__weak:iOS4之後才提供使用,而且比__unsafe_unretained
更加安全,因爲當它指向的對象銷燬時,會自動將指針置爲nil;推薦使用。
__unsafe_unretained:在__weak
出現以前常用修飾符,其指向的對象銷燬時,指針存儲的地址值不變,所以沒有__weak
安全。
方法2:使用__block說明符
回憶__block
修飾基本類型的C++源碼,我們可以知道__block
修飾對象時其實也會封裝一個結構體類型,而這個結構體中會持有自動變量對象,這樣就會造成下圖的情況:
使用__block
解決上述問題,需要改進的代碼如下:
int main(int argc, char * argv[]) {
@autoreleasepool {
MyObject *myObject = [[MyObject alloc] init];
__block MyObject *tempObject = myObject;
myObject.block = ^{
NSLog(@"捕獲對象:%@", tempObject );
tempObject = nil; //關鍵代碼1
};
myObject.block(); //關鍵代碼2:執行持有的block;
}
NSLog(@"myObject的作用域結束了");
return 0;
}
上述代碼有兩句關鍵,已經通過註釋標註;在block中通過tempObject = nil
這句代碼,__block
變量tempObject
對於MyObject類對象的強引用失效了,而這句代碼生效的前提又是block被調用了(關鍵代碼2);這種方式避免了循環引用的產生的過程如下圖:
**特別注意:**如果關鍵代碼2沒有被調用,同樣會造成循環引用。
使用__block變量相比弱引用修飾符的優缺點:
優點:
1.通過執行block的方式,可動態決定__block
變量可以控制對象的持有時間;
2.在不能使用__weak
修飾符的環境下,避免使用__unsafe_unretained
(因爲要考慮野指針問題);
缺點:
爲了避免循環引用,必須執行Block;
2.解決MRC環境下的循環引用問題
方法1:使用弱引用修飾符__unsafe_unretained修飾符;
在MRC環境下不支持使用__weak
,所以只能使用__unsafe_unretained
;使用原理同ARC環境下相同,這裏不再贅述。
方法2:使用__block說明符
MRC環境下,__block
說明符被用來避免循環引用。這是因爲當Block從棧拷貝到堆時,若Block使用的變量是附有__block
說明符的id類型或者對象類型的自動變量,不會被retain,否則就會被retain。這一點和ARC環境是不同的。現在我們在MRC環境下改進代碼,具體如下:
int main(int argc, char * argv[]) {
MyObject *myObject = [[MyObject alloc] init];
__unsafe_unretained MyObject *tempObject = myObject;
myObject.block = ^{
NSLog(@"捕獲對象:%@", tempObject );
};
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
[myObject autorelease];
[pool drain]; //等同於[myObject release];
return 0;
}
//打印結果:
//MyObject dealloc!
上述操作將代碼改爲了MRC下的自動釋放池,相比之前在ARC中使用__block
,這裏沒有在Block內部置nil的操作,也沒有調用block,但同樣解決了循環引用的問題;
重要總結:__block
說明符在ARC與MRC環境下的用途有很大區別,因此在編寫代碼時我們必須區分好這兩種環境。