在线观看不卡亚洲电影_亚洲妓女99综合网_91青青青亚洲娱乐在线观看_日韩无码高清综合久久

鍍金池/ 教程/ iOS/ 線程安全類的設(shè)計(jì)
與四軸無人機(jī)的通訊
在沙盒中編寫腳本
結(jié)構(gòu)體和值類型
深入理解 CocoaPods
UICollectionView + UIKit 力學(xué)
NSString 與 Unicode
代碼簽名探析
測試
架構(gòu)
第二期-并發(fā)編程
Metal
自定義控件
iOS 中的行為
行為驅(qū)動開發(fā)
Collection View 動畫
截圖測試
MVVM 介紹
使 Mac 應(yīng)用數(shù)據(jù)腳本化
一個(gè)完整的 Core Data 應(yīng)用
插件
字符串
為 iOS 建立 Travis CI
先進(jìn)的自動布局工具箱
動畫
為 iOS 7 重新設(shè)計(jì) App
XPC
從 NSURLConnection 到 NSURLSession
Core Data 網(wǎng)絡(luò)應(yīng)用實(shí)例
GPU 加速下的圖像處理
自定義 Core Data 遷移
子類
與調(diào)試器共舞 - LLDB 的華爾茲
圖片格式
并發(fā)編程:API 及挑戰(zhàn)
IP,TCP 和 HTTP
動畫解釋
響應(yīng)式 Android 應(yīng)用
初識 TextKit
客戶端
View-Layer 協(xié)作
回到 Mac
Android
Core Image 介紹
自定義 Formatters
Scene Kit
調(diào)試
項(xiàng)目介紹
Swift 的強(qiáng)大之處
測試并發(fā)程序
Android 通知中心
調(diào)試:案例學(xué)習(xí)
從 UIKit 到 AppKit
iOS 7 : 隱藏技巧和變通之道
安全
底層并發(fā) API
消息傳遞機(jī)制
更輕量的 View Controllers
用 SQLite 和 FMDB 替代 Core Data
字符串解析
終身學(xué)習(xí)的一代人
視頻
Playground 快速原型制作
Omni 內(nèi)部
同步數(shù)據(jù)
設(shè)計(jì)優(yōu)雅的移動游戲
繪制像素到屏幕上
相機(jī)與照片
音頻 API 一覽
交互式動畫
常見的后臺實(shí)踐
糟糕的測試
避免濫用單例
數(shù)據(jù)模型和模型對象
Core Data
字符串本地化
View Controller 轉(zhuǎn)場
照片框架
響應(yīng)式視圖
Square Register 中的擴(kuò)張
DTrace
基礎(chǔ)集合類
視頻工具箱和硬件加速
字符串渲染
讓東西變得不那么糟
游戲中的多點(diǎn)互聯(lián)
iCloud 和 Core Data
Views
虛擬音域 - 聲音設(shè)計(jì)的藝術(shù)
導(dǎo)航應(yīng)用
線程安全類的設(shè)計(jì)
置換測試: Mock, Stub 和其他
Build 工具
KVC 和 KVO
Core Image 和視頻
Android Intents
在 iOS 上捕獲視頻
四軸無人機(jī)項(xiàng)目
Mach-O 可執(zhí)行文件
UI 測試
值對象
活動追蹤
依賴注入
Swift
項(xiàng)目管理
整潔的 Table View 代碼
Swift 方法的多面性
為什么今天安全仍然重要
Core Data 概述
Foundation
Swift 的函數(shù)式 API
iOS 7 的多任務(wù)
自定義 Collection View 布局
測試 View Controllers
訪談
收據(jù)驗(yàn)證
數(shù)據(jù)同步
自定義 ViewController 容器轉(zhuǎn)場
游戲
調(diào)試核對清單
View Controller 容器
學(xué)無止境
XCTest 測試實(shí)戰(zhàn)
iOS 7
Layer 中自定義屬性的動畫
第一期-更輕量的 View Controllers
精通 iCloud 文檔存儲
代碼審查的藝術(shù):Dropbox 的故事
GPU 加速下的圖像視覺
Artsy
照片擴(kuò)展
理解 Scroll Views
使用 VIPER 構(gòu)建 iOS 應(yīng)用
Android 中的 SQLite 數(shù)據(jù)庫支持
Fetch 請求
導(dǎo)入大數(shù)據(jù)集
iOS 開發(fā)者的 Android 第一課
iOS 上的相機(jī)捕捉
語言標(biāo)簽
同步案例學(xué)習(xí)
依賴注入和注解,為什么 Java 比你想象的要好
編譯器
基于 OpenCV 的人臉識別
玩轉(zhuǎn)字符串
相機(jī)工作原理
Build 過程

