第6章 塊與大中樞派發(fā)
block顿锰、塊呛哟、Block 塊癣诱、Block 對象在大多數(shù) Objective-C 文檔中語義相同。
block 是一種可在 C龄章、C++ 及 Objective-C 代碼中使用的“詞法閉包"(lexical closure)吃谣,借由此機制,開發(fā)者可將代碼像對象一樣傳遞做裙,令其在不同環(huán)境(context)下運行岗憋。還有個關鍵的地方是,在定義 block 的范圍內锚贱,它可以訪問到其中的全部變量仔戈。
GCD 是一種與 block 有關的技術,它提供了對線程的抽象,而這種抽象則基于“派發(fā)隊列” (dispatch queue) 监徘。開發(fā)者可將塊排入隊列中晋修,由GCD負責處理所有調度事宜。GCD會根據(jù)系統(tǒng)資源情況耐量,適時地創(chuàng)建飞蚓、復用、摧毀后臺線程(background thread)廊蜒,以便處理每個隊列趴拧。 此外,使用GCD還可以方便地完成常見編程任務山叮,比如編寫 “只執(zhí)行一次的線程安全代碼” (thread-safe single-code execution)著榴,或者根據(jù)可用的系統(tǒng)資源來并發(fā)執(zhí)行多個操作。
第37條:理解“塊”這一概念
塊的基礎知識
簡單的 block:
^{
//Block implementation here
};
block 語法結構:
typedef returnType(^name)(arguments);
定義一個名為 someBlock 的變量:
void (^someBlock)() = ^{
//Block implementation here
};
block 的強大之處在于屁倔,在 block 內部可以訪問 block 外部變量:
// 將變量聲明為 __block 之后才可以在 Block 內部對此變量進行修改
__block int additional = 5;
// 聲明Block塊
int (^addBlock)(int a, int b) = ^(int a, int b) {
additional = 10;
return a + b + additional;
};
// 使用Block塊
int add = addBlock(2, 5);
- 如果block所捕獲的變量是對象類型脑又,那么就會自動保留它。系統(tǒng)在釋放這個塊的時候锐借,也會將其一并釋放问麸。
- block本身可視為對象,它也有引用計數(shù)钞翔。
- 如果將block定義在 Objective-C 類的實例方法中严卖,那么除了可以訪問類的所有實例變童之外,還可以使用
self
變量布轿。block總能修改實例變量哮笆,所以在聲明時無須加_block
。不過汰扭,如果通過讀寫操作捕獲了實例變量稠肘,那么也會自動把self
變量一并捕獲,因為實例變量是與self
所指代的實例關聯(lián)在一起的萝毛。
- (void)anInstanceMethod {
//...
void (^someBlock)() = ^{
_anInstanceVariable = @"Something";
NSLog(@"_anInstanceVariable = %@",_anInstanceVariable);
};
//...
}
- 在block中项阴,直接訪問實例變量和通過
self
來訪問該實例變量是等效的。 -
self
也是個對象珊泳,因而 block 在捕獲它時也會將其保留鲁冯。如果self
所指代的那個對象同時也保留了塊,那么這種情況通常就會導致引用循環(huán)色查。
塊的內部結構
block對象在棧中的結構:
對應的結構體定義:
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. */
};
-
isa
指針:指向表明該block類型的類。 -
flags
:按bit位表示一些block的附加信息撞芍,比如判斷block類型秧了、判斷block引用計數(shù)、判斷block是否需要執(zhí)行輔助函數(shù)等序无。 -
reserved
:保留變量验毡,我的理解是表示block內部的變量數(shù)衡创。 -
invoke
:函數(shù)指針,指向具體的block實現(xiàn)的函數(shù)調用地址晶通。 -
descriptor
:block的附加描述信息璃氢,比如保留變量數(shù)、block對象的大小狮辽、進行copy
或dispose
的輔助函數(shù)指針一也。 -
variables
:即捕獲到的變量,因為block有閉包性喉脖,所以可以訪問block外部的局部變量椰苟。這些variables
就是復制到結構體中的外部局部變量或變量的地址。
全局塊树叽、棧塊及堆塊
- 雖然 block 是對象舆蝴,但是其所占的內存區(qū)域是分配在棧中的。這就是說题诵,塊只在定義它的那個范圍內有效洁仗。
- 給 block 對象發(fā)送
copy
消息可以把 Block 從棧復制到堆⌒远В拷貝后的塊赠潦,可以在定義它的那個范圍之外使用。
void (^block)();
if (/** condition */) {
block = [^{
NSLog(@"Block A");
} copy];
}else {
block = [^{
NSLog(@"Block B");
} copy];
}
block();
- 全局塊(global block)不會捕捉任何狀態(tài)(比如外圍的變量等)篷店,運行時也無須有狀態(tài)來參與祭椰。塊所使用的整個內存區(qū)域,在編譯期已經(jīng)完全確定了疲陕,因此方淤,全局塊可以聲明在全局內存里,而不需要在每次用到的時候于棧中創(chuàng)建蹄殃。另外携茂,全局塊的拷貝操作是個空操作,因為全局塊決不可能為系統(tǒng)所回收诅岩。這種塊實際上相當于單例讳苦。下面就是個全局塊:
void (^block) () = ^{
NSLog(@"This is a block");
);
- 全局的靜態(tài) block:
_NSConcreteGlobalBlock
類型的block要么是空block吩谦,要么是不訪問任何外部變量的block鸳谜。它既不在棧中,也不在堆中式廷,我理解為它可能在內存的全局區(qū)咐扭。 - 保存在棧中的block:
_NSConcreteStackBlock
類型的block有閉包行為,也就是有訪問外部變量,并且該block只且只有有一次執(zhí)行蝗肪,因為棧中的空間是可重復使用的袜爪,所以當棧中的block執(zhí)行一次之后就被清除出棧了,所以無法多次使用薛闪。 - 保存在堆中的block:
_NSConcreteMallocBlock
類型的block有閉包行為辛馆,并且該block需要被多次執(zhí)行。當需要多次執(zhí)行時豁延,就會把該block從棧中復制到堆中昙篙,供以多次執(zhí)行。
要點
- Clang 是開發(fā) Mac OS X 及 iOS 程序所用的編譯器术浪。
- block 塊是 C瓢对、C++、Objective-C 中的詞法閉包胰苏。
- block 塊可以接收參數(shù)硕蛹,也可以有返回值。
- block 塊可以分配在椝恫ⅲ或堆上法焰,也可以是全局的。分配在棧上的 block 塊可拷貝到堆里倔毙,這樣的話埃仪,就和標準的 Objective-C 對象一樣,具備引用計數(shù)了陕赃。
第38條:為常用的塊類型創(chuàng)建 typedef
代碼塊便捷寫法:typedefBlock
typedef <#returnType#>(^<#name#>)(<#arguments#>);
示例一:
// 定義 Block 塊
typedef int(^EOCSomeBlock)(BOOL flag, int value);
// 使用 Block 塊
EOCSomeBlock blcok = ^(BOOL flag, int value) {
// Implementation
};
示例二:
typedef void(^EOCCompletionHandler)(NSData *data, NSError *error);
// 方法使用 Block 塊作為參數(shù)
- (void)startingWithCompletionHandler:(EOCCompletionHandler)completion;
使用 typedef
類型定義還便于重構 block 的類型簽名卵蛉。
// 新增一個參數(shù),用以表示完成任務所花的時間
typedef void(^EOCCompletionHandler)
(NSData *data, NSTimeInterval duration, NSError *error);
要點
- 以
typedef
重新定義 block 類型么库,可以令 block 變量用起來更加簡單傻丝。 - 定義新類型時應遵循現(xiàn)有的命名習慣,勿使其名稱與別的的類型相沖突诉儒。
- 不妨為同一個 block 簽名定義多個類型別名葡缰。如果要重構的代碼使用了 block 類型的某個別名,那么只需修改相應的
typedef
中的 block 簽名即可忱反,無需改動其他typedef
泛释。
第39條:用 handler 塊降低代碼分散程度
為用戶界面編碼時,一種常用的范式就是“異步執(zhí)行任務”(perform task asynchronously)温算。這種范式的好處在于:處理用戶界面的顯示及觸摸操作所用的線程怜校,不會因為要執(zhí)行I/O或網(wǎng)絡通信這類耗時的任務而阻塞。這個線程通常稱為主線程(main thread)注竿。
異步方法在執(zhí)行完任務之后韭畸,需要以某種手段通知相關代碼宇智。實現(xiàn)此功能有很多辦法蔓搞。常用的技巧是設計一個委托協(xié)議(參見第23條)胰丁,令關注此事件的對象遵從該協(xié)議。對象成為delegate之后喂分,就可以在相關事件發(fā)生時(例如某個異步任務執(zhí)行完畢時)得到通知了锦庸。
Delegate 模式:
#import <Foundation/Foundation.h>
@class EOCNetworkFetcher;
@protocol EOCNetworkFetcherDelegate <NSObject>
- (void)networkFetcher:(EOCNetworkFetcher *)fetcher
didFinishWithData:(NSData *)data;
@end
@interface EOCNetworkFetcher : NSObject
@property (nonatomic, weak) id<EOCNetworkFetcherDelegate> delegate;
- (instancetype)initWithURL:(NSURL *)url;
- (void)start;
@end
其他類則可以像下面這樣使用此類所提供的 API 并遵守實現(xiàn)相應的 delegate 協(xié)議:
- (void)fetchFooData {
NSURL *url = [[NSURL alloc] initWithString:
@"https://www.pinterest.com"];
EOCNetworkFetcher *fetcher =
[[EOCNetworkFetcher alloc] initWithURL:url];
fetcher.delegate = self;
[fetcher start];
}
- (void)networkFetcher:(EOCNetworkFetcher *)fetcher didFinishWithData:(NSData *)data {
// deal with data
}
block 模式:
#import <Foundation/Foundation.h>
typedef void(^EOCNetworkFetcherCompletionHandler)(NSData *data);
@interface EOCNetworkFetcher : NSObject
- (instancetype)initWithURL:(NSURL *)url;
- (void)startWithCompletionHandler:(EOCNetworkFetcherCompletionHandler)handler;
@end
其他類獲取數(shù)據(jù):
- (void)fetchFooData {
NSURL *url = [[NSURL alloc] initWithString:
@"https://www.pinterest.com"];
EOCNetworkFetcher *fetcher =
[[EOCNetworkFetcher alloc] initWithURL:url];
// 調用 start 方法時直接以內聯(lián)形式定義 Completion Handler。
[fetcher startWithCompletionHandler:^(NSData *data) {
// deal with data
}];
}
相比于委托模式蒲祈,block 模式可以使代碼更清晰整潔甘萧、API 更緊致、邏輯關聯(lián)性更強梆掸。
委托模式有個缺點:如果類要分別使用多個獲取器下載不同數(shù)據(jù)扬卷,那么就得在 delegate 回調方法里根據(jù)傳入的獲取器參數(shù)來切換。
而使用 block 來寫的好處是:無須保存獲取器酸钦,也無須在回調方法里切換怪得。每個 completion handler 的業(yè)務邏輯都是和相關的獲取器對象一起來定義的。
1. 分別用兩個處理程序來處理操作失敗和操作成功
這種 API 設計風格很好卑硫,由于成功和失敗的情況要分別處理徒恋,所以調用此 API 的代碼也就會按照邏輯,把應對成功和失敗情況的代碼分開來寫欢伏,這將令代碼更易讀懂入挣。而且,若有需要硝拧,還可以把處理失敗情況或成功情況所用的代碼省略径筏。
#import <Foundation/Foundation.h>
typedef void(^EOCNetworkFetcherCompletionHandler)(NSData *data);
typedef void(^EOCNetworkFetcherErrorHandler)(NSError *error);
@interface EOCNetworkFetcher : NSObject
- (instancetype)initWithURL:(NSURL *)url;
- (void)startWithCompletionHandler:
(EOCNetworkFetcherCompletionHandler)handler
failureHandler:
(EOCNetworkFetcherErrorHandler)failure;
@end
// 其他類使用:
- (void)fetchFooData {
NSURL *url = [[NSURL alloc] initWithString:
@"https://www.pinterest.com"];
EOCNetworkFetcher *fetcher =
[[EOCNetworkFetcher alloc] initWithURL:url];
[fetcher startWithCompletionHandler:^(NSData *data) {
// deal with data
} failureHandler:^(NSError *error) {
// deal with error
}];
}
2. 把處理失敗所需代碼與處理成功所用代碼,都封裝到同一個 completion handle 塊里
缺點:由于全部邏輯都寫在一起障陶,導致塊代碼冗長復雜滋恬。
優(yōu)點:能把所有業(yè)務邏輯都放在一起使其更加靈活。例如咸这,在傳入錯誤信息時夷恍,可以把數(shù)據(jù)也傳進來。有時數(shù)據(jù)正下載到一半媳维,突然網(wǎng)絡故障了酿雪。在這種情況下,可以把數(shù)據(jù)及相關的錯誤都回傳給塊侄刽。這樣的話指黎,completion handler 就能據(jù)此判斷問題并適當處理了,而且還可利用已下載好的這部分數(shù)據(jù)做些事情州丹。
總體來說醋安,筆者建議使用同一個塊來處理成功與失敗情況杂彭。
#import <Foundation/Foundation.h>
typedef void(^EOCNetworkFetcherCompletionHandler)
(NSData *data, NSError *error);
@interface EOCNetworkFetcher : NSObject
- (instancetype)initWithURL:(NSURL *)url;
- (void)startWithCompletionHandler:
(EOCNetworkFetcherCompletionHandler)handler;
@end
// 其他類使用:
- (void)fetchFooData {
NSURL *url = [[NSURL alloc] initWithString:
@"https://www.pinterest.com"];
EOCNetworkFetcher *fetcher =
[[EOCNetworkFetcher alloc] initWithURL:url];
// 需要在塊代碼中檢測傳人的error變量,并且要把所有邏輯代碼都放在一處
[fetcher startWithCompletionHandler:^(NSData *data, NSError *error) {
if (error) {
// handle failure
}else {
// handle success
}
}];
}
基于 handler 來設計API還有個原因吓揪,就是某些代碼必須運行在特定的線程上亲怠,比如,Cocoa 與 Cocoa Touch 中的 UI 操作必須在主線程上執(zhí)行:
// NSNotificationCenter
- (id <NSObject>)addObserverForName:(nullable NSNotificationName)name
object:(nullable id)obj
queue:(nullable NSOperationQueue *)queue
usingBlock:(void (^)(NSNotification *note))block;
要點
- 在創(chuàng)建對象時柠辞,使用內聯(lián)的 handler 塊將相關業(yè)務邏輯一并聲明团秽。
- 在有多個實例需要監(jiān)控時,如果采用委托模式叭首,那么經(jīng)常需要根據(jù)傳入的對象來切換习勤,而若改用 handler 塊來實現(xiàn),則可直接將 block 與相關對象放在一起焙格。
- 設計 API 時如果用到了 handler 塊图毕,那么可以增加一個參數(shù),使調用者可通過此參數(shù)來決定應該把 block 安排在哪個隊列上執(zhí)行眷唉。
第40條:用塊引用其所屬對象時不要出現(xiàn)保留環(huán)
使用 block 很容易導致循環(huán)引用:
// EOCNetworkFetcher.h
#import <Foundation/Foundation.h>
typedef void(^EOCNetworkFetcherCompletionHandler)
(NSData *data);
@interface EOCNetworkFetcher : NSObject
@property (nonatomic, strong, readonly) NSURL *url;
- (instancetype)initWithURL:(NSURL *)url;
- (void)startWithCompletionHandler:
(EOCNetworkFetcherCompletionHandler)completion;
@end
// EOCNetworkFetcher.m
#import "EOCNetworkFetcher.h"
@interface EOCNetworkFetcher ()
@property (nonatomic, strong, readwrite) NSURL *url;
@property (nonatomic, copy)
EOCNetworkFetcherCompletionHandler completionHandler;
@property (nonatomic, strong) NSData *downloadData;
@end
@implementation EOCNetworkFetcher
- (instancetype)initWithURL:(NSURL *)url {
if (self = [super init]) {
_url = url;
}
return self;
}
- (void)startWithCompletionHandler:
(EOCNetworkFetcherCompletionHandler)completion {
self.completionHandler = completion;
// 開啟網(wǎng)絡請求
// 設置 downloadData 屬性
// 下載完成后予颤,以回調方式執(zhí)行 Block
[self p_requestCompleted];
}
// 為了能在下載完成后通過 p_requestCompleted 方法執(zhí)行調用者所指定的塊,
// 需要把 completion handler 保存到實例變量
// ?? _networkFetcher → _completionHandler
- (void)p_requestCompleted {
if (_completionHandler) {
_completionHandler(_downloadData);
}
}
@end
// 某個類可能會創(chuàng)建以上網(wǎng)絡數(shù)據(jù)獲取器對象厢破,并用其從URL中下載數(shù)據(jù):
@implementation EOCDataModel {
// ?? EOCDataModel → _networkFetcher
EOCNetworkFetcher *_networkFetcher;
NSData *_fetchedData;
}
- (void)downloadData {
NSURL *url = [[NSURL alloc] initWithString:@"http://www.example.com"];
_networkFetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
[_networkFetcher startWithCompletionHandler:^(NSData *data) {
NSLog(@"Request URL %@ finished",_networkFetcher.url);
// 因為 completion handler 塊要設置 _fetchedData 實例變量,所以它必須捕獲 self 變量荣瑟,而 self 指向 EOCDataModel 類
// ?? _completionHandler → EOCDataModel
_fetchedData = data;
// ??等 completion handler 塊執(zhí)行完畢后,再打破保留環(huán)摩泪,以便使獲取器對象在handler 塊執(zhí)行期間保持存活狀態(tài)笆焰。
_networkFetcher = nil;
}];
}
問題:
- 在上例中,唯有 completion handler 運行過后见坑,方能解除保留環(huán)嚷掠。若是completion handler—直不運行,那么保留環(huán)就無法打破荞驴,于是內存就會泄漏不皆。
- 如果 completion handler 塊所引用的對象最終又引用了這個塊本身,那么就會出現(xiàn)保留環(huán)熊楼。
- (void)downloadData {
NSURL *url = [[NSURL alloc] initWithString:
@"http://www.example.com"];
EOCNetworkFetcher *networkFetcher = [[EOCNetworkFetcher alloc] initWithURL:url];
[networkFetcher startWithCompletionHandler:^(NSData *data) {
// completionHandler → networkFetcher.url
// networkFetcher → completionHandler
NSLog(@"Request URL %@ finished",networkFetcher.url);
_fetchedData = data;
}];
}
解決方法:獲取器對象之所以要把 completion handler 塊保存在屬性里面霹娄,其唯一目的就是想稍后使用這個塊■昶可是犬耻,獲取器一旦運行過 completion handler 之后,就沒有必要再保留它了执泰。所以枕磁,只需將 p_requestCompleted
方法按如下方式修改即可:
- (void)p_requestCompleted {
if (_completionHandler) {
_completionHandler(_downloadData);
}
self.completionHandler = nil; // ??
}
要想清楚塊可能會捕獲并保留哪些對 象。如果這些對象又直接或間接保留了塊术吝,那么就要考慮怎樣在適當?shù)臅r機解除保留環(huán)计济。
要點
- 如果 block 所捕獲的對象直接或間接的保留了 block 本身茸苇,那么就得當心循環(huán)引用的問題。
- 一定要找個適當?shù)臅r機解除循環(huán)引用沦寂,而不能把責任推給 API 的調用者学密。
第41條:多用派發(fā)隊列,少用同步鎖
使用同步鎖實現(xiàn)同步機制:
-
@synchronized 同步塊:
- (void)synchronizedMethod { @synchronized (self) { // ... // 根據(jù)給定對象凑队,自動創(chuàng)建一個鎖则果,并等待塊中的代碼執(zhí)行完畢。 // 濫用 @synchronized (self) 會降低代碼效率 } // 執(zhí)行到這段代碼結尾處漩氨,釋放鎖。 }
-
NSRecursiveLock 遞歸鎖
- 線程能夠多次持有該鎖遗增, 而不會出現(xiàn)死鎖(deadlock)現(xiàn)象叫惊。
- 在極端情況下,同步塊會導致死鎖做修。
GCD:串行并發(fā)隊列
@implementation HQLBlockObject {
dispatch_queue_t _syncQueue;
}
// 自定義并發(fā)隊列
// ??注意到霍狰,文章中此處作者使用的是全局并發(fā)隊列,而在 Ray Wenderlich 的GCD系列教程中使用的是自定義并發(fā)隊列:
// 原因在于:全局隊列中還可能有其他任務正在執(zhí)行饰及,一旦加鎖就會阻塞其他任務的正常執(zhí)行蔗坯,因此我們開辟一個新的自定義并發(fā)隊列專門處理這個問題。
_syncQueue = dispatch_queue_create("com.effectiveobjectivec.syncQueue", DISPATCH_QUEUE_CONCURRENT);
- (NSString *)someString {
__block NSString *localSomeString;
dispatch_sync(_syncQueue, ^{
localSomeString = _someString;
});
return localSomeString;
}
- (void)setSomeString:(NSString *)someString {
dispatch_barrier_async(_syncQueue, ^{
_someString = someString;
})
}
把設置操作與獲取操作都安排在序列化的隊列里執(zhí)行燎含,這樣的話宾濒,所有針對屬性的訪問操作就都同步了。
柵欄塊
在隊列中屏箍,柵欄塊必須單獨執(zhí)行绘梦,不能與其他塊并行。這只對并發(fā)隊列有意義赴魁,因為串行隊列中的塊總是按順序逐個來執(zhí)行的卸奉。并發(fā)隊列如果發(fā)現(xiàn)接下來要處理的塊是個柵欄塊 (barrier block) ,那么就一直要等當前所有并發(fā)塊都執(zhí)行完畢颖御,才會單獨執(zhí)行這個柵欄塊榄棵。待柵欄塊執(zhí)行過后,再按正常方式繼續(xù)向下處理潘拱。
dispatch_barrier_sync(dispatch_queue_t _Nonnull queue, ^{
})
dispatch_barrier_async(dispatch_queue_t _Nonnull queue, ^{
})
- (NSString *)someString {
// 后臺執(zhí)行
_syncQueue = dispatch_get_global_queue(0, 0);
__block NSString *localSomeString;
// 同步后臺隊列
dispatch_sync(_syncQueue, ^{
localSomeString = _someString;
});
return localSomeString;
}
- (void)setSomeString:(NSString *)someString {
// 異步柵欄隊列
dispatch_barrier_async(_syncQueue, ^{
_someString = someString;
});
}
要點
- 派發(fā)隊列可用來表述同步語義疹鳄,這種做法要比使用 @synchronized 塊或 NSLock 對象更簡單。
- 將同步與異步派發(fā)結合起來泽铛,可以實現(xiàn)與普通加鎖機制一樣的同步行為尚辑,而這么做卻不會阻塞執(zhí)行異步派發(fā)的線程。
- 使用同步隊列及柵欄塊盔腔,可以令同步行為更加高效杠茬。
第42條:多用GCD月褥,少用 performSelector 系列方法
// 接受的參數(shù)就是要執(zhí)行的選擇子
- (id)performSelector:(SEL)aSelector;
該方法與直接調用選擇子等效:
[self performSelector:@selector(selectorMethod)];
[self selectorMethod];
特點:編譯器要等到運行期才能知道執(zhí)行的選擇子∑昂恚可以在動態(tài)綁定之上再次使用動態(tài)綁定宁赤,因而可以實現(xiàn)出下面這種功能:
SEL selector;
if ( /** condition A */ ) {
selector = @selector(foo);
}else if ( /** condition B */ ) {
selector = @selector(bar);
}else {
selector = @selector(baz);
}
[self performSelector:selector];
不推薦使用 performSelector
方法的原因:
?? ARC 下會引起內存泄漏:編譯器并不知道將要執(zhí)行的選擇子是什么,因此栓票,也就不了解其方法簽名及返回值决左,甚至連是否有返回值都不清楚,而由于編譯器不知道方法名走贪,所以就沒辦法運用ARC的內存管理規(guī)則來判定返回值是不是應該釋放佛猛。鑒于此,ARC采用了比較謹慎的做法坠狡,就是不添加釋放操作继找。然而這么做可能導致內存泄漏,因為方法在返回對象時可能已經(jīng)將其保留了逃沿。
即使使用靜態(tài)分析器婴渡,也很難偵測到隨后的內存泄漏馋记。
??返回值只能是 void 或對象類型辅鲸,performSelector
方法的返回值類型是 id张惹。如果想返回整數(shù)或浮點數(shù)等類型的值歼狼,那么就需要執(zhí)行一些復雜的轉換操作了籽慢,而這種轉換很容易出錯怠堪。
如果返冋值的類型為C語言結構體弛饭,則不可以使用 performSelector
方法倘感。
??performSelector
方法還有諸多局限性置谦,傳入的參數(shù)類型必須是對象類型且最多只能接受2個參數(shù)堂鲤、具備延后執(zhí)行的方法無法處理帶有2個參數(shù)的選擇子。
下面是幾個使用 Block 的替代方案:
延后執(zhí)行
?推薦:
double delayInSeconds = 5.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW,
delayInSeconds *NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(), ^{
[self doSomething];
});
?反對:
[self performSelector:@selector(doSomething)
withObject:nil
afterDelay:5.0];
主線程執(zhí)行
?推薦:
// 同步主線程(waitUntilDone:YES)
dispatch_sync(dispatch_get_main_queue(), ^{
[self doSomething];
});
// 異步主線程(waitUntilDone:NO)
dispatch_async(dispatch_get_main_queue(), ^{
[self doSomething];
});
?反對:
[self performSelectorOnMainThread:@selector(doSomething)
withObject:nil
waitUntilDone:NO];
要點
-
performSelector
系列方法在內存管理方面容易有疏失媒峡。它無法確定將要執(zhí)行的 selector 具體是什么瘟栖,因而ARC編譯器就無法插入適當?shù)膬却婀芾矸椒ā?/li> -
performSelector
系列方法所能處理的 selector 太過局限了,selector 的返回值類型及發(fā)送給方法的參數(shù)個數(shù)都受到限制谅阿。 - 如果想把任務放在另一個線程上執(zhí)行半哟,那么最好不要用
performSelector
系列方法而是應該把任務封裝到 block 里然后調用 GCD 機制的相關方法來實現(xiàn)。
第43條:掌握 GCD 及操作隊列的使用時機
GCD & NSOperation
GCD 是純 C 的 API签餐,而 NSOperation(操作隊列)則是 Objective-C 的對象寓涨。
在GCD中,任務用 Block 來表示氯檐,而 Block 是個輕量級數(shù)據(jù)結構(參見第37條)戒良。與之相反,“操作"(operation) 則是個更為重量級的 Objective-C 對象
-
用 NSOperationQueue 類的
addOperationWithBlock:
方法搭配 NSBlockOperation 類來使用操作隊列冠摄,其語法與純 GCD 方式非常類似糯崎。使用 NSOperation 及 NSOperationQueue 的好處如下:- 取消某個操作几缭。運行任務之前, 可以在 NSOperation 對象上調用
cancel
方法取消任務執(zhí)行。 - 指定操作間的依賴關系沃呢。
- 通過鍵值觀測機制(簡稱 KVO)監(jiān)控 NSOperation 對象的屬性年栓。
- 指定操作的優(yōu)先級。操作的優(yōu)先級表示此操作與隊列中其他操作之間的優(yōu)先關 系薄霜。優(yōu)先級高的操作先執(zhí)行某抓,優(yōu)先級低的后執(zhí)行。
- 重用 NSOperation 對象惰瓜。
- 取消某個操作几缭。運行任務之前, 可以在 NSOperation 對象上調用
操作隊列有很多地方勝過派發(fā)隊列否副。操作隊列提供了多種預設的執(zhí)行任務的方式,開發(fā)者可以直接使用鸵熟。
-
有一個API選用了操作隊列而非派發(fā)隊列副编,這就是 NSNotificationCemer,開發(fā)者可通過其中的方法來注冊監(jiān)聽器流强,以便在發(fā)生相關事件時得到通知,而這個方法接受的參數(shù)是塊呻待,不是選擇子打月。
- (id <NSObject>)addObserverForName:(nullable NSNotificationName)name object:(nullable id)obj queue:(nullable NSOperationQueue *)queue usingBlock:(void (^)(NSNotification *note))bloc;
應該盡可能選用高層API,只在確有必要時才求助于底層蚕捉。筆者也同意這個說法奏篙,但我并不盲從。某些功能確實可以用高層的Objective-C方法來做迫淹,但這并不等于說它就一定比底層實現(xiàn)方案好秘通。要想確定哪種方案更佳,最好還是測試一下性能敛熬。
要點
- 在解決多線程與任務管理問題時肺稀,派發(fā)隊列并非唯一方案。
- 操作隊列提供了一套高層的 Objective-C API应民,能實現(xiàn)純 GCD 所具備的絕大部分功能话原,而且還能完成一些更為復雜的操作,那些操作若改用GCD來實現(xiàn)诲锹,則需另外編寫代碼繁仁。
第44條:通過 Dispatch Group 機制,根據(jù)系統(tǒng)資源狀況來執(zhí)行任務
dispatch group 是 GCD 的一項特性归园,能夠把任務分組黄虱。調用者可以等待這組任務執(zhí)行完畢,也可以在提供回調函數(shù)之后繼續(xù)往下執(zhí)行庸诱,這組任務完成時捻浦,調用者會得到通知晤揣。這個功能有許多用途,其中最重要默勾、最值得注意的用法碉渡,就是把將要并發(fā)執(zhí)行的多個任務合為一組,于是調用者就可以知道這些任務何時才能全部執(zhí)行完畢母剥。比方說滞诺,可以把壓縮一系列文件的任務表示成 dispatch group。
創(chuàng)建及使用 dispatch group:
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
// 并行執(zhí)行的線程一
});
dispatch_group_async(group, dispatch_get_global_queue(0, 0), ^{
// 并行執(zhí)行的線程二
});
dispatch_group_notify(group, dispatch_get_global_queue(0, 0), ^{
// 匯總結果
});
給任務編組的兩種方法:
// 1.比 dispatch_async 多一個參數(shù)环疼,用于表示待執(zhí)行的塊所歸屬的組
dispatch_group_async(dispatch_group_t group,
dispatch_queue_t queue,
dispatch_block_t block);
// 2.dispatch_group_enter习霹、dispatch_group_leave 需要成對使用
dispatch_group_enter(dispatch_group_t group); // 使分組里正要執(zhí)行的任務數(shù)遞增,
dispatch_group_leave(dispatch_group_t group); // 使分組里正要執(zhí)行的任務數(shù)遞減.
dispatch_group_wait 函數(shù)用于等待 dispatch group 執(zhí)行完畢:
dispatch_group_wait(dispatch_group_t group,
dispatch_time_t timeout);
示例:
令數(shù)組中的每個對象都執(zhí)行某項任務炫隶,并且等待所有任務執(zhí)行完畢:
dispatch_queue_t queue =
dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0);
dispatch_group_t group = dispatch_group_create();
for (id object in collection) {
dispatch_group_async(group, queue, ^{
[object performTask];
});
}
dispatch_group_wait(group, DISPATCH_TIME_FOREVER);
// 任務執(zhí)行完畢后繼續(xù)操作
// 若當前線程不應阻塞淋叶,則可以使用 dispatch_group_notify 代替 dispatch_group_wait
dispatch_queue_t notifyQueue = dispatch_get_main_queue();
dispatch_group_notify(group, notifyQueue, ^{
// 任務執(zhí)行完畢后繼續(xù)操作
});
開發(fā)者未必總需要使用 dispatch group。有時候采用單個隊列搭配標準的異步派發(fā)伪阶,也可實現(xiàn)同樣效果:
// 自定義串行隊列
dispatch_queue_t queue =
dispatch_queue_create("com.effecitveobjectivec.queue", NULL);
for (id object in collection) {
dispatch_async(queue, ^{
[object performTask];
});
}
dispatch_async(queue, ^{
// 任務執(zhí)行完畢后繼續(xù)操作
});
根據(jù)系統(tǒng)資源狀況來執(zhí)行任務:
為了執(zhí)行隊列中的塊煞檩,GCD會在適當?shù)臅r機自動創(chuàng)建新線程或復用舊線程。如果使用并發(fā)隊列栅贴,那么其中有可能會有多個線程斟湃,這也就意味著多個塊可以并發(fā)執(zhí)行。在并發(fā)隊列中檐薯,執(zhí)行任務所用的并發(fā)線程數(shù)量凝赛,取決于各種因素,而GCD主要是根據(jù)系統(tǒng)資源狀況來判定這些因素的坛缕。假如CPU有多個核心墓猎,并且隊列中有大量任務等待執(zhí)行,那么GCD就可能會給該隊列配備多個線程赚楚。通過dispatch group所提供的這種簡便方式毙沾,既可以并發(fā)執(zhí)行一系列給定的任務,又能在全部任務結束時得到通知直晨。由于GCD 有并發(fā)隊列機制搀军,所以能夠根據(jù)可用的系統(tǒng)資源狀況來并發(fā)執(zhí)行任務。而開發(fā)者則可以專注于業(yè)務邏輯代碼勇皇,無須再為了處理并發(fā)任務而編寫復雜的調度器罩句。
遍歷某個 collection ,并在其每個元素上執(zhí)行任務敛摘,而這也可以用另外一個 GCD 函數(shù)來實現(xiàn):
dispatch_apply(size_t iterations,
dispatch_queue_t queue,
void (^block)(size_t));
此函數(shù)會將塊反復執(zhí)行一定的次數(shù)门烂,每次傳給塊的參數(shù)值都會遞增,從0開始,直至 “ iterations-1 ”屯远。
// 自定義串行隊列
dispatch_queue_t queue =
dispatch_queue_create("com.effecitveobjectivec.queue", NULL);
dispatch_apply(10, queue, ^(size_t) {
// Perform Task:0~9
});
與 for 循環(huán)不同的是蔓姚, dispatch_apply 所用的隊列可以是并發(fā)隊列。但是 dispatch_apply 會持續(xù)阻塞慨丐,直到所有任務都執(zhí)行完畢為止坡脐。
假如把塊派給了當前隊列(或者體系中高于當前隊列的某個串行隊列),就將導致死鎖房揭。若想在后臺執(zhí)行任務备闲,則應使用 dispatch group。
要點
- 一系列任務可歸入一個 dispatch group 之中捅暴。開發(fā)者可以在這組任務執(zhí)行完畢時獲得通知恬砂。
- 通過 dispatch group,可以在并發(fā)式派發(fā)隊列里同時執(zhí)行多項任務蓬痒。此時GCD會根據(jù)系統(tǒng)資源狀況來調度這些并發(fā)執(zhí)行的任務泻骤。開發(fā)者若自己來實現(xiàn)此功能,則需編寫大量代碼梧奢。
第45條:使用 dispatch_once 來執(zhí)行只需運行一次的線程安全代碼
單例模式:
+ (instancetype)sharedInstance {
static EOCClass *sharedInstance = nil;
// 為保證線程安全狱掂,將創(chuàng)建單例實例的代碼包裹在同步塊里。
@synchronized (self) {
if (!sharedInstance) {
sharedInstance = [[self alloc] init];
}
}
return sharedInstance;
}
dispatch_once 函數(shù):
_dispatch_once(dispatch_once_t *predicate,
dispatch_block_t block)
該函數(shù)保證相關的塊必定會執(zhí)行亲轨,且僅執(zhí)行一次符欠。首次調用該函數(shù)時,必然會執(zhí)行塊中的代碼瓶埋,最重要的一點在于,此操作完全是線程安全的诊沪。
+ (instancetype)sharedInstance {
static EOCClass *sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
- 使用 dispatch_once 可以簡化代碼并且徹底保證線程安全养筒。
- 由于每次調用時都必須使用完全相同的標記, 所以標記要聲明成
static
。把該變量定義在static
作用域中端姚,可以保證編譯器在每次執(zhí)行sharedlnstance
方法時都會復用這個變量晕粪,而不會創(chuàng)建新變量。 - 此外渐裸,dispatch_once 更高效巫湘。 @synchronized 使用了重量級的同步機制,每次運行代碼前都要獲取鎖昏鹃。而 dispatch_once 采用原子訪問(atomic access)來查詢標記尚氛,以判斷其所對應的代碼原來是否已經(jīng)執(zhí)行過。
要點
- 經(jīng)常需要編寫只需執(zhí)行一次的線程安全代碼洞渤。通過 GCD 所提供的 dispatch_once 函數(shù)阅嘶,很容易就能實現(xiàn)此功能。
- 標記應該聲明在
static
或global
作用域中,這樣的話讯柔,在把只需執(zhí)行一次的 block 傳給dispatch_once 函數(shù)時抡蛙,傳進去的標記也是相同的。
第46條:不要使用 dispatch_get_current_queue
// 此函數(shù)返回當前正在執(zhí)行代碼的隊列魂迄。
// This function is deprecated and will be removed in a future release.
dispatch_get_current_queue(void);
Tips:iOS 系統(tǒng)從 6.0 版本起粗截,已經(jīng)正式棄用此函數(shù)了。
要點
- dispatch_get_current_queue 函數(shù)的行為常常與開發(fā)者所預期的不同捣炬。此函數(shù)已廢棄熊昌,只應做調試之用。
- 由于派發(fā)隊列是按層級來組織的遥金,所以無法單用某個隊列對象來描述當前隊列這一概念浴捆。
- dispatch_get_current_queue 函數(shù)用于解決由不可重入的代碼所引發(fā)的死鎖,然而能用此函數(shù)解決的問題稿械,通常也能改用“隊列特定數(shù)據(jù)”來解決选泻。