-
什么是Block
OC作為C語言的超集钩杰,將面向過程的C語言擴展成了一門動態(tài)的面向?qū)ο笳Z言隧哮,其中Block就是OC對C語言中的函數(shù)指針幽勒、結(jié)構(gòu)體進行擴展而成的新的特色語法,block本質(zhì)是一個代碼塊焊虏,你也可以把block理解成能夠作為OC對象進行傳遞的匿名函數(shù)淡喜,并且是可以直接定義在其他函數(shù)內(nèi)部并共享該函數(shù)內(nèi)所有變量的匿名函數(shù)。
-
如何使用Block
既然block是OC的對象诵闭,那么我將通過用OC的NSString對象進行類比的方式幫助你更好的了解它×锻牛現(xiàn)有如下代碼:
#import "ViewController.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSString *myString = @"這是一個字符串對象";
[self stringTest:myString];
}
- (void)stringTest:(NSString *)pString{
NSLog(@"%@",pString);
}
上述代碼很簡單,把string對象傳遞給stringTest方法疏尿,在該方法中對該對象進行了打印操作∥林ィ現(xiàn)在,我們依樣畫葫蘆褥琐,用同樣的形式傳遞一個block對象锌俱,類似的,block的形參和實參的聲明如下:
- (void)viewDidLoad {
[super viewDidLoad];
void (^myBlock)(int, int) = ^void (int a, int b) {
NSLog(@"計算結(jié)果=%d",a+b);
};
[self sumFunction:myBlock];
}
- (void)sumFunction:(void(^)(int a,int b))block{
block(2,3);
}
第一次接觸block的新手看到以上代碼或許會感到費勁敌呈,我們一步一步來解析這個語法稍顯"別扭"的block贸宏。
void (^myBlock)(int, int) = ^void (int a, int b) {
NSLog(@"計算結(jié)果=%d",a+b);
};
這是Block的完整定義造寝,等號左邊從左往右看,該block的返回類型為void吭练、變量名叫myBlock诫龙、^符號用于申明myBlock是一個block類型的變量、block入?yún)閮蓚€int型變量线脚,等號右邊則為myBlock的具體內(nèi)部實現(xiàn)赐稽,用{}將實現(xiàn)代碼包裹起來』虢模看到這里你是否覺得block和函數(shù)越發(fā)類似,有返回值類型晰绎,有入?yún)⒃⒙洹H绻闶煜語音,你會發(fā)現(xiàn)等號左邊的申明方式和C語言中的函數(shù)指針非常相似荞下,僅把*變成了^而已伶选,當然myBlock變量實際上是一個結(jié)構(gòu)體,而非單獨一個指針尖昏,等號右邊實際上則是一個沒有函數(shù)名的匿名函數(shù)仰税,將一個匿名函數(shù)的實現(xiàn)賦值給myBlock結(jié)構(gòu)體中的一個指針,就構(gòu)成了這樣一個完整的block型變量抽诉。而myBlock變量根據(jù)其作用域不同決定了其可以在對象內(nèi)部陨簇,甚至對象間進行傳遞。在實際開發(fā)中迹淌,我們常常會將myString申明為一個屬性河绽,以供本類中其他方法讀寫,現(xiàn)在唉窃,我們同樣將myBlock申明為一個屬性耙饰,通過申明屬性的方式,可以讓代碼看起來更加清晰明了纹份,申明方式如下:
//用typedef將MyBlock自定成一個類型名
typedef void(^MyBlock)(int a,int b);
#import "ViewController.h"
@interface ViewController ()
//block創(chuàng)建在棧區(qū)苟跪,使用copy修飾
@property(nonatomic,copy)MyBlock myBlock;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.myBlock = ^void (int a, int b) {
NSLog(@"計算結(jié)果=%d",a+b);
};
[self sumFunction:self.myBlock];
}
- (void)sumFunction:(MyBlock)pBlock{
pBlock(2,3);
}
@end
通過這種方式申明block,比第一種方式更清晰明了蔓涧。需要注意的是件已,使用typedef重命名時,(^MyBlock)中的MyBlock被抽象了一種自定義類型名而不再是變量名蠢笋,self.myBlock中的myBlock才是被作為變量進行傳遞拨齐。在此筆者希望讀者都能使用typedef的方式申明Block,這樣不論是形參又或?qū)崊⒌纳昝髯蚰寄苁褂媚阕约喝〉腗yBlock類型名來直接創(chuàng)建對象瞻惋。這種方式更接近于我們平時的代碼習慣厦滤,現(xiàn)在仔細觀察以上代碼,我們不難發(fā)現(xiàn)viewDidLoad方法和sumFunction方法之間歼狼,進行了一次簡單的"通信"掏导,我們先在viewDidLoad方法中創(chuàng)建了self.myBlock變量,即在viewDidLoad方法中內(nèi)聯(lián)了一個匿名函數(shù)羽峰,我們知道self.myBlock的內(nèi)部實現(xiàn)趟咆,但我們暫時還不想要執(zhí)行這個self.myBlock對象內(nèi)的實現(xiàn)代碼,直到代碼執(zhí)行到sumFunction方法梅屉,在sumFunction方法內(nèi)才又反向調(diào)用了這個Block值纱。嗯,看上去很不錯坯汤,但實際好像并沒有什么用處虐唠。事實上,在對象內(nèi)部方法之間使用block通信的確有些多此一舉惰聂,實際開發(fā)中也很少用到疆偿。因為你完全可以把self.myBlock的實現(xiàn)重新定義成新的方法,進行兩次正向調(diào)用搓幌。其實block的真正用武之地確實并不在此杆故,接下來,請閱讀如下較復雜的常見場景:
在UIController的viewDidLoad方法中溉愁,我們初始化頁面的同時還需要異步的從接口獲取頁面數(shù)據(jù)從而完成對view的渲染处铛,假設(shè)你的UIController已經(jīng)十分臃腫,你不希望UIController再負責網(wǎng)絡(luò)請求的邏輯叉钥,于是你寫了一個URLRequestManager類來專門負責網(wǎng)絡(luò)請求業(yè)務罢缸。當你需要發(fā)起URL請求時,只需要實例化這個manager投队,由他發(fā)起請求即可枫疆,Controller并不關(guān)心manager的內(nèi)部實現(xiàn)代碼,也不關(guān)心何時完成請求敷鸦,只需要在請求成功或者失敗時的結(jié)果告訴控制器即可息楔,控制器會在拿到數(shù)據(jù)后將數(shù)據(jù)賦值給view,完成界面的最終顯示扒披。
對于上述需求值依,我們就可以通過block來達到目的。代碼如下:
#import "ViewController.h"
#import "URLRequestManager.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//創(chuàng)建頁面
[self initSubViews];
//數(shù)據(jù)請求
[self requestData];
}
- (void)initSubViews{
//view創(chuàng)建代碼實現(xiàn)
}
- (void)requestData{
//發(fā)起請求
[URLRequestManager requestWithUrl:@“url地址”
parameters:nil
backHandler:^(BOOL isSucessful, NSError *error, NSData *data) {
if (isSucessful) {
//拿到數(shù)據(jù)
NSLog(@"請求成功");
NSLog(@"%@",data);
//可以在這里進行界面賦值
}else{
//彈出錯誤提示
NSLog(@"%@",error);
}
}];
}
@end
URLRequestManager提供了一個網(wǎng)絡(luò)請求方法碟案,URLRequestManager申明和實現(xiàn)如下:
#import <Foundation/Foundation.h>
typedef void(^RequestBackHandler)(BOOL isSucessful, NSError *error,NSData *data);
@interface URLRequestManager : NSObject
/**
發(fā)起網(wǎng)絡(luò)請求
@param url 請求地址
@param parameters 請求體
@param handler 回調(diào)Block
*/
+(void)requestWithUrl:(NSString *)url parameters:(NSDictionary *)parameters backHandler:(requestBackHandler)handler;
@end
#import "URLRequestManager.h"
@implementation URLRequestManager
+(void)requestWithUrl:(NSString *)url parameters:(NSDictionary *)parameters backHandler:(RequestBackHandler)handler{
//延時兩秒調(diào)用block愿险,模擬網(wǎng)絡(luò)請求
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
//執(zhí)行block回調(diào)
handler(YES,nil,nil);
});
}
@end
上述代碼中,我們就利用block完成了一次對象間的通信价说,代碼簡單的還原了異步網(wǎng)絡(luò)請求的需求辆亏,控制器的requestData方法中調(diào)用了manager的類方法發(fā)起網(wǎng)絡(luò)請求风秤,并且傳入了請求所需的參數(shù)以及定義好的block對象,實際使用中扮叨,我們不關(guān)心類方法的內(nèi)部實現(xiàn)缤弦,只要在其完成請求后再執(zhí)行調(diào)用我們早已經(jīng)定義好的block對象即可。如果你熟悉代理模式彻磁,會發(fā)現(xiàn)其實這兩者之間相似但又有細微差別碍沐,兩者主要都用于對象的回調(diào),但block更注重結(jié)果的傳輸衷蜓,代碼更清晰簡練累提,delegate更偏向過程信息的傳輸,代碼更規(guī)范嚴謹恍箭。
-
Block的內(nèi)部構(gòu)造
前面提到刻恭,block對象實際上是一個結(jié)構(gòu)體而非簡單的函數(shù)指針,現(xiàn)在我們就具體來探索一下Block的神秘本質(zhì)扯夭。block的數(shù)據(jù)結(jié)構(gòu)定義如下
對應的結(jié)構(gòu)體定義如下:
struct Block_descriptor {
unsigned long int reserved;
unsigned long int size;
void (*copy)(void *dst, void *src);
void (*dispose)(void *);
};
struct Block_layout {
void *isa;
int flags;
int reserved;
void (*invoke)(void *, ...);
struct Block_descriptor *descriptor;
/* Imported variables. */
};
從上面代碼看出,一個block實例實際上由以下6部分構(gòu)成:
isa指針:指向該block類型的類的指針,每個Objective-C對象鞍匾,都有一個isa
指針交洗,指向?qū)ο蟮念悾鳦lass里也有個isa的指針, 指向meteClass(元類)橡淑。元類保存了類方法的列表构拳。元類也有isa指針,它的isa指針最終指向的是一個根元類(root meteClass)。根元類的isa指針指向本身梁棠。
flags:按bit位表示一些block的附加信息置森,比如判斷block類型、判斷block引用計數(shù)符糊、判斷block是否需要執(zhí)行輔助函數(shù)等凫海。
reserved:保留變量,我的理解是表示block內(nèi)部的變量數(shù)男娄。
invoke:函數(shù)指針行贪,指向block的實現(xiàn)代碼地址。
descriptor:指向結(jié)構(gòu)體的指針模闲,block的附加描述信息建瘫,比如保留變量數(shù)、block的大小尸折、copy和dispose輔助函數(shù)的函數(shù)指針啰脚,copy函數(shù)為當block執(zhí)行copy操作或者當block從棧上拷貝到堆上時調(diào)用,dispose函數(shù)則是block在堆上釋放時調(diào)用实夹。
variables:block內(nèi)部捕獲的對象橄浓,如void (^blk)(void) = ^{print(fmt,val)}粒梦;此時,variables中則為fmt和val這兩個變量
由于篇幅有限贮配,筆者不再對block的各個部分做具體介紹谍倦。
-
Block的類型
根據(jù)block的本身的存儲位置,block有三種類型泪勒,分別如下:
NSGlobalBlock: 類似函數(shù)昼蛀,位于text段;
NSStackBlock : 位于棧內(nèi)存圆存,僅在函數(shù)作用域內(nèi)有效叼旋;
NSMallocBlock: 位于堆內(nèi)存。
block的類型并非我們創(chuàng)建block時手動指定的沦辙,而是編譯器根據(jù)block捕獲的外部變量的不同而自動確定的夫植。以下三個例子分別對應三種類型的block。
{
float (^myBlock)(float, float) = ^(float a, float b){
NSLog(@"heheda");
};
NSLog(@"block is %@", myBlock);
//block is <__NSGlobalBlock__: 0x47d0>
}
{
NSString *str = @"heheda";
NSLog(@"block is %@", ^{
NSLog(@"%@", str);
});
//block is <__NSStackBlock__: 0xbfffdac0>
}
{
NSString *str = @"heheda";
void (^TestBlock)(void) = ^{
NSLog(@"%@", str);
};
NSLog(@"block is %@", TestBlock);
//block is <__NSStackBlock__: 0x75425a0> MRC
//block is <__NSMallocBlock__: 0x75425a0> ARC
}
分析以上三個打印結(jié)果油讯,我們得出以下結(jié)論:
- 如果block沒有捕獲任何外部變量详民,該block所需要的全部信息都能在編譯期確定。該block是全局存在的陌兑,相當于函數(shù)沈跨。
- 如果block捕獲了自動變量,block存在于棧區(qū)兔综,copy操作可以使其存儲于堆區(qū)饿凛。
- 在ARC下,賦值的同時編譯器會幫我們進行copy操作软驰,無需手動涧窒。
-
Block注意事項
1、若要修改捕獲到的自動變量锭亏,用__block修飾該變量纠吴。
示例:
typedef void(^MyBlock)(void);
- (void)viewDidLoad {
[super viewDidLoad];
int x = 3;
MyBlock myBlock = ^{
x+=1;
};
myBlock();
NSLog(@"%d",x);
}
運行以上代碼編譯器會報錯,并告訴你要將變量x添加__block修飾符贰镣。其實這個錯誤原因很容易理解呜象,學習C語言的時候我們知道,向某個函數(shù)傳入變量的值碑隆,實際上只是將該變量的值賦值給該函數(shù)內(nèi)的形參恭陡,函數(shù)內(nèi)部并不能修改這個變量本身,若要修改該變量上煤,應傳入其地址休玩。block同樣可以通過這種方式達到目的,不同之處在于x是直接被“捕獲”而不是作為參數(shù)傳入,代碼如下:
typedef void(^MyBlock)(void);
- (void)viewDidLoad {
[super viewDidLoad];
int x = 3;
int *p = &x;
MyBlock myBlock = ^{
*p +=1;
};
myBlock();
NSLog(@"%d",x);
}
然而這樣的代碼明顯不是我們想要的拴疤,因此OC為我們提供了__block修飾符永部,所以上述代碼可以用以下代碼代替:
typedef void(^MyBlock)(void);
- (void)viewDidLoad {
[super viewDidLoad];
__block int x = 3;
MyBlock myBlock = ^{
x +=1;
};
myBlock();
NSLog(@"%d",x);
}
將x用__block修飾后,指針p指向x的操作便交由block內(nèi)部去實現(xiàn)呐矾,此外苔埋,x在存儲方式也發(fā)上了變化,由原本的棧區(qū)改為了堆區(qū)蜒犯。而對于全局變量和靜態(tài)變量组橄,我們則可以直接在block內(nèi)部修改其值。
block內(nèi)部還可以訪問類的實例變量和self變量罚随,且block會按照屬性的修飾語義進行引用玉工。這就引出了我們需要特別注意的問題,即循環(huán)引用淘菩。
2遵班、如果塊所捕獲的對象直接或間接地保留了塊本身,那么就要當心循環(huán)引用問題潮改。
示例:
#import "ViewController.h"
typedef void(^MyBlock)(void);
@interface ViewController ()
@property(nonatomic,strong)NSString *myStr;
@property(nonatomic,copy)MyBlock myBlock;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.myBlock = ^{
self.myStr = @"123";
};
}
@end
運行上述代碼狭郑,系統(tǒng)會有如下警告
Capturing 'self' strongly in this block is likely to lead to a retain cycle
如果你熟悉OC內(nèi)存管理機制,你應該知道這是self和myBlock兩個對象相互引用汇在,從而導致了內(nèi)存無法正確釋放愿阐。為了避免循環(huán)引用,可以將代碼如下修改(MRC下將__weak替換為__block):
__weak __typeof__(self) weakSelf = self;
self.myBlock = ^{
weakSelf.myStr = @"123";
};
上述情況系統(tǒng)很容易能夠檢測出趾疚,故我們可以排查修改,但有時候會遇到情況較為復雜的情況以蕴,編譯器未必能發(fā)現(xiàn)糙麦,而循環(huán)引用導致的crash難以追蹤,一旦出現(xiàn)非常頭疼丛肮。所以在使用block的時候赡磅,希望讀者們能夠多多注意這方面的問題。