1. 前言 Block:帶有自動變數(局部變數)的匿名函數。它是C語言的擴充功能。之所以是拓展,是因為C語言不允許存在這樣匿名函數。 1.1 匿名函數 匿名函數是指不帶函數名稱函數。C語言中,函數是怎樣的呢?類似這樣: int func(int count); 調用的時候: int result = ...
1. 前言
Block:帶有自動變數(局部變數)的匿名函數。它是C語言的擴充功能。之所以是拓展,是因為C語言不允許存在這樣匿名函數。
1.1 匿名函數
匿名函數是指不帶函數名稱函數。C語言中,函數是怎樣的呢?類似這樣:
int func(int count);
調用的時候:
int result = func(10);
func就是它的函數名。也可以通過指針調用函數,看起來沒用到函數名:
int result = (*funcptr)(10);
實際,在賦值給函數指針時,必須通過函數的名稱才能獲得該函數的地址。完整的步驟應該是:
int (*funcptr)(int) = &func; int result = (*funcptr)(10);
而通過Block,就能夠使用匿名函數,即不帶函數名稱的函數。
1.2 帶有自動變數
關於“帶有自動變數(局部變數)”的含義,這是因為Block擁有捕獲外部變數的功能。在Block中訪問一個外部的局部變數,Block會持用它的臨時狀態,自動捕獲變數值,外部局部變數的變化不會影響它的的狀態。
捕獲外部變數,看一個經典block面試題:
int val = 10; void (^blk)(void) = ^{ printf("val=%d
",val);
};
val = 2;
blk();
上面這段代碼,輸出值是:val = 10,而不是2。
block 在實現時就會對它引用到的它所在方法中定義的棧變數進行一次只讀拷貝,然後在 block 塊內使用該只讀拷貝;換句話說block截獲自動變數的瞬時值;或者block捕獲的是自動變數的副本。
由於block捕獲了自動變數的瞬時值,所以在執行block語法後,即使改寫block中使用的自動變數的值也不會影響block執行時自動變數的值。
所以,上面的面試題的結果是2不是10。
解決block不能修改自動變數的值,這一問題的另外一個辦法是使用__block修飾符。
__block int val = 10; void (^blk)(void) = ^{printf("val=%d
",val);};
val = 2;
blk();
上面的代碼,跟第一個代碼段相比只是多了一個__block修飾符。但是輸出結果確是2。
2. Block語法大全
約定:用法中的符號含義列舉如下:
-
return_type表示返回的對象/關鍵字等(可以是void,並省略)
-
blockName表示block的名稱
-
var_type表示參數的類型(可以是void,並省略)
-
varName表示參數名稱
2.1 Block聲明及定義語法,及其變形
(1) 標準聲明與定義
return_type (^blockName)(var_type) = ^return_type (var_type varName) { // ... };
blockName(var);
(2) 當返回類型為void
void (^blockName)(var_type) = ^void (var_type varName) { // ... };
blockName(var);
可省略寫成
void (^blockName)(var_type) = ^(var_type varName) { // ... };
blockName(var);
(3) 當參數類型為void
return_type (^blockName)(void) = ^return_type (void) { // ... };
blockName();
可省略寫成
return_type (^blockName)(void) = ^return_type { // ... };
blockName();
(4) 當返回類型和參數類型都為void
void (^blockName)(void) = ^void (void) { // ... };
blockName();
可省略寫成
void (^blockName)(void) = ^{ // ... };
blockName();
(5) 匿名Block
Block實現時,等號右邊就是一個匿名Block,它沒有blockName,稱之為匿名Block:
^return_type (var_type varName)
{ //... };
2.2 typedef簡化Block的聲明
利用typedef簡化Block的聲明:
- 聲明
typedef return_type (^BlockTypeName)(var_type);
- 例子1:作屬性
//聲明 typedef void(^ClickBlock)(NSInteger index); //block屬性 @property (nonatomic, copy) ClickBlock imageClickBlock;
- 例子2:作方法參數
//聲明 typedef void (^handleBlock)(); //block作參數 - (void)requestForRefuseOrAccept:(MessageBtnType)msgBtnType messageModel:(MessageModel *)msgModel handle:(handleBlock)handle{
...
2.3 Block的常見用法
2.3.1 局部位置聲明一個Block型的變數
- 位置
return_type (^blockName)(var_type) = ^return_type (var_type varName) { // ... };
blockName(var);
- 例子
void (^globalBlockInMemory)(int number) = ^(int number){ printf("%d
",number);
};
globalBlockInMemory(90);
2.3.2 @interface位置聲明一個Block型的屬性
- 位置
@property(nonatomic, copy)return_type (^blockName) (var_type);
- 例子
//按鈕點擊Block @property (nonatomic, copy) void (^btnClickedBlock)(UIButton *sender);
2.3.3 在定義方法時,聲明Block型的形參
- 用法
- (void)yourMethod:(return_type (^)(var_type))blockName;
- 例子
UIView+AddClickedEvent.h
- (void)addClickedBlock:(void(^)(id obj))clickedAction;
2.3.4 在調用如上方法時,Block作實參
- 例子
UIView+AddClickedEvent.m
- (void)addClickedBlock:(void(^)(id obj))clickedAction{ self.clickedAction = clickedAction; // :先判斷當前是否有交互事件,如果沒有的話。。。所有gesture的交互事件都會被添加進gestureRecognizers中 if (![self gestureRecognizers]) { self.userInteractionEnabled = YES; // :添加單擊事件 UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tap)];
[self addGestureRecognizer:tap];
}
}
- (void)tap{ if (self.clickedAction) { self.clickedAction(self);
}
}
2.4 Block的少見用法
2.4.1 Block的內聯用法
這種形式並不常用,匿名Block聲明後立即被調用:
^return_type (var_type varName)
{ //... }(var);
2.4.2 Block的遞歸調用
Block內部調用自身,遞歸調用是很多演算法基礎,特別是在無法提前預知迴圈終止條件的情況下。註意:由於Block內部引用了自身,這裡必須使用__block避免迴圈引用問題。
__block return_type (^blockName)(var_type) = [^return_type (var_type varName)
{ if (returnCondition)
{
blockName = nil; return;
} // ... // 【遞歸調用】 blockName(varName);
} copy];
【初次調用】
blockName(varValue);
2.4.3 Block作為返回值
方法的返回值是一個Block,可用於一些“工廠模式”的方法中:
- 用法:
- (return_type(^)(var_type))methodName
{ return ^return_type(var_type param) { // ... };
}
- 例子:Masonry框架裡面的
- (MASConstraint * (^)(id))equalTo { return ^id(id attribute) { return self.equalToWithRelation(attribute, NSLayoutRelationEqual);
};
}
- (MASConstraint * (^)(id, NSLayoutRelation))equalToWithRelation { return ^id(id attribute, NSLayoutRelation relation) { if ([attribute isKindOfClass:NSArray.class]) { NSAssert(!self.hasLayoutRelation, @"Redefinition of constraint relation"); NSMutableArray *children = NSMutableArray.new; for (id attr in attribute) {
MASViewConstraint *viewConstraint = [self copy];
viewConstraint.secondViewAttribute = attr;
[children addObject:viewConstraint];
}
MASCompositeConstraint *compositeConstraint = [[MASCompositeConstraint alloc] initWithChildren:children];
compositeConstraint.delegate = self.delegate;
[self.delegate constraint:self shouldBeReplacedWithConstraint:compositeConstraint]; return compositeConstraint;
} else { NSAssert(!self.hasLayoutRelation || self.layoutRelation == relation && [attribute isKindOfClass:NSValue.class], @"Redefinition of constraint relation"); self.layoutRelation = relation; self.secondViewAttribute = attribute; return self;
}
};
}
3. Block應用場景
3.1 響應事件
情景:UIViewContoller有個UITableView並是它的代理,通過UITableView載入CellView。現在需要監聽CellView中的某個按鈕(可以通過tag值區分),並作出響應。
如上面 2.3.2節在CellView.h中@interface位置聲明一個Block型的屬性,為了設置激活事件調用Block,接著我們在CellView.m中作如下設置:
// 激活事件 #pragma mark - 按鈕點擊事件 - (IBAction)btnClickedAction:(UIButton *)sender { if (self.btnClickedBlock) { self.btnClickedBlock(sender);
}
}
隨後,在ViewController.m的適當位置(- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{...代理方法)中通過setter方法設置CellView的Block屬性。Block寫著當按鈕被點擊後要執行的邏輯。
// 響應事件 cell.btnClickedBlock = ^(UIButton *sender) { //標記消息已讀 [weakSelf requestToReadedMessageWithTag:sender.tag]; //刷新當前cell [tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
};
其實,即使Block不傳遞任何參數,也可以傳遞事件的。但這種情況,無法區分事件的激活方(cell裡面的哪一個按鈕?)。即:
//按鈕點擊Block @property (nonatomic, copy) void (^btnClickedBlock)(void);
// 激活事件 #pragma mark - 按鈕點擊事件 - (IBAction)btnClickedAction:(UIButton *)sender { if (self.btnClickedBlock) { self.btnClickedBlock();
}
}
// 響應事件 cell.btnClickedBlock = ^{ //標記消息已讀 [weakSelf requestToReadedMessageWithTag:nil]; //刷新當前cell [tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
};
3.2 傳遞數據
上面的響應事件,其實也是傳遞數據,只是它傳遞的對象是UIButton。如下所示,SubTableView是VC的一個屬性和子視圖。
- 傳遞數值
SubTableView.h
@property (strong, nonatomic) void (^handleDidSelectedItem)(int indexPath);
SubTableView.m
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
[tableView deselectRowAtIndexPath:indexPath animated:YES];
_handleDidSelectedItem ? _handleDidSelectedItem(indexPath) : NULL;
}
VC.m
[_subView setHandleDidSelectedItem:^(int indexPath) {
[weakself handleLabelDidSearchTableSelectedItem:indexPath];
}];
- (void)handleLabelDidSearchTableSelectedItem:(int )indexPath { if (indexPath==0) {
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:[NSString stringWithFormat:@"telprompt:%@", self.searchNullView.telLabel.text]]];
}else if (indexPath==1){
[self.navigationController popViewControllerAnimated:YES];
}
}
- 傳遞對象
例如HYBNetworking網路框架中請求成功時傳遞介面返回數據對象的Block:
[HYBNetworking postWithUrl:kSearchProblem refreshCache:NO params:params success:^(id response) { typeof(weakSelf) strongSelf = weakSelf; // [KVNProgress dismiss]; NSString *stringData = [response mj_JSONString];
stringData = [DES3Util decrypt:stringData]; NSLog(@"stirngData: %@", stringData);
...
}
3.3 鏈式語法
鏈式編程思想:核心思想為將block作為方法的返回值,且返回值的類型為調用者本身,並將該方法以setter的形式返回,這樣就可以實現了連續調用,即為鏈式編程。
Masonry的一個典型的鏈式編程用法如下:
[self.containerView addSubview:self.bannerView];
[self.bannerView mas_makeConstraints:^(MASConstraintMaker *make) {
make.leading.equalTo(self.containerView.mas_leading);
make.top.equalTo(self.containerView.mas_top);
make.trailing.equalTo(self.containerView.mas_trailing);
make.height.equalTo(@(kViewWidth(131.0)));
}];
現在,簡單使用鏈式編程思想實現一個簡單計算器的功能:
3.3.1 在CaculateMaker.h文件中聲明一個方法add:
- CaculateMaker.h
// CaculateMaker.h // ChainBlockTestApp #import #import @interface CaculateMaker : NSObject @property (nonatomic, assign) CGFloat result;
- (CaculateMaker *(^)(CGFloat num))add; @end
3.3.2 在CaculateMaker.m文件中實現add方法:
- CaculateMaker.m
// CaculateMaker.m // ChainBlockTestApp #import "CaculateMaker.h" @implementation CaculateMaker - (CaculateMaker *(^)(CGFloat num))add;{ return ^CaculateMaker *(CGFloat num){
_result += num; return self;
};
} @end
3.3.3 在viewController裡面導入CaculateMaker.h文件,然後調用add方法就完成了鏈式語法:
- ViewController.m
CaculateMaker *maker = [[CaculateMaker alloc] init];
maker.add(20).add(30);
4. Block使用註意
4.1 截獲自動變數與__block說明符
前面講過block所在函數中的,捕獲自動變數。但是不能修改它,不然就是“編譯錯誤”。但是可以改變全局變數、靜態變數、全局靜態變數。其實這兩個特點不難理解:
-
不能修改自動變數的值是因為:block捕獲的是自動變數的const值,名字一樣,不能修改
-
可以修改靜態變數的值:靜態變數屬於類的,不是某一個變數。由於block內部不用調用self指針。所以block可以調用。
解決block不能修改自動變數的值,這一問題的另外一個辦法是使用__block修飾符。
4.2 截獲對象
對於捕獲ObjC對象,不同於基本類型;Block會引起對象的引用計數變化。
@interface MyClass : NSObject { NSObject* _instanceObj;
} @end @implementation MyClass NSObject* __globalObj = nil;
- (id) init { if (self = [super init]) {
_instanceObj = [[NSObject alloc] init];
} return self;
}
- (void) test { static NSObject* __staticObj = nil;
__globalObj = [[NSObject alloc] init];
__staticObj = [[NSObject alloc] init]; NSObject* localObj = [[NSObject alloc] init];
__block NSObject* blockObj = [[NSObject alloc] init]; typedef void (^MyBlock)(void) ;
MyBlock aBlock = ^{ NSLog(@"%@", __globalObj); NSLog(@"%@", __staticObj); NSLog(@"%@", _instanceObj); NSLog(@"%@", localObj); NSLog(@"%@", blockObj);
};
aBlock = [[aBlock copy] autorelease];
aBlock(); NSLog(@"%d", [__globalObj retainCount]); NSLog(@"%d", [__staticObj retainCount]); NSLog(@"%d", [_instanceObj retainCount]); NSLog(@"%d", [localObj retainCount]); NSLog(@"%d", [blockObj retainCount]);
} @end int main(int argc, charchar *argv[]) { @autoreleasepool {
MyClass* obj = [[[MyClass alloc] init] autorelease];
[obj test]; return 0;
}
}
執行結果為1 1 1 2 1。
__globalObj和__staticObj在記憶體中的位置是確定的,所以Blockcopy時不會retain對象。
_instanceObj在Blockcopy時也沒有直接retain_instanceObj對象本身,但會retain self。所以在Block中可以直接讀寫_instanceObj變數。
localObj在Blockcopy時,系統自動retain對象,增加其引用計數。
blockObj在Blockcopy時也不會retain。
4.3 Block引起的迴圈引用
一般來說我們總會在設置Block之後,在合適的時間回調Block,而不希望回調Block的時候Block已經被釋放了,所以我們需要對Block進行copy,copy到堆中,以便後用。
Block可能會導致迴圈引用問題,因為block在拷貝到堆上的時候,會retain其引用的外部變數,那麼如果block中如果引用了他的宿主對象,那很有可能引起迴圈引用,如:
- TestCycleRetain
- (void) dealloc { NSLog(@"no cycle retain");
}
- (id) init { self = [super init]; if (self) { #if TestCycleRetainCase1 //會迴圈引用 self.myblock = ^{
[self doSomething];
}; #elif TestCycleRetainCase2 //會迴圈引用 __block TestCycleRetain * weakSelf = self; self.myblock = ^{
[weakSelf doSomething];
}; #elif TestCycleRetainCase3 //不會迴圈引用 __weak TestCycleRetain * weakSelf = self; self.myblock = ^{
[weakSelf doSomething];
}; #elif TestCycleRetainCase4 //不會迴圈引用 __unsafe_unretained TestCycleRetain * weakSelf = self; self.myblock = ^{
[weakSelf doSomething];
}; #endif NSLog(@"myblock is %@", self.myblock); } return self;
}
- (void) doSomething { NSLog(@"do Something");
}
- main
int main(int argc, char * argv[]) { @autoreleasepool {
TestCycleRetain * obj = [[TestCycleRetain alloc] init];
obj = nil; return 0;
}
}
-
MRC情況下,用__block可以消除迴圈引用。
-
ARC情況下,必須用弱引用才可以解決迴圈引用問題,iOS 5之後可以直接使用__weak,之前則只能使用__unsafe_unretained了,__unsafe_unretained缺點是指針釋放後自己不會置
在上述使用 block中,雖說使用__weak,但是此處會有一個隱患,你不知道 self 什麼時候會被釋放,為了保證在block內不會被釋放,我們添加__strong。更多的時候需要配合strongSelf使用,如下:
__weak __typeof(self) weakSelf = self; self.testBlock = ^{
__strong __typeof(weakSelf) strongSelf = weakSelf;
[strongSelf test];
});
4.4 實用巨集定義:避免Block引起迴圈引用
- 第一步
在工程的TestAPP-Prefix.pch的文件中直接(不推薦)或在其導入的頭文件中間接寫入以下巨集定義:
//----------------------強弱引用---------------------------- #ifndef weakify #if DEBUG #if __has_feature(objc_arc) #define weakify(object) autoreleasepool{} __weak __typeof__(object) weak##_##object = object; #else #define weakify(object) autoreleasepool{} __block __typeof__(object) block##_##object = object; #endif #else #if __has_feature(objc_arc) #define weakify(object) try{} @finally{} {} __weak __typeof__(object) weak##_##object = object; #else #define weakify(object) try{} @finally{} {} __block __typeof__(object) block##_##object = object; #endif #endif #endif #ifndef strongify #if DEBUG #if __has_feature(objc_arc) #define strongify(object) autoreleasepool{} __typeof__(object) object = weak##_##object; #else #define strongify(object) autoreleasepool{} __typeof__(object) object = block##_##object; #endif #else #if __has_feature(objc_arc) #define strongify(object) try{} @finally{} __typeof__(object) object = weak##_##object; #else #define strongify(object) try{} @finally{} __typeof__(object) object = block##_##object; #endif #endif #endif
- 第二步
在設置Block體的時候,像如下這樣使用即可。
@weakify(self);
[footerView setClickFooterBlock:^{
@strongify(self);
[self handleClickFooterActionWithSectionTag:section];
}];
4.5 所有的Block裡面的self必須要weak一下?
很顯然答案不都是,有些情況下是可以直接使用self的,比如調用系統的方法:
[UIView animateWithDuration:0.5 animations:^{ NSLog(@"%@", self);
}];
因為這個block存在於靜態方法中,雖然block對self強引用著,但是self卻不持有這個靜態方法,所以完全可以在block內部使用self。
另外,來看一個Masonry代碼佈局的例子,這裡面的self會不會造成迴圈引用呢?
[self.headView mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerY.equalTo(self.otherView.mas_centerY);
}];
並不是 block 就一定會造成迴圈引用,是不是迴圈引用要看是不是相互持有強引用。block 里用到了 self,那 block 會保持一個 self 的引用,但是 self 並沒有直接或者間接持有 block,所以不會造成迴圈引用。可以看一下Masonry的源代碼:
- View+MASAdditions.m
- (NSArray *)mas_makeConstraints:(void(^)(MASConstraintMaker *))block { self.translatesAutoresizingMaskIntoConstraints = NO;
MASConstraintMaker *constraintMaker = [[MASConstraintMaker alloc] initWithView:self];
block(constraintMaker); return [constraintMaker install];
}
- MASConstraintMaker.m
- (id)initWithView:(MAS_VIEW *)view { self = [super init]; if (!self) return nil; self.view = view; self.constraints = NSMutableArray.new; return self;
}
持有鏈是這樣的,並沒有形成引用迴圈:
self ->self.headView ··· MASConstraintMaker構造block->self
註意觀察,這個作為方法參數的Block體並沒有被任何方持有。因此,我們放心在Masonry中使用self.xxx 不會迴圈引用的。而且這個block裡面用weakSelf還有可能會出問題,因為mas_qeual如果得到一個nil參數的話應該會導致程式崩潰。
因為UIView未強持有block,所以這個block只是個棧block,而且構不成迴圈引用的條件。棧block有個特性就是它執行完畢之後就出棧,出棧了就會被釋放掉。看mas_makexxx的方法實現會發現這個block很快就被調用了,完事兒就出棧銷毀,構不成迴圈引用,所以可以直接放心的使self。另外,這個與網路請求裡面使用self道理是一樣的。
作為一個開發者,有一個學習的氛圍跟一個交流圈子特別重要,這是一個我的iOS交流群:519832104 不管你是小白還是大牛歡迎入駐,分享經驗,討論技術,大家一起交流學習成長!
另附上一份各好友收集的大廠面試題,需要iOS開發學習資料、面試真題,可以添加iOS開發進階交流群,進群可自行下載!
5. Block與記憶體管理
根據Block在記憶體中的位置分為三種類型:
-
NSGlobalBlock是位於全局區的block,它是設置在程式的數據區域(.data區)中。
-
NSStackBlock是位於棧區,超出變數作用域,棧上的Block以及 __block變數都被銷毀。
-
NSMallocBlock是位於堆區,在變數作用域結束時不受影響。
註意:在 ARC 開啟的情況下,將只會有 NSConcreteGlobalBlock 和 NSConcreteMallocBlock 類型的 block。
正如它們名字顯示得一樣,表明瞭block的三種存儲方式:棧、全局、堆。獲取block對象中的isa的值,可以得到上面其中一個,下麵開始說明哪種block存儲在棧、堆、全局。
5.1 位於全局區:GlobalBlock
生成在全局區block有兩種情況:
- 定義全局變數的地方有block語法時
void(^block)(void) = ^ { NSLog(@"Global Block");}; int main() {
}
- block語法的表達式中沒有使用應截獲的自動變數時
int(^block)(int count) = ^(int count) { return count;
};
block(2);
雖然,這個block在迴圈內,但是blk的地址總是不變的。說明這個block在全局段。註:針對沒有捕獲自動變數的block來說,雖然用clang的rewrite-objc轉化後的代碼中仍顯示_NSConcretStackBlock,但是實際上不是這樣的。
5.2 位於棧記憶體:StackBlock
這種情況,在非ARC下是無法編譯的,在ARC下可以編譯。
- block語法的表達式中使用截獲的自動變數時
NSInteger i = 10;
block = ^{ NSLog(@"%ld", i);
};
block;
設置在棧上的block,如果其作用域結束,該block就被銷毀。同樣的,由於__block變數也配置在棧上,如果其作用域結束,則該__block變數也會被銷毀。
另外,例如
typedef void (^block_t)() ;
-(block_t)returnBlock{
__block int add=10; return ^{ printf("add=%d
",++add);
};
}
5.3 位於堆記憶體:MallocBlock
堆中的block無法直接創建,其需要由_NSConcreteStackBlock類型的block拷貝而來(也就是說block需要執行copy之後才能存放到堆中)。由於block的拷貝最終都會調用_Block_copy_internal函數。
void(^block)(void); int main(int argc, const char * argv[]) { @autoreleasepool {
__block NSInteger i = 10;
block = [^{
++i;
} copy];
++i;
block(); NSLog(@"%ld", i);
} return 0;
}
我們對這個生成在棧上的block執行了copy操作,Block和__block變數均從棧複製到堆上。上面的代碼,有跟沒有copy,在非ARC和ARC下一個是stack一個是Malloc。這是因為ARC下預設為Malloc(即使如此,ARC下還是有一些例外,下麵會講)。
block在ARC和非ARC下有巨大差別。多數情況下,ARC下會預設把棧block被會直接拷貝生成到堆上。那麼,什麼時候棧上的Block會複製到堆上呢?
-
調用Block的copy實例方法時
-
Block作為函數返回值返回時
-
將Block賦值給附有__strong修飾符id類型的類或Block類型成員變數時
-
將方法名中含有usingBlock的Cocoa框架方法或GCD的API中傳遞Block時
block在ARC和非ARC下的巨大差別
-
在 ARC 中,捕獲外部了變數的 block 的類會是 NSMallocBlock 或者 NSStackBlock,如果 block 被賦值給了某個變數,在這個過程中會執行 _Block_copy 將原有的 NSStackBlock 變成 NSMallocBlock;但是如果 block 沒有被賦值給某個變數,那它的類型就是 NSStackBlock;沒有捕獲外部變數的 block 的類會是 NSGlobalBlock 即不在堆上,也不在棧上,它類似 C 語言函數一樣會在代碼段中。
-
在非 ARC 中,捕獲了外部變數的 block 的類會是 NSStackBlock,放置在棧上,沒有捕獲外部變數的 block 時與 ARC 環境下情況相同。
例如
- (void)viewDidLoad {
[super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. [self testBlockForHeapOfARC];
}
-(void)testBlockForHeapOfARC{ int val =10; typedef void (^blk_t)(void);
blk_t block = ^{ NSLog(@"blk0:%d",val);
};
block();
}
即使如此,ARC下還是有一些例外:
例外
- (void)viewDidLoad {
[super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. [self testBlockForHeap0];
} #pragma mark - testBlockForHeap0 - crash -(NSArray *)getBlockArray0{ int val =10; return [NSArray arrayWithObjects:
^{NSLog(@"blk0:%d",val);},
^{NSLog(@"blk1:%d",val);},nil];
}
-(void)testBlockForHeap0{ NSArray *tempArr = [self getBlockArray0]; NSMutableArray *obj = [tempArr mutableCopy]; typedef void (^blk_t)(void);
blk_t block = (blk_t){[obj objectAtIndex:0]};
block();
}
這段代碼在最後一行blk()會異常,因為數組中的block是棧上的。因為val是棧上的。解決辦法就是調用copy方法。這種場景,ARC也不會為你添加copy,因為ARC不確定,採取了保守的措施:不添加copy。所以ARC下也是會異常退出。
例外的改進1
調用block 的copy函數,將block拷貝到堆上:
- (void)viewDidLoad {
[super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. [self testBlockForHeap1];
}
-(void)testBlockForHeap1{ NSArray *tempArr = [self getBlockArray1]; NSMutableArray *obj = [tempArr mutableCopy]; typedef void (^blk_t)(void);
blk_t block = (blk_t){[obj objectAtIndex:0]};
block();
}
-(NSArray *)getBlockArray1{ int val =10; return [NSArray arrayWithObjects:
[^{NSLog(@"blk0:%d",val);} copy],
[^{NSLog(@"blk1:%d",val);} copy],nil];
}
打個斷點可見,該Block的類型:
例外的改進2
例如下麵代碼中,在addBlockToArray方法中的block還是_NSConcreteStackBlock類型的,在testBlockForHeap2方法中就被覆制到了堆中,成為_NSConcreteMallocBlock類型的block:
- (void)viewDidLoad {
[super viewDidLoad]; // Do any additional setup after loading the view, typically from a nib. [self testBlockForHeap2];
}
- (void)addBlockToArray:(NSMutableArray *)array { int val =10;
[array addObjectsFromArray:@[
^{NSLog(@"blk0:%d",val);},
^{NSLog(@"blk1:%d",val);}]];
}
- (void)testBlockForHeap2{
NSMutableArray *array = [NSMutableArray array];
[self addBlockToArray:array]; typedef void (^blk_t)(void); blk_t block = (blk_t){[array objectAtIndex:0]};
block();
}
打個斷點可見,其中Block的類型:
5.4 Block的複製
-
在全局block調用copy什麼也不做
-
在棧上調用copy那麼複製到堆上
-
在堆上調用block 引用計數增加
-(void) stackOrHeap{
__block int val =10;
blkt1 s= ^{ return ++val;};
s();
blkt1 h = [s copy];
h();
}
不管block配置在何處,用copy方法複製都不會引起任何問題。在ARC環境下,如果不確定是否要copy這個block,那儘管copy即可。
最後的強調,在 ARC 開啟的情況下,除非上面的例外,預設只會有 NSConcreteGlobalBlock 和 NSConcreteMallocBlock 類型的 block。
6. Block的底層研究方法
6.1 研究工具:clang
為了研究編譯器是如何實現 block 的,我們需要使用 clang。clang 提供一個命令,可以將 Objetive-C 的源碼改寫成 c 語言的,藉此可以研究 block 具體的源碼實現方式。
首先cd到代碼文件目錄
cd /Users/ChenMan/iOSTest/BlockTestApp
然後執行clang命令
clang -rewrite-objc main.m
其中,main.m的代碼寫好如下
#include int main(int argc, char * argv[]) {
@autoreleasepool { typedef void (^blk_t)(void); blk_t block = ^{ printf("Hello, World!
");
};
block(); // return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); }
}
執行情況:
你會看到main.cpp
6.2 實現分析
這裡只選取部分關鍵代碼。
不難看出int main(int argc, char * argv[]) {就是主函數的實現。
int main(int argc, char * argv[]) { /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; typedef void (*blk_t)(void); blk_t block = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)block)->FuncPtr)((__block_impl *)block);
}
}
其中,__main_block_impl_0是block的一個C++的實現(最後面的_0代表是main中的第幾個block),也就是說也是一個結構體。
(1) __main_block_impl_0
__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;
}
};
(2) __block_impl
其中__block_impl的定義如下:
struct __block_impl { void *isa; int Flags; int Reserved; void *FuncPtr;
};
其結構體成員如下:
-
isa,指向所屬類的指針,也就是block的類型
-
flags,標誌變數,在實現block的內部操作時會用到
-
Reserved,保留變數
-
FuncPtr,block執行時調用的函數指針
可以看出,它包含了isa指針(包含isa指針的皆為對象),也就是說block也是一個對象(runtime裡面,對象和類都是用結構體表示)。
(3) __main_block_desc_0
__main_block_desc_0的定義如下:
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)};
其結構成員含義如下:
-
reserved:保留欄位
-
Block_size:block大小(sizeof(struct __main_block_impl_0))
以上代碼在定義__main_block_desc_0結構體時,同時創建了__main_block_desc_0_DATA,並給它賦值,以供在main函數中對__main_block_impl_0進行初始化。
(4) __main_block_desc_0
如上的main函數中,__main_block_func_0也是block的一個C++的實現
static void __main_block_func_0(struct __main_block_impl_0 *__cself) { printf("Hello, World!
");
}
(5) 綜合可知:
-
__main_block_impl_0的isa指針指向了_NSConcreteStackBlock。
-
從main函數的main.cpp中看,__main_block_impl_0的FuncPtr指向了函數__main_block_func_0。
-
__main_block_impl_0的Desc也指向了定義__main_block_desc_0時就創建的__main_block_desc_0_DATA,其中紀錄了block結構體大小等信息。
以上就是根據編譯轉換的結果。當然,由於 clang 改寫的具體實現方式和 LLVM 不太一樣,有急切底層興趣的讀者可以進行更深入的研究。