線程安全類的設(shè)計(jì)

這篇文章將專注于實(shí)用技巧,設(shè)計(jì)模式,以及對于寫出線程安全類和使用 GCD 來說所特別需要注意的一些反面模式。

線程安全

Apple 的框架

首先讓我們來看看 Apple 的框架。一般來說除非特別聲明,大多數(shù)的類默認(rèn)都不是線程安全的。對于其中的一些類來說,這是很合理的,但是對于另外一些來說就很有趣了。

就算是在經(jīng)驗(yàn)豐富的 iOS/Mac 開發(fā)者,也難免會犯從后臺線程去訪問 UIKit/AppKit 這種錯(cuò)誤。比如因?yàn)閳D片的內(nèi)容本身就是從后臺的網(wǎng)絡(luò)請求中獲取的話,順手就在后臺線程中設(shè)置了 image 之類的屬性,這樣的錯(cuò)誤其實(shí)是屢見不鮮的。Apple 的代碼都經(jīng)過了性能的優(yōu)化,所以即使你從別的線程設(shè)置了屬性的時(shí)候,也不會產(chǎn)生什么警告。

在設(shè)置圖片這個(gè)例子中,癥結(jié)其實(shí)是你的改變通常要過一會兒才能生效。但是如果有兩個(gè)線程在同時(shí)對圖片進(jìn)行了設(shè)定,那么很可能因?yàn)楫?dāng)前的圖片被釋放兩次,而導(dǎo)致應(yīng)用崩潰。這種行為是和時(shí)機(jī)有關(guān)系的,所以很可能在開發(fā)階段沒有崩潰,但是你的用戶使用時(shí)卻不斷 crash。

現(xiàn)在沒有官方的用來尋找類似錯(cuò)誤的工具,但我們確實(shí)有一些技巧來避免這個(gè)問題。UIKit Main Thread Guard 是一段用來監(jiān)視每一次對 setNeedsLayoutsetNeedsDisplay 的調(diào)用代碼,并檢查它們是否是在主線程被調(diào)用的。因?yàn)檫@兩個(gè)方法在 UIKit 的 setter (包括 image 屬性)中廣泛使用,所以它可以捕獲到很多線程相關(guān)的錯(cuò)誤。雖然這個(gè)小技巧并不包含任何私有 API, 但我們還是不建議將它是用在發(fā)布產(chǎn)品中,不過在開發(fā)過程中使用的話還是相當(dāng)贊的。

Apple沒有把 UIKit 設(shè)計(jì)為線程安全的類是有意為之的,將其打造為線程安全的話會使很多操作變慢。而事實(shí)上 UIKit 是和主線程綁定的,這一特點(diǎn)使得編寫并發(fā)程序以及使用 UIKit 十分容易的,你唯一需要確保的就是對于 UIKit 的調(diào)用總是在主線程中來進(jìn)行。

為什么 UIKit 不是線程安全的?

對于一個(gè)像 UIKit 這樣的大型框架,確保它的線程安全將會帶來巨大的工作量和成本。將 non-atomic 的屬性變?yōu)?atomic 的屬性只不過是需要做的變化里的微不足道的一小部分。通常來說,你需要同時(shí)改變?nèi)舾蓚€(gè)屬性,才能看到它所帶來的結(jié)果。為了解決這個(gè)問題,蘋果可能不得不提供像 Core Data 中的 performBlock:performBlockAndWait: 那樣類似的方法來同步變更。另外你想想看,絕大多數(shù)對 UIKit 類的調(diào)用其實(shí)都是以配置為目的的,這使得將 UIKit 改為線程安全這件事情更顯得毫無意義了。

