《Objective-C高級編程:iOS與OS X多線程和內(nèi)存管理》是iOS開發(fā)中一本經(jīng)典書籍,書中有關(guān)ARC、Block、GCD的梳理是iOS開發(fā)進階路上必不可少的知識儲備。筆者讀完此書后為了加強理解,特以筆記記之。本文為中篇,主要談?wù)揙bjective-C中的Block。
鑒于本書翻譯自日文原版且翻譯偏向書面,筆者希望采用通俗的語言記錄,文章結(jié)構(gòu)略有調(diào)整。
本文首發(fā)于Rachal's blog。
Block概要
定義:Block是帶有自動變量(局部變量)的匿名函數(shù)。
匿名函數(shù)就是不帶名稱的函數(shù)。
C語言的函數(shù)中可能使用的變量:
- 自動變量(局部變量)
- 函數(shù)的參數(shù)
- 靜態(tài)變量(靜態(tài)局部變量)
- 靜態(tài)全局變量
- 全局變量
前兩種在超出作用域會銷毀,后三種可以在函數(shù)的多次調(diào)用之間傳遞。
在計算機科學(xué)中,“帶有自動變量值的匿名函數(shù)”這一概念稱為閉包,Block就是Objective-C對閉包的實現(xiàn)。
Block模式
Block語法
Block的完整語法:^
返回值類型
參數(shù)列表
表達式
^ int (int count) {return count + 1;}
完整形式的Block語法與一般C語言函數(shù)定義相比,僅有兩點不同:
- 沒有函數(shù)名
- 帶有
^
其中返回值類型和參數(shù)列表可省略,省略后為:^
表達式
^ {printf("Blocks\n");}
Block變量類型
在Block語法中下,可將Block語法賦值給聲明為Block類型的變量。
int (^blk)(int);// 聲明Block類型
int (^blk1)(int) = ^(int count) {return count + 1};// 變量blk1
int (^blk2)(int);// 變量blk2
blk2 = blk1;// Block類型變量賦值
通過typedef
可聲明Block類型變量,函數(shù)定義就變得更容易理解。
typedef int (^blk)(int);
blk blk1 = ^(int count) {return count + 1;};
blk blk2 = blk1; // blk類型變量賦值
截獲自動變量
- Block可以截獲自動變量的值。
NSInteger aa = 10;
void (^blk)(void) = ^{
NSLog(@"%ld",aa);
};
aa = 2;
blk();
// 輸出結(jié)果為10
- Block中使用時向截獲的自動變量進行賦值會出錯。
id array = [[NSMutableArray alloc] init];
void (^blk) (void) = ^{
id obj = [[NSObject alloc] init];
/* 使用array變量調(diào)方法,沒問題 */
[array addObject:obj];
};
blk();
id array = [[NSMutableArray alloc] init];
void (^blk) (void) = ^{
/* 想array變量賦值,出錯 */
array = [[NSMutableArray alloc] init];
};
blk();
__block說明符
若想在Block語法的表達式中將值賦給在Block語法外聲音的自動變量,需要在該自動變量上附加__block
說明符。
__block int val = 0;
void (^blk)(void) = ^{val = 1;};
blk();
printf("val = %d\n",val);// 輸出結(jié)果為val = 1;
__block id array = [[NSMutableArray alloc] init];
void (^blk) (void) = ^{
/* 向array變量賦值,不會出錯 */
array = [[NSMutableArray alloc] init];
};
blk();
Block實現(xiàn)
Block的實質(zhì)
將Objective-C代碼轉(zhuǎn)化成可讀源碼(C++)的方法:
// 終端cd 源代碼文件夾
clang -rewrite-objc 源代碼文件名
書中將一段Block代碼轉(zhuǎn)化為可讀的C++源碼,簡化源碼,關(guān)注其中的結(jié)構(gòu)體:
struct __main_block_imp_0 {
struct __block_impl impl;
struct __main_block_desc_0* Desc;
}
struct __block_impl {
void *isa;
int Flags;
int Reserved;
void *FuncPtr;
}
Block的結(jié)構(gòu)體中含有isa
指針,足以證明Block即為Objective-C的對象。
isa = &_NSConcreteStackBlock;
isa
被初始化,說明在將Block作為Objective-C的對象處理時,關(guān)于該類的信息放置于_NSCocreteStackBlock
中。
三種Block
前面提到了_NSConcreteStackBlock
類型的Block,與之對應(yīng)的還有_NSConcreteGlobalBlock
、_NSConcreteMallocBlock
。
名稱 | Block的類 | 對象存儲域 | 復(fù)制效果 |
---|---|---|---|
棧Block | _NSConcreteStackBlock | 棧 | 從棧復(fù)制到堆 |
堆Block | _NSConcreteMallocBlock | 堆 | 引用計數(shù)增加 |
全局Block | _NSConcreteGlobalBlock | 程序的數(shù)據(jù)區(qū)(.data區(qū)) | 什么也不做 |
全局Block產(chǎn)生途徑:
- 記述全局變量的地方有Block語法時
- Block語法的表達式中不使用應(yīng)截獲的自動變量時
除此之外的Block語法生成的Block為棧Block,將棧上的Block復(fù)制到堆上,Block就會變成堆Block。
Blobk如何截獲自動變量值
通過clang源碼轉(zhuǎn)換得知:
Block語法表達式中使用的自動變量被作為成員變量追加到Block的結(jié)構(gòu)體中。Block語法表達式中沒有使用的自動變量不會被追加。Block的自動變量截獲只針對Block中使用的自動變量。
所謂“截獲自動變量值”意味著在執(zhí)行Block語法時,Block語法表達式所使用的的自動變量值被保存到Block的結(jié)構(gòu)體實例(即Block自身)中。
__block變量
__block
說明符能用來指定Block中想變更值的自動變量。
準確的表述方式為“__block存儲域類說明符”,C語言中有以下存儲域類說明符:
- typedef
- extern
- static
- auto
- register
它們用于指定將變量值設(shè)置在哪個存儲域中。例如:auto
表示作為自動變量存儲在棧中,static
表示作為靜態(tài)變量存儲在數(shù)據(jù)區(qū)。
自動變量加上__block
,源代碼會急劇增加,變量的轉(zhuǎn)換如下:
__block int val = 10;
__Block_byref_val_0 val = {
0,
&val,
0,
sizeof(__Block_byref_val_0),
10
};
自動變量轉(zhuǎn)換后竟然變成了結(jié)構(gòu)體實例,即棧上生成的__Block_byref_val_0
結(jié)構(gòu)體實例。
結(jié)構(gòu)體聲明如下:
struct __Block_byref_val_0 {
void *__isa;
__Block_byref_val_0 *__forwarding;
int __flags;
int __size;
int val;
};
__Block_byref_val_0
結(jié)構(gòu)體實例的成員變量__forwarding
持有指向該實例自身的指針,也就是原自動變量。
另外,__block
變量的__Block_byref_val_0
結(jié)構(gòu)體并不在Block的結(jié)構(gòu)體中,這樣做是為了在多個Block中使用__block
變量。
Block從棧復(fù)制到堆時,
__block
變量也會一并從棧復(fù)制到堆并被該Block所持有。在多個Block中使用__block
變量時,被復(fù)制的Block持有__block
變量,并增加__block
變量的引用計數(shù)。如果配置在堆上的Block被廢棄,那么它所使用的__block變量也就被釋放。
棧上的__block
變量的結(jié)構(gòu)體實例在復(fù)制到堆上時,會將成員變量__forwarding
的值替換為復(fù)制目標堆上的__block
變量的結(jié)構(gòu)體的地址。
通過該功能,無論是在Block語法中、Block語法外使用__block
變量,還是__block
變量配置在棧上或堆上,都可以順利訪問同一個__block
變量。
Block截獲的對象和__block變量
- (void)blockTest {
blk_t blk;
{
id array = [[NSMutableArray alloc] init];
blk = ^(id obj){
[array addObject:obj];
NSLog(@"array count is %ld", [array count]);
};
}
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
blk([[NSObject alloc] init]);
}
以上blockTest
方法在ARC無效和有效情況下執(zhí)行結(jié)果:
- ARC無效時,程序會強制結(jié)束。因為對象
array
超出作用域后不存在。 - ARC有效時,輸出如下:
array count is 1
array count is 2
array count is 3
ARC有效時,大部分情況下編譯器會適當?shù)倪M行判斷,自動生成將Block從棧上復(fù)制到堆上的代碼。被棧上Block截獲的對象可以超出作用域而存在。
通過編譯器轉(zhuǎn)換后的源碼查看Block結(jié)構(gòu)體,發(fā)現(xiàn)Block結(jié)構(gòu)體中有__strong
修飾的成員變量array
。
struct __main_block_impl_0 {
struct __blobk_impl impl;
struct __main_block_desc_0* Desc;
id __strong array;
}
C語言結(jié)構(gòu)體中不能含有__strong
修飾的變量,因為編譯器不能很好地對其內(nèi)存管理。但是Objective-C運行時庫能夠準確把握從棧復(fù)制到堆上的Block被廢棄的時機,恰當?shù)貙ψ兞窟M行初始化和廢棄。
在此對比截獲對象和使用__block
變量時持有(copy
)和廢棄(dispose
)函數(shù)源碼:
- 截獲對象:
static void __main_block_copy_0(struct __main_block_impl_0 *dst, struct __main_block_impl_0 *src) {
_Block_object_assign(&dst->array, src->array, BLOCK_FIELD_IS_OBJECT);
}
static void __main_block_dispose_0(struct __main_block_impl_0 *src) {
_Block_object_dispose(src->array, BLOCK_FIELD_IS_OBJECT);
}
- 使用
__block
變量:
static void __main_block_copy_0(struct __main_block_impl_0 *dst, struct __main_block_impl_0 *src) {
_Block_object_assign(&dst->val, src->val, BLOCK_FIELD_IS_BYREF);
}
static void __main_block_dispose_0(struct __main_block_impl_0 *src) {
_Block_object_dispose(src->val, BLOCK_FIELD_IS_BYREF);
}
通過對比發(fā)現(xiàn):
- 截獲對象和
__block
變量使用_Block_object_assign
函數(shù) - 廢棄對象和
__block
變量使用_Block_object_dispose
函數(shù) - 通過Block
_FIELD_IS_OBJECT
和BLOCK_FIELD_IS_BYREF
參數(shù)區(qū)分copy
和dispose
函數(shù)的對象類型是對象還是__block
變量。
由此可知:Block中使用的賦值給__strong
修飾(ARC下常省略)的自動變量的對象和復(fù)制到堆上的__block
變量,由于被堆上的Block所持有,因而可超出其變量作用域而存在。
棧上的Block復(fù)制到堆上的時機:
- 調(diào)用Block的
copy
方法時 - Block作為函數(shù)返回值返回時
- 將Block賦值給
__strong
修飾的id類型或Block類型變量時 - 在方法名中含有
usingBlock
的Cocoa框架方法或GCD的API中傳遞Block時
棧上的Block為何需要復(fù)制到堆上
將棧上的Block復(fù)制到堆上,這樣即使Block記述的變量作用域結(jié)束,堆上的Block還可以繼續(xù)存在。
- (void)blockTest {
id obj = [self getBlockArray];
typedef void (^blk_t)(void);
blk_t blk = (blk_t)[obj objectAtIndex:0];
blk();
}
- (id)getBlockArray {
int val = 10;
return [[NSArray alloc] initWithObjects:^{NSLog(@"blk0:%d",val);},^{NSLog(@"blk1:%d",val);},nil];
}
執(zhí)行以上blockTest
方法會出錯,原因在于變量作用域。如下修改后沒問題:
- (id)getBlockArray {
int val = 10;
return [[NSArray alloc] initWithObjects:[^{NSLog(@"blk0:%d",val);} copy],[^{NSLog(@"blk1:%d",val);} copy],nil];
}
注意:將Block從棧上復(fù)制到堆上是相當消耗CPU的。
Block循環(huán)引用
Block為何會引起循環(huán)引用
Block中使用__strong
修飾的對象類型自動變量,當Block從棧復(fù)制到堆時,該對象為Block所持有,這樣容易引起循環(huán)引用。
typedef void(^blk_t) (void);
@interface MyObject : NSObject {
blk_t _blk;
}
@end
@implementation MyObject
- (id)init {
self = [super init];
_blk = ^{
NSLog(@"self = %@", self);//循環(huán)引用警告
};
return self;
}
@end
int main() {
id o = [[MyObject alloc] init];
NSLog(@"%@", o);
return 0;
}
MyObject類對象實例self持有Block,Block語法中使用了self,Block從棧復(fù)制到堆并持有所使用的self。self和Block相互持有,引起循環(huán)引用。
__weak解決循環(huán)引用
為避免循環(huán)引用,可聲明__weak
修飾的變量,并將self賦值使用。
- (id)init {
self = [super init];
id __weak tmp = self;
_blk = ^{
NSLog(@"self = %@", tmp);
};
return self;
}
__weak
打破了Block對self的強引用,解決了循環(huán)引用問題。
另外,Block內(nèi)使用MyObject類的實例變量也會引起循環(huán)引用,因為會造成Block對self的間接持有。同樣使用__weak
可解決循環(huán)引用問題。
typedef void(^blk_t) (void);
@interface MyObject : NSObject {
blk_t _blk;
id _obj;
}
@end
@implementation MyObject
- (id)init {
self = [super init];
id __weak tmp = _obj;
_blk = ^{
NSLog(@"_obj = %@", _obj);
};
return self;
}
@end
__block解決循環(huán)引用
使用__block
變量也可以避免循環(huán)引用。
typedef void(^blk_t) (void);
@interface MyObject : NSObject {
blk_t _blk;
}
- (void)execBlock;
@end
@implementation MyObject
- (id)init {
self = [super init];
__block id tmp = self;
_blk = ^{
NSLog(@"self = %@", tmp);
tmp = nil;
};
return self;
}
- (void)execBlock {
blk();
}
@end
int main() {
id o = [[MyObject alloc] init];
[o execBlock];
return 0;
}
使用__block
變量來避免循環(huán)引用注意以下兩點:
-
__block
變量在使用完時要被賦值nil
- 必須執(zhí)行Block
PS:筆者認為這種方式其實是在模擬__weak
功能。因為__weak
修飾的變量在超出作用域銷毀時會自動被賦值nil
,而執(zhí)行Block剛好就是促使變量被賦值nil
這一操作執(zhí)行。
注意:ARC有效和無效時,__block
說明符的區(qū)別很大。ARC無效時,__block
說明符用來避免Block中的循環(huán)引用,因為有__block
說明符的變量不會被retain
。
以上為Block篇的學(xué)習(xí)內(nèi)容。