當前多線程編程的核心就是“塊”(block)與“大中樞派發”(Grand Central Dispatch,GCD)。
37.理解“塊”這一概念
1.塊的基礎知識
塊與函數類似,只不過是直接定義在另一個函數里的,和定義它的那個函數共享同一個范圍內的東西。塊用“^”符號來表示,后面跟著一對花括號,括號里面是塊的實現代碼。
^{
//Block implementation
}
//塊類型的語法結構及事例
return_type (^block_name)(parameters)
//使用
block_name(parameters);
eg:
int (^addBlock)(int a, int b) = ^(int a, int b){
return a + b;
}
//使用
int result = addBlock(3,5);
塊的強大之處在于:在聲明它的范圍里,所有變量都可以為其所捕獲。這就是說,塊所在的范圍里的全部變量,在塊里依然可用。默認情況下,為塊所捕獲的變量,是不可以在塊里修改的,聲明變量的時候可以加上__block修飾符,這樣就可以在塊內修改了。對于實例變量,塊總是能夠修改的,所以對于要修改的實例變量則無需加__block.
塊的保留環:塊里面使用了實例變量或self,self也是個對象,因而塊在捕獲它時也會將其保留。如果self所指代的那個對象同時也保留了塊,那么這種情況通常就會導致保留環。
2.全局塊、棧塊及堆塊
定義塊的時候,其所占的內存區域是分配在棧中的。這就是說,塊只在定義它的那個范圍內有效。
void (^block)();
if(/* some condition */){
block = ^{
NSLog(@"Block A");
}
}else{
block = ^{
NSLog(@"Block B");
}
}
block();
定義在if及else語句中的兩個塊都分配在棧內存中。編譯器會給每個塊分配好棧內存,然而等離開了相應的范圍之后,編譯器有可能把分配給塊的內存覆寫掉。于是,這兩個塊只能保證在對于的if或else語句范圍內有效。這樣寫出來的代碼可以編譯,但是運行起來時而正確,時而錯誤。若編譯器未覆寫待執行的塊,則程序照常運行,若覆寫,則程序崩潰。
為解決此問題,可給塊對象發送copy消息來拷貝之。這樣的話,就可以把塊從棧復制到堆了。拷貝后的塊,可以在定義它的那個范圍之外使用。而且,一旦復制到堆上,塊就成了帶引用計數的對象了。
//全局塊
void (^blocks)(void) = ^{
// self.propert = @"string";//會報錯,不會捕捉任何狀態
};
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
}
除了“棧塊”和“堆塊”之外,還有一類塊叫做“全局塊”(global block)。這種塊不會捕捉任何狀態(比如外圍的變量等),運行時也無需有狀態來參與。塊所使用的整個內存區域,在編譯期已經完全確定了,因此,全局塊可以聲明在全局內存里,而不需要在每次用到的時候于棧中創建。另外,全局塊的拷貝操作是空操作,因為全局塊決不可能為系統所回收。
要點:
- 塊是C、C++、Objective-C中的詞法閉包。
- 塊可接受參數,也可返回值
- 塊可以分配在棧或堆上,也可以是全局的。分配在棧上的塊可拷貝到堆里,這樣的話,就和標準的Objective-C對象一樣,具備引用計數了。
38.為常用的塊類型創建typedef
與其他類型的變量不同,在定義塊變量時,要把變量名放在類型之中,而不是放在右側。鑒于此,我們應該為常用的塊類型起個別名。為了隱藏復雜的塊類型,需要用到C語言中名為“類型定義”(type definition)的特性。typedif關鍵字用于給類型起個易讀的別名。
typedef int(^BlockName)(BOOL flag, int value);
BlockName block = ^(BOOL flag, int value){
// block implementation
}
要點:
- 以typedef重新定義塊類型,可令塊變量用起來更加簡單
- 定義新類型時應遵循現有的命名習慣,勿使用其名稱與別的類型相沖突
- 不妨為同一個塊簽名定義多個類型別名。如果要重構的代碼使用了塊類型的某個別名,那么只需要修改相應typedef中的塊簽名即可,無需改動其他typedef。
39.用handler塊降低代碼分散程度
設計API時,對于回調的選擇有多種,選用合適的回調方式能夠讓我們的代碼更加清晰整潔。
要點:
- 在創建對象時,可以使用內聯的handler塊將相關業務邏輯一并聲明
- 在有多個實例需要監控時,如果采用委托模式,那么經常需要根據傳入的對象來切換,而若改用handler塊來實現,則可直接將塊與相關對象放在一起
- 設計API時如果用到了handler塊,那么可以增加一個參數,使調用者可通過此參數來決定應該把塊安排在哪個隊列上執行。
40.用塊引用其所屬對象時不要出現保留環
要點:
- 如果塊所捕獲的對象直接或間接地保留了塊本身,那么就得當心保留環問題
- 一定要找個適當的時機解除保留環,而不能把責任推給API的調用者。
41.多用派發隊列,少用同步鎖
在Objective-C中,如果有多個線程要執行同一份代碼,那么有時可能會出現問題。這種情況下,通常要使用鎖來實現某種同步機制,在GCD出現之前,有兩種辦法,第一種是采用內置的“同步塊”(synchronization block);第二種是直接使用NSLock對象;
//同步塊(synchronization block)
- (void)synchronizedMehtod{
@synchronized(self){
// safe 安全的執行代碼
}
}
這種寫法會根據給定的對象,自動創建一個鎖,并等待塊中的代碼執行完畢。執行到這段代碼結尾處,鎖就釋放了。這么寫通常沒錯,因為它可以保證每個對象實例都能不受干擾地運行其synchronizationMehtod方法。然而,濫用@synchronized(self)則會降低代碼效率,因為共用同一個鎖的那些同步塊,都必須按照順序執行。若是在self對象上頻繁加鎖,那么程序可能要等另一段與此無關的代碼執行完畢,才能繼續執行當前代碼,這樣做其實并沒有必要。
//NSLock對象
_lock = [[NSLock alloc] init];
- (void)synchronizedMethod{
[_lock lock];
//safe code
[_lock unlock];
}
也可以使用NSRecursiveLock這種“遞歸鎖”(recursive lock),線程能夠多次持有該鎖,而不會出現死鎖(deadlock)現象。這兩種方法都很好,不過也有其缺陷。比方說,在極端情況下,同步塊會導致死鎖,另外,效率也不見得很高,而如果直接使用鎖對象的話,一旦遇到死鎖,就會非常麻煩。
有種簡單而高效的辦法可以代替同步塊或鎖對象,那就是使用“串行同步隊列”。將讀取操作及寫入操作都安排在同一個隊列里,即可保證數據同步。
_syncQueue = dispatch_queue_create("com.effectiveobjectivec.syncQueue",NULL);
- (NSString *)someString{
__block NSString *localString;
dispatch_sync(_syncQueue,^{
localString = _someString;
});
return localString;
}
- (void)setSomeString:(NSString *)someString{
dispatch_sync(_syncQueue,^{
_someString = someString;
});
}
此模式的思路是:把設置操作與獲取操作都安排在序列化的隊列里執行,這樣的話,所有針對屬性的訪問操作就都同步了。為了shi塊代碼能夠設置局部變量,獲取方法中用到了__block語法,若是拋開這一點,那么這種寫法要比前面那些更為整潔。全部加鎖任務都在GCD中處理,而GCD是在相當深的底層來實現的,于是能夠做許多優化。因此,開發者無需擔心那些事,只要專心把訪問方法寫好就行。
要點:
- 派發隊列可用來表述同步語義(synchronization semantic),這種做法要比使用@synchronized塊或NSLock對象更簡單
- 將同步與異步派發結合起來,可以實現與普通加鎖機制一樣的同步行為,而這么做卻不會阻塞執行異步派發的形成
- 使用同步隊列及柵欄塊,可以令同步行為更加高效
42.多用GCD,少用performSelector系列方法
SEL selector = @selector(test);
[self performSelector:selector];
報警告:PerformSelector may cause a leak because its selector is unknown
原因在于:編譯器并不知道將要調用的選擇子是什么,因此,也就不了解其方法簽名及返回值,甚至連是否有返回值都不清楚。而且,由于編譯器不知道方法名,所以就沒辦法運用ARC的內存管理規則來判定返回值是不是應該釋放。鑒于此,ARC采用了比較謹慎的做法,就是不添加釋放操作。然而這么做可能導致內存泄漏,因為方法在返回對象時可能已經將其保留了。
- (id)performSelector:(SEL)aSelector;
- (id)performSelector:(SEL)aSelector withObject:(id)object;
- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;
傳入的參數都是id類型,所以傳入的參數必須是對象才行,基本數據類型不行;再者,返回值也是id。還有一個問題就是,多個參數的傳遞,我們可能需要使用字典等集合來進行封裝再進行傳遞。
如果改為其他替代方案,那就不受這些限制了。最主要的替代方案就是使用塊。
//using performSelector
[self performSelector:@selector(doSomeThing) withObject:nil afterDelay:5.0];
//using GCD
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC));
dispatch_after(time, dispatch_get_main_queue(), ^{
[self doSomeThing];
});
//using performSelector
[self performSelectorOnMainThread:@selector(doSomeThing) withObject:nil waitUntilDone:NO];
//using GCD
//if waitUntilDone is YES,then dispatch_sync
dispatch_async(dispatch_get_main_queue(), ^{
[self doSomeThing];
});
要點:
- performSelector系列方法在內存管理方法容易有疏失。它無法確定將要執行的選擇子具體是什么,因而ARC編譯器也就無法插入適當的內存管理方法。
- performSelector系列方法所能處理的選擇子太多局限了,選擇子的返回值類型及發送給方法的參數個數都受到限制
- 如果想把任務放另一個線程上執行,那么最好不要用performSelector系列方法,而是應該把任務封裝到塊里,然后調用大中樞派發機制的相關方法來實現。
43.掌握GCD及操作隊列的使用時機
出了GCD之外,還有一種技術叫做NSOperationQueue,它雖然與GCD不同,但是卻與之相關,開發者可以把操作以NSOperation子類的形式放在隊列中,而這些操作也能并發執行。區別:GCD是純C的API,而操作隊列則是Objective-C的對象。在GCD中,任務用塊來表示。用NSOperationQueue類的“addOperationWithBlock:”方法搭配NSBlockOperation類來使用操作隊列,其語法與GCD方式類似。使用NSOperation及NSOperationQueue的好處如下:
- 可以取消某個操作。
- 指定操作間的依賴關系。
- 通過鍵值觀測機制監控NSOperation對象的屬性。
- 指定操作的優先級。
- 重用NSOperation對象
NSNotificationCeter使用的就是操作隊列而非派發隊列
- (id <NSObject>)addObserverForName:(nullable NSNotificationName)name object:(nullable id)obj queue:(nullable NSOperationQueue *)queue usingBlock:(void (^)(NSNotification *note))block
[[NSNotificationCenter defaultCenter] addObserverForName:(nullable NSNotificationName) object:(nullable id) queue:(nullable NSOperationQueue *) usingBlock:^(NSNotification * _Nonnull note) {
}];
要點:
- 在解決多線程與任務管理問題時,派發隊列并非唯一方案。
- 操作隊列提供了一套高層的Objective-C API,能實現純GCD所具備的絕大部分功能,而且還能完成一些更為復雜的操作,那些操作若改用GCD來實現,則需另外編寫代碼
44.通過Dispatch Group機制,根據系統資源狀況來執行任務
GCD常見方法
//創建隊列組
dispatch_group_t group = dispatch_group_create();
//任務編組
//方式一:把待執行的任務塊歸屬某個組
void dispatch_group_async(dispatch_group_t group,
dispatch_queue_t queue,
dispatch_block_t block);
//方式二:進組 與 出組 成對出現
void dispatch_group_enter(dispatch_group_t group);
void dispatch_group_leave(dispatch_group_t group);
//等待dispatch_group執行完畢
//arg0:等待的隊列組 arg1:等待時間 DISPATCH_TIME_FOREVER表示一直等著dispatch_group執行完
//返回類型long,如果執行group所需的時間小于timeout,則返回0,否則返回非0值
long dispatch_group_wait(dispatch_group_t group, dispatch_time_t timeout);
//通知隊列組執行完后在指定的隊列進行回調 與上面的方法相比,在非主隊列中不會阻塞
void dispatch_group_notify(dispatch_group_t group,
dispatch_queue_t queue,
dispatch_block_t block);
//GCD遍歷集合 該方法會持續阻塞,從0開始,直至iterations - 1
void dispatch_apply(size_t iterations, dispatch_queue_t queue,DISPATCH_NOESCAPE void (^block)(size_t));
要點:
- 一系列任務可歸入一個dispatch group中。開發者可以在這組執行完畢時獲得通知。
- 通過dispatch group,可以在并發式派發隊列中同時執行多項任務。此時GCD會根據系統資源來調度這些并發執行的任務。開發者若自己來實現此功能,則需要編寫大量代碼。
45.使用dispatch_once來執行只需運行一次的線程安全代碼
//單例中的dispatch_once使用
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
});
要點:
- 經常需要編寫“只需要執行一次的線程安全代碼”。通過GCD所提供的dispatch_once函數,很容易就能實現此功能。
- 標記應該聲明在static或global作用域中,這樣的話,在把只需執行一次的塊傳給dispatch_once函數時,傳進去的標記也是相同的。
46.不要使用dispatch_get_current_queue
要點:
- dispatch_get_current_queue函數的行為常常與開發者所預期的不同。此函數已經廢棄,只應做調試使用
- 由于派發隊列是按層級來組織的,所以無法單用某個隊列對象來描述“當前隊列”這一概念
- dispatch_get_current_queue函數用于解決由不可重入的代碼所引發的死鎖,然而能用此函數解決的問題,通常也能改用“隊列特定數據”來解決
PDF格式的資料來自iOS開發交流群、感覺作者的貢獻,對于知識的系統歸納總結很有幫助。
編寫高質量代碼的52個有效方法
編寫高質量代碼的52個有效方法(一)—熟悉OC
編寫高質量代碼的52個有效方法(二)—對象、消息、運行期
編寫高質量代碼的52個有效方法(三)—接口與API設計
編寫高質量代碼的52個有效方法(四)—協議與分類
編寫高質量代碼的52個有效方法(五)—內存管理
編寫高質量代碼的52個有效方法(六)—塊與大中樞派發
編寫高質量代碼的52個有效方法(七)---系統框架