然而即使是那些與配置共享的內(nèi)部狀態(tài)之類事情無關(guān)的調(diào)用,其實(shí)也不是線程安全的。如果你做過 iOS 3.2 或之前的黑暗年代的 app 開發(fā)的話,你肯定有過一邊在后臺準(zhǔn)備圖像時(shí)一邊使用 NSString 的 drawInRect:withFont: 時(shí)的隨機(jī)崩潰的經(jīng)歷。值得慶幸的事,在 iOS 4 中 蘋果將大部分繪圖的方法和諸如 UIColorUIFont 這樣的類改寫為了后臺線程可用

但不幸的是 Apple 在線程安全方面的文檔是極度匱乏的。他們推薦只訪問主線程,并且甚至是繪圖方法他們都沒有明確地表示保證線程安全。因此在閱讀文檔的同時(shí),去讀讀 iOS 版本更新說明會是一個(gè)很好的選擇。

對于大多數(shù)情況來說,UIKit 類確實(shí)只應(yīng)該用在應(yīng)用的主線程中。這對于那些繼承自 UIResponder 的類以及那些操作你的應(yīng)用的用戶界面的類來說,不管如何都是很正確的。

內(nèi)存回收 (deallocation) 問題

另一個(gè)在后臺使用 UIKit 對象的的危險(xiǎn)之處在于“內(nèi)存回收問題”。Apple 在技術(shù)筆記 TN2109 中概述了這個(gè)問題,并提供了多種解決方案。這個(gè)問題其實(shí)是要求 UI 對象應(yīng)該在主線程中被回收,因?yàn)樵谒鼈兊?dealloc 方法被調(diào)用回收的時(shí)候,可能會去改變 view 的結(jié)構(gòu)關(guān)系,而如我們所知,這種操作應(yīng)該放在主線程來進(jìn)行。

因?yàn)檎{(diào)用者被其他線程持有是非常常見的(不管是由于 operation 還是 block 所導(dǎo)致的),這也是很容易犯錯(cuò)并且難以被修正的問題。在 AFNetworking 中也一直長久存在這樣的 bug,但是由于其自身的隱蔽性而鮮為人知,也很難重現(xiàn)其所造成的崩潰。在異步的 block 或者操作中一致使用 __weak,并且不去直接訪問局部變量會對避開這類問題有所幫助。

Collection 類

Apple 有一個(gè)針對 iOS 和 Mac 的很好的總覽性文檔,為大多數(shù)基本的 foundation 類列舉了其線程安全特性??偟膩碚f,比如 NSArry 這樣不可變類是線程安全的。然而它們的可變版本,比如 NSMutableArray 是線程不安全的。事實(shí)上,如果是在一個(gè)隊(duì)列中串行地進(jìn)行訪問的話,在不同線程中使用它們也是沒有問題的。要記住的是即使你申明了返回類型是不可變的,方法里還是有可能返回的其實(shí)是一個(gè)可變版本的 collection 類。一個(gè)好習(xí)慣是寫類似于 return [array copy] 這樣的代碼來確保返回的對象事實(shí)上是不可變對象。

與和[Java]()這樣的語言不一樣,F(xiàn)oundation 框架并不提供直接可用的 collection 類,這是有其道理的,因?yàn)榇蠖鄶?shù)情況下,你想要的是在更高層級上的鎖,以避免太多的加解鎖操作。但緩存是一個(gè)值得注意的例外,iOS 4 中 Apple 添加的 NSCache 使用一個(gè)可變的字典來存儲不可變數(shù)據(jù),它不僅會對訪問加鎖,更甚至在低內(nèi)存情況下會清空自己的內(nèi)容。

也就是說,在你的應(yīng)用中存在可變的且線程安全的字典是可以做到的。借助于 class cluster 的方式,我們也很容易寫出這樣的代碼

原子屬性 (Atomic Properties)

你曾經(jīng)好奇過 Apple 是怎么處理 atomic 的設(shè)置/讀取屬性的么?至今為止,你可能聽說過自旋鎖 (spinlocks),信標(biāo) (semaphores),鎖 (locks),@synchronized 等,Apple 用的是什么呢?因?yàn)?Objctive-C 的 runtime 是開源的,所以我們可以一探究竟。

一個(gè)非原子的 setter 看起來是這個(gè)樣子的:

- (void)setUserName:(NSString *)userName {
      if (userName != _userName) {
          [userName retain];
          [_userName release];
          _userName = userName;
      }
}

