這篇文章將專注於適用技巧,設計形式,以及關於寫出線程平安類和運用 GCD 來說所特別需求留意的一些背面形式。
線程平安 Apple 的框架首先讓我們來看看 Apple 的框架。普通來說除非特別聲明,大少數的類默許都不是線程平安的。關於其中的一些類來說,這是很合理的,但是關於另外一些來說就很風趣了。
就算是在經歷豐厚的 IOS/Mac 開發者,也難免會犯從後台線程去訪問 UIKit/AppKit 這種錯誤。比方由於圖片的內容自身就是從後台的網絡懇求中獲取的話,隨手就在後台線程中設置了 image
之類的屬性,這樣的錯誤其實是屢見不鮮的。Apple 的代碼都經過了功能的優化,所以即便你從別的線程設置了屬性的時分,也不會發生什麼正告。
在設置圖片這個例子中,症結其實是你的改動通常要過一會兒才干失效。但是假如有兩個線程在同時對圖片停止了設定,那麼很能夠由於以後的圖片被釋放兩次,而招致使用解體。這種行為是和機遇有關系的,所以很能夠在開發階段沒有解體,但是你的用戶運用時卻不時 crash。
如今沒有官方的用來尋覓相似錯誤的工具,但我們的確有一些技巧來防止這個問題。UIKit Main Thread Guard 是一段用來監視每一次對 setNeedsLayout
和 setNeedsDisplay
的調用代碼,並反省它們能否是在主線程被調用的。由於這兩個辦法在 UIKit 的 setter (包括 image 屬性)中普遍運用,所以它可以捕捉到很多線程相關的錯誤。雖然這個小技巧並不包括任何公有 API, 但我們還是不建議將它是用在發布產品中,不過在開發進程中運用的話還是相當贊的。
Apple沒有把 UIKit 設計為線程平安的類是有意為之的,將其打造為線程平安的話會使很多操作變慢。而現實上 UIKit 是和主線程綁定的,這一特點使得編寫並發順序以及運用 UIKit 非常容易的,你獨一需求確保的就是關於 UIKit 的調用總是在主線程中來停止。
為什麼 UIKit 不是線程平安的?關於一個像 UIKit 這樣的大型框架,確保它的線程平安將會帶來宏大的任務量和本錢。將 non-atomic 的屬性變為 atomic 的屬性只不過是需求做的變化裡的微乎其微的一小局部。通常來說,你需求同時改動若干個屬性,才干看到它所帶來的後果。為理解決這個問題,蘋果能夠不得不提供像 Core Data 中的 performBlock:
和 performBlockAndWait:
那樣相似的辦法來同步變卦。另外你想想看,絕大少數對 UIKit 類的調用其實都是以配置為目的的,這使得將 UIKit 改為線程平安這件事情更顯得毫有意義了。
但是即便是那些與配置共享的外部形態之類事情有關的調用,其實也不是線程平安的。假如你做過 IOS 3.2 或之前的黑暗年代的 app 開發的話,你一定有過一邊在後台預備圖像時一邊運用 NSString 的 draWinRect:withFont:
時的隨機解體的閱歷。值得慶幸的事,在 IOS 4 中 蘋果將大局部繪圖的辦法和諸如 UIColor
和 UIFont
這樣的類改寫為了後台線程可用。
但不幸的是 Apple 在線程平安方面的文檔是極度匮乏的。他們引薦只訪問主線程,並且甚至是繪圖辦法他們都沒有明白地表示保證線程平安。因而在閱讀文檔的同時,去讀讀 iOS 版本更新闡明會是一個很好的選擇。
關於大少數狀況來說,UIKit 類的確只應該用在使用的主線程中。這關於那些承繼自 UIResponder 的類以及那些操作你的使用的用戶界面的類來說,不論如何都是很正確的。
內存回收 (deallocation) 問題另一個在後台運用 UIKit 對象的的風險之處在於“內存回收問題”。Apple 在技術筆記 TN2109 中概述了這個問題,並提供了多種處理方案。這個問題其實是要求 UI 對象應該在主線程中被回收,由於在它們的 dealloc
辦法被調用回收的時分,能夠會去改動 view 的構造關系,而如我們所知,這種操作應該放在主線程來停止。
由於調用者被其他線程持有是十分罕見的(不論是由於 operation 還是 block 所招致的),這也是很容易犯錯並且難以被修正的問題。在 A.networking 中也不斷持久存在這樣的 bug,但是由於其本身的蔭蔽性而不為人知,也很難重現其所形成的解體。在異步的 block 或許操作中分歧運用 __weak
,並且不去直接訪問部分變量會對避開這類問題有所協助。
Apple 有一個針對 iOS 和 Mac 的很好的總覽性文檔,為大少數根本的 foundation 類羅列了其線程平安特性。總的來說,比方 NSArry
這樣不可變類是線程平安的。但是它們的可變版本,比方 NSMutableArray
是線程不平安的。現實上,假如是在一個隊列中串行地停止訪問的話,在不同線程中運用它們也是沒有問題的。要記住的是即便你聲明了前往類型是不可變的,辦法裡還是有能夠前往的其實是一個可變版本的 collection 類。一個好習氣是寫相似於 return [array copy]
這樣的代碼來確保前往的對象現實上是不可變對象。
與和Java這樣的言語不一樣,Foundation 框架並不提供直接可用的 collection 類,這是有其道理的,由於大少數狀況下,你想要的是在更高層級上的鎖,以防止太多的加解鎖操作。但緩存是一個值得留意的例外,iOS 4 中 Apple 添加的 NSCache
運用一個可變的字典來存儲不可變數據,它不只會對訪問加鎖,更甚至在低內存狀況下會清空自己的內容。
也就是說,在你的使用中存在可變的且線程平安的字典是可以做到的。借助於 class cluster 的方式,我們也很容易寫出這樣的代碼。
原子屬性 (Atomic Properties)你已經獵奇過 Apple 是怎樣處置 atomic 的設置/讀取屬性的麼?至今為止,你能夠聽說過自旋鎖 (spinlocks),信標 (semaphores),鎖 (locks),@synchronized 等,Apple 用的是什麼呢?由於 Objctive-C 的 runtime 是開源的,所以我們可以一探求竟。
一個非原子的 setter 看起來是這個樣子的:
- (void)setUserName:(NSString *)userName {
if (userName != _userName) {
[userName retain];
[_userName release];
_userName = userName;
}
}
這是一個手動 retain/release 的版本,ARC 生成的代碼和這個看起來也是相似的。當我們看這段代碼時,不言而喻要是 setUserName:
被並發調用的話會形成費事。我們能夠會釋放 _userName
兩次,這回使內存錯誤,並且招致難以發現的 bug。
關於任何沒有手動完成的屬性,編譯器都會生成一個 objc_setProperty_non_gc(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy)
的調用。在我們的例子中,這個調用的參數是這樣的:
objc_setProperty_non_gc(self, _cmd,
(ptrdiff_t)(&_userName) - (ptrdiff_t)(self), userName, NO, NO);`
ptrdiff_t
能夠會嚇到你,但是實踐上這就是一個復雜的指針算術,由於其實 Objective-C 的類僅僅只是 C 構造體而已。
objc_setProperty
調用的是如下辦法:
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);
}
除開辦法名字很風趣以外,其實辦法實踐做的事情十分直接,它運用了在 PropertyLocks
中的 128 個自旋鎖中的 1 個來給操作上鎖。這是一種務虛和疾速的方式,最蹩腳的狀況下,假如遇到了哈希碰撞,那麼 setter 需求等候另一個和它有關的 setter 完成之後再停止任務。
雖然這些辦法沒有定義在任何地下的頭文件中,但我們還是可用手動調用他們。我不是說這是一個好的做法,但是知道這個還是蠻風趣的,而且假如你想要同時完成原子屬性和自定義的 setter 的話,這個技巧就十分有用了。
// 手動聲明運轉時的辦法
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)
參考這個 gist 來獲取包括處置構造體的完好的代碼,但是我們其實並不引薦運用它。
為何不必 @synchronized ?你也許會想問為什麼蘋果不必 @synchronized(self)
這樣一個曾經存在的運轉時特性來鎖定屬??你可以看看這裡的源碼,就會發現其實發作了很多的事情。Apple 運用了最多三個加/解鎖序列,還有一局部緣由是他們也添加了異常開解(exception unWinding)機制。相比於更快的自旋鎖方式,這種完成要慢得多。由於設置某個屬性普通來說會相當快,因而自旋鎖更合適用來完成這項任務。@synchonized(self)
更合適運用在你
需求確保在發作錯誤時代碼不會死鎖,而是拋出異常的時分。
獨自運用原子屬性並不會使你的類變成線程平安。它不能維護你使用的邏輯,只能維護你免於在 setter 中遭遇到競態條件的困擾。看看上面的代碼片段:
if (self.contents) {
CFAttributedStringRef stringRef = CFAttributedStringCreate(NULL,
(__bridge CFStringRef)self.contents, NULL);
// 渲染字符串
}
我之前在 PSPDFKit 中就犯了這個錯誤。時不時地使用就會由於 contents
屬性在經過反省之後卻又被設成了 nil 而招致 EXC_BAD_Access 解體。捕捉這個變量就可以復雜修復這個問題;
NSString *contents = self.contents;
if (contents) {
CFAttributedStringRef stringRef = CFAttributedStringCreate(NULL,
(__bridge CFStringRef)contents, NULL);
// 渲染字符串
}
在這裡這樣就能處理問題,但是大少數狀況下不會這麼復雜。想象一下我們還有一個 textColor
的屬性,我們在一個線程中將兩個屬性都做了改動。我們的渲染線程有能夠運用了新的內容,但是照舊堅持了舊的顏色,於是我們失掉了一組奇異的組合。這其實也是為什麼 Core Data 要將 model 對象都綁定在一個線程或許隊列中的緣由。
關於這個問題,其實沒有萬用解法。運用 不可變模型是一個能夠的方案,但是它也有自己的問題。另一種途徑是限制對存在在主線程或許某個特定隊列中的既存對象的改動,而是先停止一次拷貝之後再在任務線程中運用。關於這個問題的更多對應辦法,我引薦閱讀 Jonathan Sterling 的關於 Objective-C 中輕量化不可變對象的文章。
一個復雜的處理方法是運用 @synchronize
。其他的方式都十分十分能夠使你迷途知返,曾經有太多聰明人在這種嘗試上一次又一次地以失敗告終。
在嘗試寫一些線程平安的東西之前,應該先想清楚是不是真的需求。確保你要做的事情不會是過早優化。假如要寫的東西是一個相似配置類 (configuration class) 的話,去思索線程平安這種事情就毫有意義了。更正確的做法是扔一個斷言上去,以保證它被正確地運用:
void PSPDFAssertIfNotMainThread(void) {
NSAssert(NSThread.isMainThread,
@"Error: Method needs to be called on the main thread. %@",
[NSThread callStackSymbols]);
}
關於那些一定應該線程平安的代碼(一個好例子是擔任緩存的類)來說,一個不錯的設計是運用並發的 dispatch_queue
作為讀/寫鎖,並且確保只鎖著那些真的需求被鎖住的局部,以此來最大化功能。一旦你運用多個隊列來給不同的局部上鎖的話,整件事情很快就會變得難以控制了。
於是你也可以重新組織你的代碼,這樣某些特定的鎖就不再需求了。看看上面這段完成了一種多委托的代碼(其真實大少數狀況下,用 NSNotifications 會更好,但是其實也還是有多委托的適用例子)的
// 頭文件
@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) {
// 調用delegate
}];
});
}
除非 addDelegate:
或許 removeDelegate:
每秒要被調用上千次,否則我們可以運用一個絕對簡約的完成方式:
// 頭文件
@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) {
// 調用delegate
}];
}
就算這樣,這個例子還是有點理想化,由於其別人可以把變卦限制在主線程中。但是關於很少數據構造,可以在可變卦操作的辦法中創立不可變的拷貝,這樣全體的代碼邏輯上就不再需求處置過多的鎖了。
GCD 的圈套關於大少數上鎖的需求來說,GCD 就足夠好了。它復雜迅速,並且基於 block 的 API 使得粗枝大葉形成非均衡鎖操作的概率下降了不少。然後,GCD 中還是有不少圈套,我們在這裡探究一下其中的一些。
將 GCD 當作遞歸鎖運用GCD 是一個對共享資源的訪問停止串行化的隊列。這個特性可以被當作鎖來運用,但實踐上它和 @synchronized
有很大區別。 GCD隊列並非是可重入的,由於這將毀壞隊列的特性。很多有試圖運用 dispatch_get_current_queue()
來繞開這個限制,但是這是一個蹩腳的做法,Apple 在 iOS6 中將這個辦法標志為廢棄,自然也是有自己的理由。
// 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);
}
對以後的隊列停止測試也許在復雜狀況下可以行得通,但是一旦你的代碼變得復雜一些,並且你能夠有多個隊列在同時被鎖住的狀況下,這種辦法很快就喜劇了。一旦這種狀況發作,簡直可以一定的是你會遇到死鎖。當然,你可以運用 dispatch_get_specific()
,這將截斷整個隊列構造,從而對某個特定的隊列停止測試。要這麼做的話,你還得為了在隊列中附加標志隊列的元數據,而去寫自定義的隊列結構函數。嘛,最好別這麼做。其真實適用中,運用 NSRecursiveLock
會是一個更好的選擇。
在運用 UIKit 的時分遇到了一些時序上的費事?很多時分,這樣停止“修正”看來十分完滿:
dispatch_async(dispatch_get_main_queue(), ^{
// Some UIKit call that had timing issues but works fine
// in the next runloop.
[self updatePopoverSize];
});
千萬別這麼做!置信我,這種做法將會在之後你的 app 規模大一些的時分讓你找不著北。這種代碼十分難以調試,並且你很快就會墮入用更多的 dispatch 來修復所謂的莫明其妙的”時序問題”。審視你的代碼,並且找到適宜的中央來停止調用(比方在 viewWillAppear 裡調用,而不是 viewDidLoad 之類的)才是處理這個問題的正確做法。我在自己的代碼中也還留有一些這樣的 hack,但是我為它們根本都做了正確的文檔任務,並且對應的 issue 也被逐個記載過。
記住這不是真正的 GCD 特性,而只是一個在 GCD 下很容易完成的罕見背面形式。現實上你可以運用 performSelector:afterDelay:
辦法來完成異樣的操作,其中 delay 是在對應時間後的 runloop。
這個問題我花了良久來研討。在 PSPDFKit 中有一個運用了 LRU(最久未運用)算法列表的緩存類來記載對圖片的訪問。當你在頁面中滾動時,這個辦法將被調用十分屢次。最初的完成運用了 dispatch_sync
來停止實踐無效的訪問,運用 dispatch_async
來更新 LRU 列表的地位。這招致了幀數遠低於原來的 60 幀的目的。
當你的 app 中的其他運轉的代碼阻撓了 GCD 線程的時分,dispatch manager 需求花時間去尋覓可以執行 dispatch_async 代碼的線程,這有時分會破費一點時間。在找到適宜的執行線程之前,你的同步伐用就會被 block 住了。其真實這個例子中,異步狀況的執行順序並不是很重要,但沒有能將這件事情通知 GCD 的好方法。讀/寫鎖這裡並不能起到什麼作用,由於在異步操作中根本上一定會需求停止順序寫入,而在此進程中讀操作將被阻塞住。假如誤用了 dispatch_async
代價將會是十分沉重的。在將它用作鎖的時分,一定要十分小心。
我們曾經議論了很多關於 NSOperations 的話題了,普通狀況下,運用這個更高層級的 API 會是一個好主見。當你要處置一段內存敏感的操作的代碼塊時,這個優勢尤為突出、
在 PSPDFKit 的老版本中,我用了 GCD 隊列來將已緩存的 JPG 圖片寫到磁盤中。當 retina 的 iPad 問世之後,這個操作呈現了問題。ß由於分辨率翻倍了,相比渲染這張圖片,將它編碼破費的時間要長得多。所以,操作堆積在了隊列中,當零碎忙碌時,甚至有能夠由於內存耗盡而解體。
我們沒有方法追蹤有多少個操作在隊列中等候運轉(除非你手動添加了追蹤這個的代碼),我們也沒有現成的辦法來在接納到低內存通告的時分來取消操作、這時分,切換到 NSOperations 可以使代碼變得容易調試得多,並且允許我們在不添加手動管理的代碼的狀況下,做到對操作的追蹤和取消。
當然也有一些不好的中央,比方你不能在你的 NSOperationQueue
中設置目的隊列(好像 DISPATCH_QUEUE_PRIORITY_BACKGROUND
之於 緩速 I/O 那樣)。但這只是為了可調試性的一點小代價,而現實上這也協助你防止遇到優先級反轉的問題。我甚至不引薦直接運用曾經包裝好的 NSBlockOperation
的 API,而是建議運用一個 NSOperation 的真正的子類,包括完成其 description。固然,這樣唱工作量會大一些,但是能輸入一切運轉中/預備運轉的操作是及其有用的。
原文 Thread-Safe Class Design
【objc.io 2.4 GCD平安 (轉)】的相關資料介紹到這裡,希望對您有所幫助! 提示:不會對讀者因本文所帶來的任何損失負責。如果您支持就請把本站添加至收藏夾哦!