系列:如何不閃退之執行緒

zaishaoyi發表於2015-08-18

如何不閃退

一個簡單的準則就是:一切都放在主執行緒中進行。現如今的機器裝置處理速度已經非常快,以致於在主執行緒中你可以做的遠比你想象中的要多。

一個不需要考慮併發的世界就是天堂,因為沒有了併發會引起的問題

但是……

我是一個效能迷,或者,更確切一點,我是一個使用者體驗主義者。在我看來最糟糕的事情就是執行緩慢和明顯阻塞主執行緒。千萬不要這樣做。

後面我會講述如何避免這個問題。現在讓我們從主執行緒講起。

主執行緒準則

我編寫的所有程式碼都應當並且只能在主執行緒執行,當然也有些例外(我們稍後會講一下這些例外)。

這樣做可以解決很多問題。例如,我在之前的部落格中寫到過在dealloc中移除訊息註冊。有一些人指出,你無法確定dealloc在哪一個執行緒中被呼叫。但實際上你是可以的,只要物件只在主執行緒工作,並且也只在主執行緒被引用。

這也意味著任何KVO變化都會在主執行緒發出,所有觀察者都在主執行緒中,並且期望通知在主執行緒傳遞過來。

無需處理併發的好處非常多。我強烈建議用這種方式寫你的app,然後測試是否有阻塞主執行緒的現象存在。如果沒有,那就棒極了,你可以提交你的 APP 了(當然,應該使用適當大規模的資料來測試。)

擁有自己獨立空間的物件

當且僅當你發現主執行緒發生了明顯的阻塞,這時你就應當想辦法解決阻塞。

第一個考慮的物件是那些可以獨立於其他部分的轉換過程。我會用解析JSON的例子說明。

當我從伺服器獲取到JSON資料,我喜歡把它轉換成之後會合並進 model 的中間物件,原因如下:

  1. 我不想讓model 物件直接處理JSON資料。
  2. 我想在其他物件使用到這個資料之前,完成處理NSNull值,資料轉換,以及其他轉換工作。

所以我用NSOperationQueue或者GCD 佇列(最近經常使用後者)將伺服器返回的NSData轉換成中間物件。(一定要使用佇列。不要使用 detachThreadSelector或者performSelectorInBackground。

一次只能有一個執行緒訪問這些中間物件。他們是由後臺執行緒建立,之後傳遞到主執行緒,主執行緒用它們來更新model,然後將他們刪除

由於在這些中間物件的生命週期中,會有不同的執行緒引用它們,所以我要確保這些物件對於除了它們自身和它們的初始化資料之外的世界一無所知。一旦在佇列中被建立,它們將是不可變的。它們不會成為觀察者也不會成為被觀察物件(畢竟它們是不可變的)。

不可變物件是執行緒安全的,因此這些中間物件也是執行緒安全的。然而,沒有必要強調執行緒安全,因為我們討論的重點在於一次只有一個執行緒訪問的情況下它們是安全的,而不是同時有多個執行緒同時訪問的情況。

多個物件

有時,一些物件要放在一起工作。拋開JSON的例子,考慮一下RSS解析器。在這個例子中,包含三個主要的物件:SAX 解析封裝器,它的委託物件,以及委託物件建立的中間物件。(理論上來說應該與上面例子中的物件相似)

SAX 解析封裝器和他的委託物件在整個操作期間都存活。儘管程式碼執行在一個獨立的執行緒上,他們不需要是執行緒安全的。因為它們只會被那一個執行緒訪問。執行期間,它們不瞭解外界,外界對它們也是一無所知。

  1. SAX parser wrapper瞭解初始化它的NSData,並且知道自己有一個委託物件。
  2. SAX解析器代理了解自己建立的中間物件。
  3. 中間物件什麼都不瞭解。

這些物件在一起工作,但重要的是,它們不會使用KVO或者是通知,而是使用委託模式。(理論上來說通過block還是委託方法實現並不重要)

這些物件在一起工作,但是在保持整體獨立的同時,這些物件內部也要儘可能低耦合。

在最後,只有中間資料物件還存在——他們被傳遞給主執行緒,主執行緒使用它們來更新model,然後將它們刪除

最差的情景

我多次用到了這個詞“更新 model ”,並且提到要在主執行緒中做這件事。幾年之前,我還無法想象可以這麼做——但是隨著計算機和裝置執行越來越快,我們應當首先考慮只在主執行緒工作,對於那些可以並且能夠被安全加入到另一個佇列中的任務,再考慮其它執行緒。

真的不要試圖在後臺程式中更新model物件。因為這很容易產生崩潰。但是在進行testingprofiling時可以會提示你需要在後臺執行緒更新 model

我們試著分解這個問題。如果更新model一般不會出現問題,除了涉及到一類操作——例如,涉及到將NSData轉換為UIImage或者NSImage的操作——那麼把處理慢的部分作為後臺任務就可以了。(利用資料或者檔案建立影像是最適合在主執行緒之外做的,因為這個操作很容易分離出來)

還有可能出問題的是資料庫:或許你發現在記憶體中建立物件或者更新屬性並不快,甚至非常慢。在這種情況下,你可能採用和我一樣的做法,將資料庫呼叫從主執行緒中分離出來(這並不困難:資料庫程式碼需要在序列後臺佇列執行,而且應該精確按照主執行緒中事件的發生順序處理一切問題。)

也就是說:有不同的方案可選。

如果你還是覺得你必須在後臺執行緒中更新model,那麼你就只能那麼做了。記住,你app中剩餘的部分在主執行緒執行,所以發出通知等等的事情,要在主執行緒中進行。

總結

一切都在主執行緒中處理。不考慮佇列和後臺執行緒。享受在天堂的感覺。

如果testing和profiling之後,你發現確實需要把一些事情移動到後臺執行緒中,那麼把那些獨立的模組移走,並且保證它們能夠很好的獨立。使用代理;不要使用KVO和通知。

如果最後,你還是需要做一些複雜的工作——例如在後臺佇列中更新model——記住:你app中的其他程式碼要麼放在主執行緒中,要麼是一些當你寫這些複雜程式碼時無需要考慮的獨立程式碼。但是:一定要仔細,不要過分樂觀。(樂觀主義者的程式碼容易出現崩潰。)

相關文章