這是一個(gè)手動 retain/release 的版本,ARC 生成的代碼和這個(gè)看起來也是類似的。當(dāng)我們看這段代碼時(shí),顯而易見要是 setUserName: 被并發(fā)調(diào)用的話會造成麻煩。我們可能會釋放 _userName 兩次,這回使內(nèi)存錯(cuò)誤,并且導(dǎo)致難以發(fā)現(xiàn)的 bug。

對于任何沒有手動實(shí)現(xiàn)的屬性,編譯器都會生成一個(gè) objc_setProperty_non_gc(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy) 的調(diào)用。在我們的例子中,這個(gè)調(diào)用的參數(shù)是這樣的:

objc_setProperty_non_gc(self, _cmd, 
  (ptrdiff_t)(&_userName) - (ptrdiff_t)(self), userName, NO, NO);`

ptrdiff_t 可能會嚇到你,但是實(shí)際上這就是一個(gè)簡單的指針?biāo)阈g(shù),因?yàn)槠鋵?shí) Objective-C 的類僅僅只是 C 結(jié)構(gòu)體而已。

objc_setProperty 調(diào)用的是如下方法:

static inline void reallySetProperty(id self, SEL _cmd, id newValue, 
  ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy) 
{
    id oldValue;
    id *slot = (id*) ((char*)self + offset);

    if (copy) {
        newValue = [newValue copyWithZone:NULL];
    } else if (mutableCopy) {
        newValue = [newValue mutableCopyWithZone:NULL];
    } else {
        if (*slot == newValue) return;
        newValue = objc_retain(newValue);
    }

    if (!atomic) {
        oldValue = *slot;
        *slot = newValue;
    } else {
        spin_lock_t *slotlock = &PropertyLocks[GOODHASH(slot)];
        _spin_lock(slotlock);
        oldValue = *slot;
        *slot = newValue;        
        _spin_unlock(slotlock);
    }

    objc_release(oldValue);
}

除開方法名字很有趣以外,其實(shí)方法實(shí)際做的事情非常直接,它使用了在 PropertyLocks 中的 128 個(gè)自旋鎖中的 1 個(gè)來給操作上鎖。這是一種務(wù)實(shí)和快速的方式,最糟糕的情況下,如果遇到了哈希碰撞,那么 setter 需要等待另一個(gè)和它無關(guān)的 setter 完成之后再進(jìn)行工作。

雖然這些方法沒有定義在任何公開的頭文件中,但我們還是可用手動調(diào)用他們。我不是說這是一個(gè)好的做法,但是知道這個(gè)還是蠻有趣的,而且如果你想要同時(shí)實(shí)現(xiàn)原子屬性自定義的 setter 的話,這個(gè)技巧就非常有用了。

// 手動聲明運(yùn)行時(shí)的方法
extern void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, 
  id newValue, BOOL atomic, BOOL shouldCopy);
extern id objc_getProperty(id self, SEL _cmd, ptrdiff_t offset, 
  BOOL atomic);

#define PSTAtomicRetainedSet(dest, src) objc_setProperty(self, _cmd, 
  (ptrdiff_t)(&dest) - (ptrdiff_t)(self), src, YES, NO) 
#define PSTAtomicAutoreleasedGet(src) objc_getProperty(self, _cmd, 
  (ptrdiff_t)(&src) - (ptrdiff_t)(self), YES)

參考這個(gè) gist 來獲取包含處理結(jié)構(gòu)體的完整的代碼,但是我們其實(shí)并不推薦使用它。

為何不用 @synchronized ?

你也許會想問為什么蘋果不用 @synchronized(self) 這樣一個(gè)已經(jīng)存在的運(yùn)行時(shí)特性來鎖定屬??你可以看看這里的源碼,就會發(fā)現(xiàn)其實(shí)發(fā)生了很多的事情。Apple 使用了最多三個(gè)加/解鎖序列,還有一部分原因是他們也添加了異常開解(exception unwinding)機(jī)制。相比于更快的自旋鎖方式,這種實(shí)現(xiàn)要慢得多。由于設(shè)置某個(gè)屬性一般來說會相當(dāng)快,因此自旋鎖更適合用來完成這項(xiàng)工作。@synchonized(self) 更適合使用在你 需要確保在發(fā)生錯(cuò)誤時(shí)代碼不會死鎖,而是拋出異常的時(shí)候。

你自己的類

單獨(dú)使用原子屬性并不會使你的類變成線程安全。它不能保護(hù)你應(yīng)用的邏輯,只能保護(hù)你免于在 setter 中遭遇到競態(tài)條件的困擾??纯聪旅娴拇a片段:

if (self.contents) {
    CFAttributedStringRef stringRef = CFAttributedStringCreate(NULL, 
      (__bridge CFStringRef)self.contents, NULL);
    // 渲染字符串
}

我之前在 PSPDFKit 中就犯了這個(gè)錯(cuò)誤。時(shí)不時(shí)地應(yīng)用就會因?yàn)?contents 屬性在通過檢查之后卻又被設(shè)成了 nil 而導(dǎo)致 EXC_BAD_ACCESS 崩潰。捕獲這個(gè)變量就可以簡單修復(fù)這個(gè)問題;

NSString *contents = self.contents;
if (contents) {
    CFAttributedStringRef stringRef = CFAttributedStringCreate(NULL, 
      (__bridge CFStringRef)contents, NULL);
    // 渲染字符串
}

在這里這樣就能解決問題,但是大多數(shù)情況下不會這么簡單。想象一下我們還有一個(gè) textColor 的屬性,我們在一個(gè)線程中將兩個(gè)屬性都做了改變。我們的渲染線程有可能使用了新的內(nèi)容,但是依舊保持了舊的顏色,于是我們得到了一組奇怪的組合。這其實(shí)也是為什么 Core Data 要將 model 對象都綁定在一個(gè)線程或者隊(duì)列中的原因。

對于這個(gè)問題,其實(shí)沒有萬用解法。使用 不可變模型是一個(gè)可能的方案,但是它也有自己的問題。另一種途徑是限制對存在在主線程或者某個(gè)特定隊(duì)列中的既存對象的改變,而是先進(jìn)行一次拷貝之后再在工作線程中使用。對于這個(gè)問題的更多對應(yīng)方法,我推薦閱讀 Jonathan Sterling 的關(guān)于 Objective-C 中輕量化不可變對象的文章。

一個(gè)簡單的解決辦法是使用 @synchronize。其他的方式都非常非??赡苁鼓阏`入歧途,已經(jīng)有太多聰明人在這種嘗試上一次又一次地以失敗告終。

