關(guān)于block
在iOS 4.0之后,block橫空出世,它本身封裝了一段代碼并將這段代碼當(dāng)做變量,通過block的方式進行回調(diào)。這不免讓我們想到在C函數(shù)中,我們可以定義一個指向函數(shù)的指針并且調(diào)用:
分享之前我還是要推薦下我自己建的iOS開發(fā)學(xué)習(xí)群:680565220,群里都是學(xué)ios開發(fā)的,如果你正在學(xué)習(xí)ios ,小編歡迎你加入,今天分享的這個案例已經(jīng)上傳到群文件,大家都是軟件開發(fā)黨,不定期分享干貨(只有iOS軟件開發(fā)相關(guān)的),包括我自己整理的一份2017最新的iOS進階資料和高級開發(fā)教程,歡迎進階中和進想深入iOS的小伙伴。
bool executeSomeTask(void) { //do something and return if success or not}bool (*taskPoint)(void);taskPoint = something;
上面的函數(shù)指針可以直接通過(*taskPoint)的方式調(diào)用executeSomeTask這個函數(shù),這樣對比block跟似乎C語言的函數(shù)指針是一樣的,但是兩者仍然存在以下區(qū)別:
block的代碼是內(nèi)聯(lián)的,效率高于函數(shù)調(diào)用
block對于外部變量默認是只讀屬性
block被Objective-C看成是對象處理
對于block的底層實現(xiàn)在網(wǎng)上已經(jīng)有很多資料了,其源碼更是可以在opensource.apple.com上下載,因此,本文更著重于對于block的應(yīng)用
block特性
認識block
先從一個簡單的需求來說:傳入兩個數(shù),并且計算這兩個數(shù)的和,為此創(chuàng)建了這樣一個block:
int (^sumOfNumbers)(int a, int b) = ^(int a, int b) { return a + b;};
這段代碼等號左側(cè)聲明一個名為sumOfNumbers的代碼塊,名稱前用^符號表示后面的字符串是block的名稱。最左側(cè)的int表示這個block的返回值,括號中間表示這個block的參數(shù)列表,這里接收兩個int類型的參數(shù)。 而在等號右側(cè)表示這個block的定義,其中返回值是可以省略的,編譯器會根據(jù)上下文自動補充返回值類型。使用^符號銜接著一個參數(shù)列表,使用括號包起來,告訴編譯器這是一個block,然后使用大括號將block的代碼封裝起來。
block代碼結(jié)構(gòu)
捕獲外界變量
block還可以訪問外界的局部變量,在我的從UIView動畫說起中有這么一段代碼,其中block內(nèi)部使用到了外部的局部變量:
CGPoint center = cell.center;CGPoint startCenter = center;startCenter.y += LXD_SCREEN_HEIGHT;cell.center = startCenter;[UIView animateWithDuration: 0.5 delay: 0.35 * indexPath.item usingSpringWithDamping: 0.6 initialSpringVelocity: 0 options: UIViewAnimationOptionCurveLinear animations: ^{ cell.center = center;} completion: ^(BOOL finished) { NSLog("animation %@ finished", finished? @"is", @"isn't");}];
這里面就用到了void(^animations)(void)跟void(^completion)(BOOL finished)兩個block,系統(tǒng)會在動畫開始以及動畫結(jié)束的時候分別調(diào)用者兩個block。在實現(xiàn)動畫的block內(nèi)部,代碼訪問了上文中的center屬性——在動畫開始的時候這個動畫函數(shù)的生命周期早已結(jié)束,而block會捕獲代碼外的局部變量,當(dāng)然這只局限于只讀操作。如果我們在block中修改外部變量,編譯器將會報錯:
block中修改外界局部變量
對于希望在block中修改的外界局部對象,我們可以給這些變量加上__block關(guān)鍵字修飾,這樣就能在block中修改這些變量。在捕獲變量特性中,還有一個有趣的小機制,我們把上面的代碼改成這樣:
CGPoint center = CGPointZero; CGPoint (^pointAddHandler)(CGPoint addPoint) = ^(CGPoint addPoint) { return CGPointMake(center.x + addPoint.x, center.y + addPoint.y);}center = CGPointMake(100, 100);NSLog(@"%@", pointAddHandler(CGPointMake(10, 10))); //輸出{10,10}
block在捕獲變量的時候只會保存變量被捕獲時的狀態(tài)(對象變量除外),之后即便變量再次改變,block中的值也不會發(fā)生改變。所以上面的代碼在計算新的坐標值時center的值依舊等于CGPointZero
循環(huán)引用
開頭說過,block在iOS開發(fā)中被視作是對象,因此其生命周期會一直等到持有者的生命周期結(jié)束了才會結(jié)束。另一方面,由于block捕獲變量的機制,使得持有block的對象也可能被block持有,從而形成循環(huán)引用,導(dǎo)致兩者都不能被釋放:
@implementation LXDObject{ void (^_cycleReferenceBlock)(void);}- (void)viewDidLoad{ [super viewDidLoad]; _cycleReferenceBlock = ^{ NSLog(@"%@", self); //引發(fā)循環(huán)引用 };}@end
遇到這種代碼編譯器只會告訴你存在警告,很多時候我們都是忽略警告的,這最后會導(dǎo)致內(nèi)存泄露,兩者都無法釋放。跟普通變量存在__block關(guān)鍵字一樣的,系統(tǒng)提供給我們__weak的關(guān)鍵字用來修飾對象變量,聲明這是一個弱引用的對象,從而解決了循環(huán)引用的問題:
__weak typeof(*&self) weakSelf = self;_cycleReferenceBlock = ^{ NSLog(@"%@", weakSelf); //弱指針引用,不會造成循環(huán)引用};
對于block這種有趣的特性,在唐巧的談Objective-C block的實現(xiàn)有詳細介紹block的底層實現(xiàn)代碼,我在這里就不多說了
使用block
在block出現(xiàn)之前,開發(fā)者實現(xiàn)回調(diào)基本都是通過代理的方式進行的。比如負責(zé)網(wǎng)絡(luò)請求的原生類NSURLConnection類,通過多個協(xié)議方法實現(xiàn)請求中的事件處理。而在最新的環(huán)境下,使用的NSURLSession已經(jīng)采用block的方式處理任務(wù)請求了。各種第三方網(wǎng)絡(luò)請求框架也都在使用block進行回調(diào)處理。這種轉(zhuǎn)變很大一部分原因在于block使用簡單,邏輯清晰,靈活等原因。接下來我會完成一次網(wǎng)絡(luò)請求,然后通過block進行回調(diào)處理。這些回調(diào)包括請求完成、下載進度
按照returnValue(^blockName)(parameters)的方式進行block的聲明未免麻煩了些,我們可以通過關(guān)鍵字typedef來為block起類型名稱,然后直接通過類型名進行block的創(chuàng)建:
@interface LXDDownloadManager: NSObject//block重命名typedef void(^LXDDownloadHandler)(NSData * receiveData, NSError * error);typedef void(^LXDDownloadProgressHandler)(CGFloat progress);- (void)downloadWithURL: (NSString *)URL parameters: (NSDictionary *)parameters handler: (LXDDownloadHandler)handler progress: (LXDDownloadProgressHandler)progress;@end@implementation LXDDownloadManager{ LXDDownloadProgressHandler _progress;}- (void)downloadWithURL: (NSString *)URL parameters: (NSDictionary *)parameters handler: (LXDDownloadHandler)handler progress: (LXDDownloadProgressHandler)progress{ //創(chuàng)建請求對象 NSURLRequest * request = [self postRequestWithURL: URL params: parameters]; NSURLSession * session = [NSURLSession sharedSession]; //執(zhí)行請求任務(wù) NSURLSessionDataTask * task = [session dataTaskWithRequest: request completionHandler: ^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { if (handler) { dispatch_async(dispatch_get_main_queue, ^{ handler(data, error); }); } }]; [task resume];}//進度協(xié)議方法- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten // 每次寫入的data字節(jié)數(shù) totalBytesWritten:(int64_t)totalBytesWritten // 當(dāng)前一共寫入的data字節(jié)數(shù) totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite // 期望收到的所有data字節(jié)數(shù) { double downloadProgress = totalBytesWritten / (double)totalBytesExpectedToWrite; if (_progress) { _progress(downloadProgress); }} @end
上面通過封裝NSURLSession的請求,傳入一個處理請求結(jié)果的block對象,就會自動將請求任務(wù)放到工作線程中執(zhí)行實現(xiàn),我們在網(wǎng)絡(luò)請求邏輯的代碼中調(diào)用如下:
#define QQMUSICURL @"https://www.baidu.com/link?url=UTiLwaXdh_-UZG31tkXPU62Jtsg2mSbZgSPSR3ME3YwOBSe97Hw6U6DNceQ2Ln1vXnb2krx0ezIuziBIuL4fWNi3dZ02t2NdN6946XwN0-a&wd=&eqid=ce6864b50004af120000000656fe235f"[[LXDDownloadManager alloc] downloadWithURL: QQMUSICURL parameters: nil handler ^(NSData * receiveData, NSError * error) { if (error) { NSLog(@"下載失敗:%@", error) } else { //處理下載數(shù)據(jù) }} progress: ^(CGFloat progress) { NSLog(@"下載進度%lu%%", progress*100);}];
仿swift高階函數(shù)
用過swift的開發(fā)者都知道swift的函數(shù)調(diào)用很好的體現(xiàn)了鏈式編程的思想,即將多個操作通過.連接起來,使得可讀性更強,比如ocString.stringByAppendingFormat("abc").stringByAppendingFormat("edf")就是連續(xù)調(diào)用了追加字符串的方法。這種編程方式的條件之一是每次函數(shù)調(diào)用必須有返回值。雖然在使用Objective-C開發(fā)的過程中,方法的調(diào)用是通過[target action]的方式完成的,但是block本身的調(diào)用方式也是通過blockName(parameters)的方式執(zhí)行的,與這種鏈式函數(shù)有異曲同工之妙。
在swift中提供了包括map、filter、reduce等十分簡潔優(yōu)秀的高階函數(shù)供我們對數(shù)組數(shù)據(jù)進行操作,同樣情況下,遍歷一個數(shù)組并求和在使用oc(不使用kvc)和swift的環(huán)境下的代碼是這樣的:
#pragma mark - OC codeNSArray numbers = @[@10, @15, @99, @66, @25];NSInteger totalNumber = 0;for (NSNumber number in numbers) { totalNumber += number.integerValue;}#pragma mark - swift codelet numbers = [10, 15, 99, 66, 25];let totalNumber = numbers.reduce(0, { $0+$1 })
無論是代碼量還是簡潔性,此時的oc都比不上swift。那么接下來就要通過神奇的block來為oc添加這些高階函數(shù)的實現(xiàn)。為此我們需要新建一個NSArray的分類擴展,命名為NSArray+LXDExtension
#import /// 數(shù)組元素轉(zhuǎn)換typedef id(^LXDItemMap)(id item);typedef NSArray *(^LXDArrayMap)(LXDItemMap itemMap);/// 數(shù)組元素篩選typedef BOOL(^LXDItemFilter)(id item);typedef NSArray *(^LXDArrayFilter)(LXDItemFilter itemFilter);/*** 擴展數(shù)組高級方法仿swift調(diào)用*/@interface NSArray (LXDExtension)@property (nonatomic, copy, readonly) LXDArrayMap map;@property (nonatomic, copy, readonly) LXDArrayFilter filter;@end
前面說了為了實現(xiàn)鏈式編程,函數(shù)調(diào)用的前提是具有返回對象。因此我使用了typedef聲明了幾個不同類型的block。雖然本質(zhì)上LXDArrayMap和LXDArrayFilter兩個block是一樣的,但是為了區(qū)分它們的功能,還是建議這么做。其實現(xiàn)文件如下:
typedef void(^LXDEnumerateHandler)(id item);@implementation NSArray (LXDTopMethod)- (LXDArrayMap)map{ LXDArrayMap map = (LXDArrayMap)^(LXDItemMap itemMap) { NSMutableArray * items = @.mutableCopy; for (id item in self) { [items addObject: itemMap(item)]; } return items; }; return map;}- (LXDArrayFilter)filter{ LXDArrayFilter filter = (LXDArrayFilter)^(LXDItemFilter itemFilter) { NSMutableArray * items = @.mutableCopy; for (id item in self) { if (itemFilter(item)) { [items addObject: item]; } } return items; }; return filter;}- (void)setFilter:(LXDArrayFilter)filter {}- (void)setMap:(LXDArrayMap)map {}@end
我們通過重寫setter方法保證block不會被外部修改實現(xiàn),并且在getter中遍歷數(shù)組的元素并調(diào)用傳入的執(zhí)行代碼來實現(xiàn)map和filter等功能。對于這兩個功能的實現(xiàn)也很簡單,下面舉出兩個調(diào)用高階函數(shù)的例子:
#pragma mark - 篩選數(shù)組中大于20的數(shù)值并轉(zhuǎn)換成字符串NSArray * numbers = @[@10, @15, @99, @66, @25, @28.1, @7.5, @11.2, @66.2];NSArray * result = numbers.filter((LXDArrayFilter)^(NSNumber * item) { return item.doubleValue > 20}).map((LXDArrayMap)^(NSNumber * item) { return [NSString stringWithFormat: @"string %g", item.doubleValue];});#pragma mark - 將數(shù)組中的字典轉(zhuǎn)換成對應(yīng)的數(shù)據(jù)模型NSArray * jsons = @[@{ ... }, @{ ... }, @{ ... }];NSArray * models = jsons.map((LXDArrayMap)^(id item) { return [[LXDModel alloc] initWithJSON: item];})
由于語法上的限制,雖然這樣的調(diào)用跟swift原生的調(diào)用對比起來還是復(fù)雜了,但通過block讓oc實現(xiàn)了函數(shù)鏈式調(diào)用的代碼看起來也清爽了很多。
總結(jié)
block捕獲變量、代碼傳遞、代碼內(nèi)聯(lián)等特性賦予了它多于代理機制的功能和靈活性,盡管它也存在循環(huán)引用、不易調(diào)試追溯等缺陷,但無可置疑它的優(yōu)點深受碼農(nóng)們的喜愛。如何更加靈活的使用block需要我們對它不斷的使用、探究了解才能完成。