一、概述
閉包 = 一個函式「或指向函式的指標」+ 該函式執行的外部的上下文變數「也就是自由變數」;
Block實質是Objective-C對閉包的物件實現,簡單說來,Block就是物件。
二、Block的宣告
1.有引數有返回值
int (^CustomBlock1)(int) = ^int (int a) {
return a + 1;
};
複製程式碼
2.有引數無返回值
void (^CustomBlock)(int) = ^void (int a) {
NSLog(@"-有引數無返回值--引數:%d", a);
};
// 也可以簡寫
void (^CustomBlock1)(int) = ^(int a) {
NSLog(@"-有引數無返回值--引數:%d", a);
};
複製程式碼
3. 無引數有返回值
int (^CustomBlock)(void) = ^int(void) {
return 1;
};
// 也可以簡寫
int (^CustomBlock1)(void) = ^int {
return 1;
};
複製程式碼
4. 無引數無返回值
void (^CustomBlock)(void) = ^void (void) {
NSLog(@"-無引數無返回值--");
};
// 也可以簡寫
void (^CustomBlock1)(void) = ^(void){
NSLog(@"-無引數無返回值--");
};
複製程式碼
5. 利用 typedef 宣告block
// 利用 typedef 宣告block
typedef return_type (^BlockTypeName)(var_type);
// 例子1:作屬性
@property (nonatomic, copy) BlockTypeName blockName;
// 例子2:作方法引數
- (void)requestForSomething:(Model)model handle:(BlockTypeName)handle;
複製程式碼
三、Block捕獲變數及物件
1、變數的定義
- 全域性變數
- 函式外面宣告
- 可以跨檔案訪問
- 可以在宣告時賦上初始值。如果沒有賦初始值,系統自動賦值為0
- 儲存位置:既非堆,也非棧,而是專門的【全域性(靜態)儲存區static】!
- 靜態變數
- 函式外面或內部宣告(即可修飾原全域性變數亦可修飾原區域性變數)
- 僅宣告該變數的檔案可以訪問
- 可以在宣告時賦上初始值。如果沒有賦初始值,系統自動賦值為0
- 儲存位置:既非堆,也非棧,而是專門的【全域性(靜態)儲存區static】!
- 區域性變數(自動變數)
- 函式內部宣告
- 僅當函式執行時存在
- 僅在本檔案本函式內可訪問
- 儲存位置:自動儲存在函式的每次執行的【棧幀】中,並隨著函式結束後自動釋放,另外,函式每次執行則儲存在【棧】中
2、Block捕獲變數
將Objective-C 轉 C++的方法
1、在OC原始檔block.m寫好程式碼。
2、開啟終端,cd到block.m所在資料夾。
3、輸入clang -rewrite-objc block.m,就會在當前資料夾內自動生成對應的block.cpp檔案。
OC程式碼:
int global_val = 10; // 全域性變數
static int static_global_val = 20; // 全域性靜態變數
int main() {
typedef void (^MyBlock)(void);
static int static_val = 30; // 靜態變數
int val = 40; // 區域性變數
int val_unuse = 50; // 未使用的區域性變數
MyBlock block = ^{
// 捕獲區域性變數
NSLog(@"val------------------%d", val);
// 修改區域性變數 -> 程式碼編譯不通過
//val = 4000;
// 全域性變數
global_val *= 10;
// 全域性靜態變數
static_global_val *= 10;
// 靜態變數
static_val *= 10;
};
val *= 10;
block();
NSLog(@"global_val-----------%d", global_val);
NSLog(@"static_global_val----%d", static_global_val);
NSLog(@"static_val-----------%d", static_val);
}
---輸出結果:---
區域性變數: val------------------40
全域性變數: global_val-----------100
全域性靜態變數: static_global_val----200
靜態變數: static_val-----------300
複製程式碼
C++程式碼:
int global_val = 10;
static int static_global_val = 20;
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
int *static_val; // 靜態變數 --> 指標
int val; // 區域性變數 --> 值
// 在建構函式中,也可以看到 static_val、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 *= 10;
static_global_val *= 10;
(*static_val) *= 10;
NSLog((NSString *)&__NSConstantStringImpl__var_folders_8k_cgm28r0d0bz94xnnrr606rf40000gn_T_block_75d081_mi_0, val);
}
// 紀錄了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() {
...
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };
複製程式碼
對於 block 外的變數引用,block預設是將其複製到其資料結構中來實現訪問的。也就是說block的自動變數截獲只針對block內部使用的自動變數,不使用則不截獲,因為截獲的自動變數會儲存於block的結構體內部, 會導致block體積變大(__main_block_desc_0)。特別要注意的是預設情況下block只能訪問不能修改區域性變數的值。
捕獲區域性變數
在Block內部使用了其外部變數,這些變數就會被Block儲存。自動變數val雖然被捕獲進來了,但是是用 __cself->val來訪問的。Block僅僅捕獲了val的值,並沒有捕獲val的記憶體地址。所以,我們在block外部修改了val的值,在block內部並沒有效果。
修改區域性變數
程式碼編譯不通過。預設情況下block只能訪問不能修改區域性變數的值。因為Block僅僅捕獲了val的值,並沒有捕獲val的記憶體地址,block內部修改值並不會對外部的val生效。可能基於此原因,O這種寫法直接編譯錯誤。
修改全域性變數&修改全域性靜態變數
可以直接訪問。全域性變數和全域性靜態變數沒有被截獲到block裡面,它們的訪問是不經過block的(見__main_block_func_0)
修改靜態變數
通過指標訪問。訪問靜態變數(static_val)時,將靜態變數的 指標 傳遞給__main_block_impl_0結構體的建構函式並儲存。修改靜態變數時,是指標操作,所以可以修改其值。
總結: 由上述Block的變數捕獲機制,可以總結出下圖:
變數型別 | 是否捕獲到Block內部 | 傳遞方式 |
---|---|---|
區域性變數 | 是 | 值傳遞 |
區域性staic變數 | 是 | 指標傳遞 |
全域性變數 | 否 | 直接訪問 |
3、Block捕獲物件
OC程式碼:
int main() {
typedef void (^MyBlock)(void);
NSMutableArray *arr = [[NSMutableArray alloc]init];
MyBlock block = ^{
[arr addObject:@1];
};
block();
NSLog(@"arr.count------------%d", (int)arr.count);
}
複製程式碼
C++程式碼:
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
NSMutableArray *arr; // 陣列物件 --> 指標
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSMutableArray *_arr, int flags=0) : arr(_arr) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
NSMutableArray *arr = __cself->arr; // bound by copy
((void (*)(id, SEL, ObjectType _Nonnull))(void *)objc_msgSend)((id)arr, sel_registerName("addObject:"), (id _Nonnull)((NSNumber *(*)(Class, SEL, int))(void *)objc_msgSend)(objc_getClass("NSNumber"), sel_registerName("numberWithInt:"), 1));
}
// 相當於retain操作,將物件賦值在物件型別的結構體成員變數中
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->arr, (void*)src->arr, 3/*BLOCK_FIELD_IS_OBJECT*/);}
// 當於release操作
static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->arr, 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() {
...
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };
複製程式碼
捕獲物件
在block中可以修改物件的值,因為捕獲物件時,在__main_block_impl_0中可以看到捕獲的是指標。
我們可以看到在捕獲物件的原始碼中多了兩個函式 __main_block_copy_0 和 __main_block_dispose_0。
這兩個函式涉及到Block的儲存域及copy操作,在下一節中會說明。
複製程式碼
四、三種不同型別的Block
- 全域性Block(_NSConcreteGlobalBlock):存在於全域性記憶體中, 生命週期從建立到應用程式結束,相當於單例。
- 棧Block(_NSConcreteStackBlock):存在於棧記憶體中, 超出其作用域則馬上被銷燬
- 堆Block(_NSConcreteMallocBlock):存在於堆記憶體中, 是一個帶引用計數的物件, 需要自行管理其記憶體
1、怎麼確定Block的型別?
在上述的原始碼中,可以看到Block的建構函式__main_block_impl_0中的isa指標指向的是&_NSConcreteStackBlock,它表示當前的Block位於棧區中。
Block型別 | 是否捕獲到Block內部 |
---|---|
_NSConcreteGlobalBlock | 沒有用到外界變數或只用到全域性變數、靜態變數 |
_NSConcreteStackBlock | 只用到外部區域性變數、成員屬性變數,且沒有強指標引用 |
_NSConcreteMallocBlock | 有強指標引用或copy修飾的成員屬性引用的block會被複制一份到堆中成為堆Block |
2、全域性Block(_NSConcreteGlobalBlock)
全域性Block的生成條件:
- 定義全域性變數的地方有block語法時
void(^block)(void) = ^ { NSLog(@"Global Block");};
int main() {
}
複製程式碼
- Block不截獲的自動變數時
int(^block)(int count) = ^(int count) {
return count;
};
block(2);
複製程式碼
3、棧Block(_NSConcreteStackBlock)
在生成Block以後,如果這個Block不是全域性Block,那麼它就是為_NSConcreteStackBlock物件,但是如果其所屬的變數作用域名結束,該block就被廢棄。在棧上的__block變數也是如此。
4、堆Block(_NSConcreteMallocBlock)
為了解決棧塊在其變數作用域結束之後被廢棄(釋放)的問題,我們需要把Block複製到堆中,延長其生命週期。開啟ARC時,大多數情況下編譯器會恰當地進行判斷是否有需要將Block從棧複製到堆。
Block的複製操作執行的是copy例項方法。不同型別的Block使用copy方法的效果如下表:
Block型別 | 儲存位置 | 複製效果 |
---|---|---|
_NSConcreteGlobalBlock | 程式的資料區域 | 什麼也不做 |
_NSConcreteStackBlock | 棧區 | 從棧區複製到堆區 |
_NSConcreteMallocBlock | 堆區 | 引用計數加一 |
5、copy和dispose
C結構體裡不能含有被__strong修飾的變數,因為編譯器不知道應該何時初始化和廢棄C結構體。但是OC的執行時庫能夠準確把握Block從棧複製到堆,以及堆上的block被廢棄的時機,在實現上是通過__main_block_copy_0函式和__main_block_dispose_0函式進行的
函式 | 呼叫時機 |
---|---|
copy | 棧上的 Block 複製到堆時 |
dispose | 堆上的 Block 被廢棄(釋放)時 |
那麼什麼時候棧上的Block會被複制到堆上呢?
- 呼叫Block的copy例項方法時
- Block作為函式返回值返回時
- 將Block賦值給附有__strong修飾符id型別的類或Block型別成員變數時
- 將方法名中含有usingBlock的Cocoa框架方法或GCD的API中傳遞Block時
五、Block迴圈引用
如果在Block內部使用__strong修飾符的物件型別的自動變數,那麼當Block從棧複製到堆的時候,該物件就會被Block所持有。
// self 持有 someBlock 物件
self.someBlock = ^(Type var){
// 在Block內部,持有self
[self dosomething];
};
複製程式碼
解決方式:
- 使用 __weak
// weakSelf 對 self進行弱引用
__weak typeof(self) weakSelf = self;
// self 持有 someBlock 物件
self.someBlock = ^(Type var){
// 在Block內部,持有weakSelf
[weakSelf dosomething];
};
複製程式碼
- 使用__block
- (instancetype)init {
self = [super init];
__block id blockSelf = self; // blockSelf 持有 self
//self持有someBlock
someBlock = ^{
NSLog(@"self = %@",blockSelf); //someBlock持有blockSelf
blockSelf = nil;
};
return self;
}
- (void)doSomething() {
someBlock();
}
複製程式碼
此時,blockSelf 持有 self, self 持有someBlock, 而someBlock持有blockSelf。此時,三者形成了一個迴圈。如果doSomething不執行,blockSelf不能置為nil,則無法打破這個迴圈。
一旦執行了doSomething,則迴圈被打破,物件也就可以被釋放。