可行的線程安全設(shè)計(jì)

在嘗試寫一些線程安全的東西之前,應(yīng)該先想清楚是不是真的需要。確保你要做的事情不會是過早優(yōu)化。如果要寫的東西是一個(gè)類似配置類 (configuration class) 的話,去考慮線程安全這種事情就毫無意義了。更正確的做法是扔一個(gè)斷言上去,以保證它被正確地使用:

void PSPDFAssertIfNotMainThread(void) {
    NSAssert(NSThread.isMainThread, 
      @"Error: Method needs to be called on the main thread. %@", 
      [NSThread callStackSymbols]);
}

對于那些肯定應(yīng)該線程安全的代碼(一個(gè)好例子是負(fù)責(zé)緩存的類)來說,一個(gè)不錯(cuò)的設(shè)計(jì)是使用并發(fā)的 dispatch_queue 作為讀/寫鎖,并且確保只鎖著那些真的需要被鎖住的部分,以此來最大化性能。一旦你使用多個(gè)隊(duì)列來給不同的部分上鎖的話,整件事情很快就會變得難以控制了。

于是你也可以重新組織你的代碼,這樣某些特定的鎖就不再需要了??纯聪旅孢@段實(shí)現(xiàn)了一種多委托的代碼(其實(shí)在大多數(shù)情況下,用 NSNotifications 會更好,但是其實(shí)也還是有多委托的實(shí)用例子)的

// 頭文件
@property (nonatomic, strong) NSMutableSet *delegates;

// init方法中
_delegateQueue = dispatch_queue_create("com.PSPDFKit.cacheDelegateQueue", 
  DISPATCH_QUEUE_CONCURRENT);

- (void)addDelegate:(id<PSPDFCacheDelegate>)delegate {
    dispatch_barrier_async(_delegateQueue, ^{
        [self.delegates addObject:delegate];
    });
}

- (void)removeAllDelegates {
    dispatch_barrier_async(_delegateQueue, ^{
        self.delegates removeAllObjects];
    });
}

