主要內容:
1.理解Block的本質
2.理解Block的存儲域分類
3.理解Block的Copy原理
一、探究Block的本質
從一個最簡單的Block使用示例說起,我們分析如下代碼:
//main.m文件:
#import <Foundation/Foundation.h>
int main(int argc, char * argv[]) {
int num = 10;
void (^myBlock)(void) =^{NSLog(@"num = %d",num);};
myBlock();
return 0;
}
Objective-C語言是基於C、C++的,爲了深入理解Block的底層結構,我們可以通過如下的編譯器命令將上述代碼轉換成C++源碼:
clang -rewrite-objc 源代碼文件名(如此例中的main.m)
轉化後的C++源碼如下:
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int num;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _num, int flags=0) : num(_num) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
int num = __cself->num; // bound by copy
NSLog((NSString *)&__NSConstantStringImpl__var_folders_wd_fhcn9bn91v56nlzv9mt5z8ym0000gn_T_main_9e3646_mi_0,num);}
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 num = 10;
void (*myBlock)(void) =((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, num));
((void (*)(__block_impl *))((__block_impl *)myBlock)->FuncPtr)((__block_impl *)myBlock);
return 0;
}
對比OC代碼與C++源碼中的main
函數,我們發現創建Block其實是調用了__main_block_impl_0
結構體的構造函數;而Block中待執行代碼也都被封裝到了__main_block_func_0
函數中。
另外值得注意的是,這些C++的結構體和函數的命名,是根據Block語法所屬的函數名(此處爲main
)和Block語法在該函數出現的順序值(此處爲0)來設定的;
根據這些對應關係,我們對C++源碼中的內容一一分析:
1.__main_block_imp_0結構體
__main_block_impl_0
結構體對應了Block的定義,結構體內部包含了三個成員變量impl
、Desc
、num
,num其實就是被捕獲的變量(後續再講),另外還有一個同名的構造函數__main_block_impl_0
;具體代碼如下:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int num;
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _num, int flags=0) : num(_num) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
Block通過調用這裏的構造函數得以創建,調用時需傳入了四個參數:(void *fp, struct __main_block_desc_0 *desc, int _num, int flags=0)
,前三個參數對應成員變量的初始化,而最後一個參數flags攜帶默認值可暫不考慮。
2.__block_impl結構體
__main_block_imp_0
結構體的第一個成員變量impl,就是__block_impl
結構體類型;尤其注意該結構體中包含有isa
指針,從這一點就可以說明Block本質上還是一個OC對象,因爲OC中只有對象纔會具有isa
指針的概念。而FuncPtr
是一個函數指針,在__main_block_imp_0
構造函數調用時被賦值;
3.__main_block_desc_0結構體
__main_block_imp_0
結構體構造函數中傳入參數desc,其實就是__main_block_desc_0
對象。該結構體包含兩個成員變量:
reserved:系統保留值
Block_size:代表Block的大小
4.__main_block_func_0函數
__main_block_imp_0
結構體構造函數中傳入函數指針fp,其實就是__main_block_func_0
函數的地址。該函數將Block中所有的代碼封裝爲函數,以待被調用;
重要總結:
1.Block對應底層__main_block_impl_0
結構體,其中包含有isa
指針,這說明Block本質上還是一個OC對象;
2.Block中待執行的代碼,在底層也被封裝爲__main_block_func_0
函數,以實現調用;說明Block還攜帶了函數執行的環境
Block的特點:
1.Block相當於其他語言中的閉包或者匿名函數;
2.Block與函數區別在於,Block相當於函數加上函數執行的上下文環境(捕獲外部變量下面會講到);
二、Block的存儲域
1.Block的存儲域分類
在之前Block結構體構造函數中,我們很容易能找到這樣一句代碼:
impl.isa = &_NSConcreteStackBlock;
我們已經知道Block也是一個Objective-C對象,每個OC對象都有一個isa指針指向其類對象,這裏的情況也是類似的;Block的isa指針指向了_NSConcreteStackBlock
類對象,即此時的Block是以_NSConcreteStackBlock
類爲模板創建的實例;
除此之外,其實還有兩個與之類似的類_NSConcreteGlobalBlock
和_NSConcreteMallocBlock
,不同的Block類創建的對象用於不同的存儲域,也對應了對應不同的OC類型,具體整理如下:
clang類 | OC類 | 內存區域 |
---|---|---|
_NSConcreteGlobalBlock | NSGlobalBlock | 靜態區 |
_NSConcreteStackBlock | NSStackBlock | 棧區 |
_NSConcreteMallocBlock | NSMallocBlock | 堆區 |
下面通過打印的方式驗證Block對象本質,具體代碼如下:
- (void)testBlock5 {
void(^block)(int a) = ^(int a) {
NSLog(@"This is a block");
};
NSLog(@"%@",[block class]);
NSLog(@"%@",[[block class] superclass]);
NSLog(@"%@",[[[block class] superclass] superclass]);
NSLog(@"%@",[[[[block class] superclass] superclass] superclass]);
}
//打印結果:
//__NSGlobalBlock__
//__NSGlobalBlock
//NSBlock
//NSObject
觀察打印結果,我們看到Block最終繼承於NSObject類型,這又一次驗證了Block本質就是OC對象的結論;而打印結果中出現的__NSGlobalBlock__
說明此處的Block的存儲域爲靜態區;
2.區分Block不同存儲域類型的方法
Block的不同存儲域對其的使用影響巨大,而正確區分Block類型的關鍵在於:Block中是否引用了自動變量(需要MRC下測試),總結起來如下:
Block類型 | 環境 | 內存區域 |
---|---|---|
_NSConcreteGlobalBlock( NSGlobalBlock) | 沒有訪問自動變量; 或者只用到靜態區變量 |
靜態區 |
_NSConcreteStackBlock( NSStackBlock) | 訪問了自動變量 | 棧區 |
_NSConcreteMallocBlock( NSMallocBlock) | __NSStackBlock__調用了copy | 堆區 |
我們可以使用代碼對上述情況進行驗證,但需要首先切換ARC到MRC環境下,因爲在ARC環境下的編譯器爲我們做了很多優化的工作,比如自動將棧區的Block拷貝到堆區,這樣我們也就不容易捕獲到Block初始狀態的位置了。所以需要暫時將開發環境切換至MRC下來測試。相關的測試代碼如下:
- (void)testBlock7 {
//1.Block內部沒有調用外部自動變量
void (^block1)(void) = ^{
NSLog(@"Block");
};
//2.Block內部調用外部自動變量
int a = 10;
void (^block2)(void) = ^{
NSLog(@"Block-%d",a);
};
//3.拷貝棧上的block
void (^block3)(void) = ^{
NSLog(@"Block-%d",a);
};
//打印Block類型
NSLog(@"%@ %@ %@", [block1 class], [block2 class], [[block3 copy] class]);
}
//打印結果:
//__NSGlobalBlock__ __NSStackBlock__ __NSMallocBlock__
分析代碼:
**NSGlobalBlock:**Block中沒有引用自動變量或者只用到靜態區變量,這種Block與全局變量一樣設置在程序的靜態區,直到程序結束纔會被回收;此類型的Block不依賴執行時的狀態,所以整個程序只需一個實例,用的也較少;
**NSStackBlock:**Block中訪問自動變量,並且存放在棧中,棧中的內存由系統自動分配和釋放,作用域執行完畢之後就會被立即釋放;所以我們有可能遇到Block內存銷燬之後才使用它的情況,開發中遇到的很多問題也都是因此而起;
NSMallocBlock:_NSStackBlock__
執行copy操作會生成__NSMallocBlock__
;棧Block被拷貝後存放在堆中後,需要我們自己進行內存管理,否則還可能造成一些循環引用的問題;
三、Block的Copy的問題
Block有着不同的存儲域類型,尤其是配置在棧上的Block(即__NSStackBlock__
類型的Block),如果其所屬的作用域結束該Block就會被釋放。此時若繼續使用Block,就需要執行copy操作,將其由棧區拷貝到堆區得到__NSMallocBlock__
,而__NSMallocBlock__
也會在其引用計數爲0的時候被釋放;
關於Block的拷貝,其實還需要分爲MRC和ARC兩種環境來考慮,下面是具體的分析:
1.MRC下的Block拷貝
在MRC環境下,我們只能顯式的通過copy來實現Block的拷貝;通常爲了避免Block的釋放,我們定義Block屬性的時候必須使用copy修飾符
也正是基於這個原因。下面是在MRC環境下測試棧Block的使用,具體代碼如下:
typedef void(^PrintBlock)(void);
@interface ViewController ()
@property (nonatomic ,copy)PrintBlock block1;
@property (nonatomic ,copy)PrintBlock block2;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self createBlock];
self.block1();
self.block2();
NSLog(@"block1:%@", [self.block1 class]); //報錯Thread 1: EXC_BAD_ACCESS (code=1, address=0x7ffeeb90b8c0)
NSLog(@"block2:%@", [self.block2 class]);
}
- (void)createBlock {
int a = 10;
//此處採用直接賦值的方式,不會觸發setter方法
_block1 = ^{
NSLog(@"This is block1-%d",a);
};
self.block2 = ^{
NSLog(@"This is block2-%d",a);
};
//離開此作用域,block1就會被釋放
NSLog(@"block1:%@、block2:%@", [self.block1 class],[self.block2 class]);
}
@end
打印結果及分析如下:
block1:__NSStackBlock__、block2:__NSMallocBlock__
This is block1-10
This is block2-10
由於block1
採用的是直接賦值的方式,沒有調用setter
方法,所以block1
並沒有被拷貝到堆上,是一個棧上的Block,這樣也就直接導致了第二次打印block1
時所發生的野指針崩潰;
2.ARC下的Block拷貝
在ARC環境下,編譯器會根據情況自動將棧上的Block複製到堆上,總結起來包含以下幾種情況:
- Block作爲函數返回值時;這就類似與MRC中對返回值Block執行了
[[returnedBlock copy] autorelease]
; - Block被強引用,如Block被賦值給
__strong
或者id
類型; - Block作爲
GCD API
的方法參數時; - Block作爲系統方法名含有
usingBlock
的方法參數時;
下面的代碼演示了這些情況:
typedef void(^Block)(void);
-(Block)getBlock{
//ARC下的Block中訪問了auto變量,此時block類型應爲__NSStackBlock__
int a = 10;
return ^{
NSLog(@"---------%d", a);
};
}
- (void)testBlock9 {
//1.測試block作爲函數返回值時
NSLog(@"bock1-:%@",[[self getBlock] class]);
//2.測試將block賦值給__strong指針時
int a = 10;
//2.1.block內沒有訪問auto變量
Block block21 = ^{
NSLog(@"block21");
};
NSLog(@"block21-%@",[block21 class]);
//2.2.block內訪問了auto變量,但沒有賦值給__strong指針
NSLog(@"block22-%@",[^{
NSLog(@"block22-%d", a);
} class]);
//2.3.block賦值給__strong指針
Block block23 = ^{
NSLog(@"block23");
};
NSLog(@"block23-%@",[block23 class]);
//3.block作爲Cocoa API中方法名含有usingBlock的方法參數時
NSArray *array = @[@"1",@"2",@"3"];
[array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
}];
//4.block作爲GCD API的方法參數時
//Block中的延時操作完成時,系統將會對Block進行釋放
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
});
}
//打印結果如下:
//bock1-:__NSMallocBlock__
//block21-__NSGlobalBlock__
//block22-__NSStackBlock__
//block23-__NSGlobalBlock__
3.其他存儲域Block的拷貝
上面講述的重點都於對棧Blok的拷貝,若是對於已經配置在堆上或者配置在靜態區的上的Block調用copy方法又將如何呢?下面是不同存儲域的Block執行copy進行的總結:
Block類型 | 副本源的配置存儲域 | 複製效果 |
---|---|---|
_NSConcreteStackBlock | 棧區 | 從棧複製到堆 |
_NSConcreteGlobalBlock | 靜態區 | 什麼也不做 |
_NSConcreteMallocBlock | 堆區 | 引用增加 |
4. 總結Block需要拷貝的原理
Block默認創建於其所在函數的函數棧上,所以當函數作用域結束時就會隨之銷燬;
在MRC環境下,沒有編譯器的優化,所以我們非常強調要使用copy
將Block拷貝到堆上,從而避免Block在其作用域結束時被直接釋放;
在ARC環境下,編譯器會根據情況自動將棧上的Block複製到堆上,對於Block使用copy
還是strong
效果是一樣的,所以寫不寫copy
都行。在ARC環境下對於Block依然使用copy
,更像是從MRC遺留下來的“傳統”,時刻提醒我們:編譯器自動對Block進行了拷貝操作。如果不寫copy
,該類的調用者有可能會忘記或者根本不知道“編譯器會自動對Block進行了拷貝操作”,他們有可能會在調用之前自行拷貝屬性值,這種操作多餘而低效。
最後,總結Block修飾符的使用:
//MRC下block屬性的建議寫法:
@property (copy, nonatomic) void (^block)(void);
//ARC下block屬性的建議寫法:
@property (strong, nonatomic) void (^block)(void);
@property (copy, nonatomic) void (^block)(void);