- (void)callDelegateForX {
    dispatch_sync(_delegateQueue, ^{
        [self.delegates enumerateObjectsUsingBlock:^(id<PSPDFCacheDelegate> delegate, NSUInteger idx, BOOL *stop) {
            // 調(diào)用delegate
        }];
    });
}

除非 addDelegate: 或者 removeDelegate: 每秒要被調(diào)用上千次,否則我們可以使用一個(gè)相對簡潔的實(shí)現(xiàn)方式:

// 頭文件
@property (atomic, copy) NSSet *delegates;

- (void)addDelegate:(id<PSPDFCacheDelegate>)delegate {
    @synchronized(self) {
        self.delegates = [self.delegates setByAddingObject:delegate];
    }
}

- (void)removeAllDelegates {
    @synchronized(self) {
        self.delegates = nil;
    }
}

- (void)callDelegateForX {
    [self.delegates enumerateObjectsUsingBlock:^(id<PSPDFCacheDelegate> delegate, NSUInteger idx, BOOL *stop) {
        // 調(diào)用delegate
    }];
}

就算這樣,這個(gè)例子還是有點(diǎn)理想化,因?yàn)槠渌丝梢园炎兏拗圃谥骶€程中。但是對于很多數(shù)據(jù)結(jié)構(gòu),可以在可變更操作的方法中創(chuàng)建不可變的拷貝,這樣整體的代碼邏輯上就不再需要處理過多的鎖了。

GCD 的陷阱

對于大多數(shù)上鎖的需求來說,GCD 就足夠好了。它簡單迅速,并且基于 block 的 API 使得粗心大意造成非平衡鎖操作的概率下降了不少。然后,GCD 中還是有不少陷阱,我們在這里探索一下其中的一些。

將 GCD 當(dāng)作遞歸鎖使用

GCD 是一個(gè)對共享資源的訪問進(jìn)行串行化的隊(duì)列。這個(gè)特性可以被當(dāng)作鎖來使用,但實(shí)際上它和 @synchronized 有很大區(qū)別。 GCD隊(duì)列并非是可重入的,因?yàn)檫@將破壞隊(duì)列的特性。很多有試圖使用 dispatch_get_current_queue() 來繞開這個(gè)限制,但是這是一個(gè)糟糕的做法,Apple 在 iOS6 中將這個(gè)方法標(biāo)記為廢棄,自然也是有自己的理由。

// This is a bad idea.
inline void pst_dispatch_sync_reentrant(dispatch_queue_t queue, 
  dispatch_block_t block) 
{
    dispatch_get_current_queue() == queue ? block() 
                                          : dispatch_sync(queue, block);
}

對當(dāng)前的隊(duì)列進(jìn)行測試也許在簡單情況下可以行得通,但是一旦你的代碼變得復(fù)雜一些,并且你可能有多個(gè)隊(duì)列在同時(shí)被鎖住的情況下,這種方法很快就悲劇了。一旦這種情況發(fā)生,幾乎可以肯定的是你會遇到死鎖。當(dāng)然,你可以使用 dispatch_get_specific(),這將截?cái)嗾麄€(gè)隊(duì)列結(jié)構(gòu),從而對某個(gè)特定的隊(duì)列進(jìn)行測試。要這么做的話,你還得為了在隊(duì)列中附加標(biāo)志隊(duì)列的元數(shù)據(jù),而去寫自定義的隊(duì)列構(gòu)造函數(shù)。嘛,最好別這么做。其實(shí)在實(shí)用中,使用 NSRecursiveLock 會是一個(gè)更好的選擇。

用 dispatch_async 修復(fù)時(shí)序問題

在使用 UIKit 的時(shí)候遇到了一些時(shí)序上的麻煩?很多時(shí)候,這樣進(jìn)行“修正”看來非常完美:

dispatch_async(dispatch_get_main_queue(), ^{
    // Some UIKit call that had timing issues but works fine 
    // in the next runloop.
    [self updatePopoverSize];
});

千萬別這么做!相信我,這種做法將會在之后你的 app 規(guī)模大一些的時(shí)候讓你找不著北。這種代碼非常難以調(diào)試,并且你很快就會陷入用更多的 dispatch 來修復(fù)所謂的莫名其妙的"時(shí)序問題"。審視你的代碼,并且找到合適的地方來進(jìn)行調(diào)用(比如在 viewWillAppear 里調(diào)用,而不是 viewDidLoad 之類的)才是解決這個(gè)問題的正確做法。我在自己的代碼中也還留有一些這樣的 hack,但是我為它們基本都做了正確的文檔工作,并且對應(yīng)的 issue 也被一一記錄過。

記住這不是真正的 GCD 特性,而只是一個(gè)在 GCD 下很容易實(shí)現(xiàn)的常見反面模式。事實(shí)上你可以使用 performSelector:afterDelay: 方法來實(shí)現(xiàn)同樣的操作,其中 delay 是在對應(yīng)時(shí)間后的 runloop。

在性能關(guān)鍵的代碼中混用 dispatch_sync 和 dispatch_async

這個(gè)問題我花了好久來研究。在 PSPDFKit 中有一個(gè)使用了 LRU(最久未使用)算法列表的緩存類來記錄對圖片的訪問。當(dāng)你在頁面中滾動時(shí),這個(gè)方法將被調(diào)用非常多次。最初的實(shí)現(xiàn)使用了 dispatch_sync 來進(jìn)行實(shí)際有效的訪問,使用 dispatch_async 來更新 LRU 列表的位置。這導(dǎo)致了幀數(shù)遠(yuǎn)低于原來的 60 幀的目標(biāo)。

當(dāng)你的 app 中的其他運(yùn)行的代碼阻擋了 GCD 線程的時(shí)候,dispatch manager 需要花時(shí)間去尋找能夠執(zhí)行 dispatch_async 代碼的線程,這有時(shí)候會花費(fèi)一點(diǎn)時(shí)間。在找到合適的執(zhí)行線程之前,你的同步調(diào)用就會被 block 住了。其實(shí)在這個(gè)例子中,異步情況的執(zhí)行順序并不是很重要,但沒有能將這件事情告訴 GCD 的好辦法。讀/寫鎖這里并不能起到什么作用,因?yàn)樵诋惒讲僮髦谢旧弦欢〞枰M(jìn)行順序?qū)懭耄诖诉^程中讀操作將被阻塞住。如果誤用了 dispatch_async 代價(jià)將會是非常慘重的。在將它用作鎖的時(shí)候,一定要非常小心。

使用 dispatch_async 來派發(fā)內(nèi)存敏感的操作

我們已經(jīng)談?wù)摿撕芏嚓P(guān)于 NSOperations 的話題了,一般情況下,使用這個(gè)更高層級的 API 會是一個(gè)好主意。當(dāng)你要處理一段內(nèi)存敏感的操作的代碼塊時(shí),這個(gè)優(yōu)勢尤為突出、

在 PSPDFKit 的老版本中,我用了 GCD 隊(duì)列來將已緩存的 JPG 圖片寫到磁盤中。當(dāng) retina 的 iPad 問世之后,這個(gè)操作出現(xiàn)了問題。?因?yàn)榉直媛史读?,相比渲染這張圖片,將它編碼花費(fèi)的時(shí)間要長得多。所以,操作堆積在了隊(duì)列中,當(dāng)系統(tǒng)繁忙時(shí),甚至有可能因?yàn)閮?nèi)存耗盡而崩潰。

我們沒有辦法追蹤有多少個(gè)操作在隊(duì)列中等待運(yùn)行(除非你手動添加了追蹤這個(gè)的代碼),我們也沒有現(xiàn)成的方法來在接收到低內(nèi)存通告的時(shí)候來取消操作、這時(shí)候,切換到 NSOperations 可以使代碼變得容易調(diào)試得多,并且允許我們在不添加手動管理的代碼的情況下,做到對操作的追蹤和取消。

當(dāng)然也有一些不好的地方,比如你不能在你的 NSOperationQueue 中設(shè)置目標(biāo)隊(duì)列(就像 DISPATCH_QUEUE_PRIORITY_BACKGROUND 之于 緩速 I/O 那樣)。但這只是為了可調(diào)試性的一點(diǎn)小代價(jià),而事實(shí)上這也幫助你避免遇到優(yōu)先級反轉(zhuǎn)的問題。我甚至不推薦直接使用已經(jīng)包裝好的 NSBlockOperation 的 API,而是建議使用一個(gè) NSOperation 的真正的子類,包括實(shí)現(xiàn)其 description。誠然,這樣做工作量會大一些,但是能輸出所有運(yùn)行中/準(zhǔn)備運(yùn)行的操作是及其有用的。