前言和導讀前端
關於「Android和iOS開發中的異步處理」這個話題,我從今年上半年就開始構思,如今已經完成了三篇(原計劃是共七篇)。中間斷斷續續修改至今,一直在尋求一個更恰當的表達方式,尚未正式對外發表過。
java
最近幾天,我把全部相關代碼整理到了GitHub上(https://github.com/tielei/AsyncProgrammingDemos)。代碼以Android代碼爲主(iOS的代碼後面有時間再補充)。git
在這個不斷修改的過程當中,我愈發感受到「異步編程」是一個很是重要的課題,它也許是過去的幾年中,我在移動端編程上的最大的收穫了。實際上,異步問題在一些分佈式系統中一直以來都是很重要的問題,不少的分佈式協議被髮明出來就是爲了處理異步事件帶來的挑戰(也許之後有機會咱們能一塊兒聊一聊)。而這個問題侷限到客戶端開發這個單進程內的環境下,有它特殊的特色,值得咱們去總結和思考。github
三篇文章一塊兒推送了,估計所有閱讀下來,要花去很多的時間。每篇各討論一個方面的話題,但基本相互獨立,你也能夠先挑選感興趣的一篇去閱讀。因爲三篇一塊兒推送的,因此無法在文章中互相加引用連接,沒有拿到所有三篇的同窗,能夠向公衆號(張鐵蕾)發送「異步」兩個字,所有相關的內容會一會兒推送給你。編程
-- 2016.08.17後端
下面是正文,歡迎閱讀。緩存
本文是我打算完成的一個系列《Android和iOS開發中的異步處理》的開篇。服務器
從2012年開始開發微愛App的第一個iOS版本計算,我和整個團隊接觸iOS和Android開發已經有4年時間了。如今回過頭來總結,iOS和Android開發與其它領域的開發相比,有什麼獨特的特徵呢?一個合格的iOS或Android開發人員,應該具有哪些技能呢?網絡
若是仔細分辨,iOS和Android客戶端的開發工做仍然能夠分爲「前端」和「後端」兩大部分(就如同服務器的開發能夠分爲「前端」和「後端」同樣)。多線程
所謂「前端」工做,就是與UI界面更相關的部分,好比組裝頁面、實現交互、播放動畫、開發自定義控件等等。顯然,爲了能遊刃有餘地完成這部分工做,開發人員須要深刻了解跟系統有關的「前端」技術,主要包含三大部分:
渲染繪製(解決顯示內容的問題)
layout(解決顯示大小和位置的問題)
事件處理(解決交互的問題)
而「後端」工做,則是隱藏在UI界面背後的東西。好比,操縱和組織數據、緩存機制、發送隊列、生命週期設計和管理、網絡編程、推送和監聽,等等。這部分工做,歸根結底,是在處理「邏輯」層面的問題,它們並非iOS或Android系統所特有的東西。然而,有一大類問題,在「後端」編程中佔據了極大的比重,這就是如何對「異步任務」進行「異步處理」。
尤爲值得指出的是,大部分客戶端開發人員,他們所經歷的培訓、學習經歷和開發經歷,彷佛都更偏重「前端」部分,而在「後端」編程的部分存在必定的空白。所以,本文會嘗試把與「後端」編程緊密相關的「異步處理」問題進行總結歸納。
本文是系列文章《Android和iOS開發中的異步處理》的第一篇,表面上看起來話題不算太大,卻相當重要。固然,若是我打算強調它在客戶端編程中的重要性,我也能夠說:縱觀整個客戶端編程的過程,無非就是在對各類「異步任務」進行「異步處理」而已——至少,對於與系統特性無關的那部分來講,我這麼講是沒有什麼大的問題的。
那麼,這裏的「異步處理」,到底指的是什麼呢?
咱們在編程當中,常常須要執行一些異步任務。這些任務在啓動後,調用者不用等待任務執行完畢便可接着去作其它事情,而任務何時執行完是不肯定的,不可預期的。本文要討論的就是在處理這些異步任務過程當中所可能涉及到的方方面面。
爲了讓所要討論的內容更清楚,先列一個提綱以下:
(一)概述——介紹常見的異步任務,以及爲何這個話題如此重要。
(二)異步任務的回調——討論跟回調接口有關的一系列話題,好比錯誤處理、線程模型、透傳參數、回調順序等。
(三)執行多個異步任務
(四)異步任務和隊列
(五)異步任務的取消和暫停,以及start ID——Cancel掉正在執行的異步任務,實際上很是困難。
(六)關於封屏與不封屏
(七)Android Service實例分析——Android Service提供了一個執行異步任務的嚴密框架 (後面也許會再多提供一些其它的實例分析,加入到這個系列中來)。
顯然,本篇文章要討論的是提綱的第(一)部分。
爲了描述清楚,這個系列文章中出現的代碼已經整理到GitHub上(持續更新),代碼庫地址爲:
https://github.com/tielei/AsyncProgrammingDemos
其中,當前這篇文章中出現的Java代碼,位於com.zhangtielei.demos.async.programming.introduction這個package中;而iOS的代碼位於iOSDemos單獨的目錄中。
下面是由這份源碼生成的安卓App的兩張截圖:
下面,咱們先從一個具體的小例子開始:Android中的Service Binding。
上面的例子展現了Activity和Service之間進行交互的一個典型用法。Activity在onResume的時候與Service綁定,在onPause的時候與Service解除綁定。在綁定成功後,onServiceConnected被調用,這時Activity拿到傳進來的IBinder的實例(service參數),即可以經過方法調用的方式與Service進行通訊(進程內或跨進程)。好比,這時在onServiceConnected中常常要進行的操做可能包括:將IBinder記錄下來存入Activity的成員變量,以備後續調用;調用IBinder獲取Service的當前狀態;設置回調方法,以監聽Service後續的事件變化;等等,諸如此類。
這個過程表面看上去無懈可擊。可是,若是考慮到bindService是一個「異步」調用,上面的代碼就會出現一個邏輯上的漏洞。也就是說,bindService被調用只是至關於啓動了綁定過程,它並不會等綁定過程結束才返回。而綁定過程什麼時候結束(也即onServiceConnected被調用),是沒法預期的,這取決於綁定過程的快慢。而按照Activity的生命週期,在onResume以後,onPause也隨時會被執行。這樣看來,在bindService執行完後,可能onServiceConnected會先於onPause執行,也可能onPause會先於onServiceConnected執行。
固然,在通常狀況下,onPause不會那麼快執行,所以onServiceConnected通常都會趕在onPause以前執行。可是,從「邏輯」的角度,咱們卻不能徹底忽視另一種可能性。實際上它真的有可能發生,好比剛打開頁面就當即退到後臺,這種可能性便能以極小的機率發生。一旦發生,最後執行的onServiceConnected會創建起Activity與Service的引用和監聽關係。這時應用極可能是在後臺,而Activity和IBinder卻可能仍互相引用着對方。這可能形成Java對象長時間釋放不掉,以及其它一些詭異的問題。
這裏還有一個細節,最終的表現其實還取決於系統的unbindService的內部實現。當onPause先於onServiceConnected執行的時候,onPause先調用了unbindService。若是unbindService在調用後可以嚴格保證ServiceConnection的回調再也不發生,那麼最終就不會形成前面說的Activity和IBinder相互引用的狀況出現。可是,unbindService彷佛沒有這樣的對外保證,並且根據我的經驗,在Android系統的不一樣版本中,unbindService在這一點上的行爲還不太同樣。
像上面的分析同樣,咱們只要瞭解了異步任務bindService所能引起的全部可能狀況,那就不難想出相似以下的應對措施。
下面咱們再來看一個iOS的小例子。
如今假設咱們要維護一個客戶端到服務器的TCP長鏈接。這個鏈接在網絡狀態發生變化時可以自動進行重連。首先,咱們須要一個能監聽網絡狀態變化的類,這個類叫作Reachability,它的代碼以下:
上述代碼封裝了Reachability類的接口。當調用者想開始網絡狀態監聽時,就調用startNetworkMonitoring;監聽完畢就調用stopNetworkMonitoring。咱們設想中的長鏈接正好須要建立和調用Reachability對象來處理網絡狀態變化。它的代碼的相關部分可能會以下所示(類名ServerConnection;頭文件代碼忽略):
長鏈接ServerConnection在初始化時建立了Reachability實例,並啓動監聽(調用startNetworkMonitoring),經過系統廣播設置監聽方法(networkStateChanged:);當長鏈接ServerConnection銷燬的時候(dealloc)中止監聽(調用stopNetworkMonitoring)。
當網絡狀態發生變化時,networkStateChanged:會被調用,而且當前網絡狀態會被傳入。若是發現網絡變得可用了(非NotReachable狀態),那麼就異步執行重連操做。
這個過程看上去合情合理。可是這裏面卻隱藏了一個致命的問題。
在進行重連操做時,咱們使用dispatch_async啓動了一個異步任務。這個異步任務在啓動後何時執行完,是不可預期的,這取決於reconnect操做執行的快慢。假設reconnect執行比較慢(對於涉及網絡的操做,這是頗有可能的),那麼可能會發生這樣一種狀況:reconnect還在運行中,但ServerConnection即將銷燬。也就是說,整個系統中全部其它對象對於ServerConnection的引用都已經釋放了,只留下了dispatch_async調度時block對於self的一個引用。
這會致使什麼後果呢?
這會致使:當reconnect執行完的時候,ServerConnection真正被釋放,它的dealloc方法不在主線程執行!而是在socketQueue上執行。
而這接下來又會怎麼樣呢?這取決於Reachability的實現。
咱們來從新分析一下Reachability的代碼來獲得這件事發生的最終影響。這個狀況發生時,Reachability的stopNetworkMonitoring在非主線程被調用了。而當初startNetworkMonitoring被調用時倒是在主線程的。如今咱們看到了,startNetworkMonitoring和stopNetworkMonitoring若是先後不在同一個線程上執行,那麼在它們的實現中的CFRunLoopGetCurrent()就不是指的同一個Run Loop。這已經在邏輯上發生「錯誤」了。在這個「錯誤」發生以後,stopNetworkMonitoring中的SCNetworkReachabilityUnscheduleFromRunLoop就沒有可以把Reachability實例從原來在主線程上調度的那個Run Loop上卸下來。也就是說,此後若是網絡狀態再次發生變化,那麼ReachabilityCallback仍然會執行,但這時原來的Reachability實例已經被銷燬過了(由ServerConnection的銷燬而銷燬)。按上述代碼的目前的實現,這時ReachabilityCallback中的info參數指向了一個已經被釋放的Reachability對象,那麼接下來發生崩潰也就不足爲奇了。
有人可能會說,dispatch_async執行的block中不該該直接引用self,而應該使用weak-strong dance. 也就是把dispatch_async那段代碼改爲下面的形式:
__weak ServerConnection *wself = self;
dispatch_async(socketQueue, ^{ __strong ServerConnection *sself = wself; [sself reconnect]; });
這樣改有沒有效果呢?根據咱們上面的分析,顯然沒有。ServerConnection的dealloc仍然在非主線程上執行,上面的問題也依然存在。weak-strong dance被設計用來解決循環引用的問題,但不能解決咱們這裏碰到的異步任務延遲的問題。
實際上,即便把它改爲下面的形式,仍然沒有效果。
__weak ServerConnection *wself = self;
dispatch_async(socketQueue, ^{ [wself reconnect]; });
即便拿weak引用(wself)來調用reconnect方法,它一旦執行,也會形成ServerConnection的引用計數增長。結果仍然是dealloc在非主線程上執行。
那既然dealloc在非主線程上執行會形成問題,那咱們強制把dealloc裏面的代碼調度到主線程執行好了,以下:
- (void)dealloc {
dispatch_async(dispatch_get_main_queue(), ^{ [reachability stopNetworkMonitoring]; }); [[NSNotificationCenter defaultCenter] removeObserver:self]; }
顯然,在dealloc再調用dispatch_async的這種方法也是行不通的。由於在dealloc執行過以後,ServerConnection實例已經被銷燬了,那麼當block執行時,reachability就依賴了一個已經被銷燬的ServerConnection實例。結果仍是崩潰。
那不用dispatch_async好了,改用dispatch_sync好了。仔細修改後的代碼以下:
- (void)dealloc {
if (![NSThread isMainThread]) {
dispatch_sync(dispatch_get_main_queue(), ^{ [reachability stopNetworkMonitoring]; }); }
else { [reachability stopNetworkMonitoring]; } [[NSNotificationCenter defaultCenter] removeObserver:self]; }
通過「先後左右」打補丁,咱們如今總算獲得了一段能夠基本能正常執行的代碼了。然而,在dealloc裏執行dispatch_sync這種可能耗時的「同步」操做,總難免使人膽戰心驚。
那到底怎樣作更好呢?
我的認爲:並非全部的銷燬工做都適合寫在dealloc裏。
dealloc最擅長的事,天然仍是釋放內存,好比調用各個成員變量的release(在ARC中這個release也省了)。可是,若是要依賴dealloc來維護一些做用域更廣(超出當前對象的生命週期)的變量或過程,則不是一個好的作法。緣由至少有兩點:
dealloc的執行可能會被延遲,沒法確保精確的執行時間;
沒法控制dealloc是否會在主線程被調用。
好比上面的ServerConnection的例子,業務邏輯本身確定知道應該在什麼時機去中止監聽網絡狀態,而不該該依賴dealloc來完成它。
另外,對於dealloc可能會在異步線程執行的問題,咱們應該特別關注它。對於不一樣類型的對象,咱們應該採起不一樣的態度。好比,對於起到View角色的對象,咱們的正確態度是:不該該容許dealloc在異步線程執行的狀況出現。爲了不出現這種狀況,咱們應該竭力避免在View裏面直接啓動異步任務,或者避免在生命週期更長的異步任務中對View產生強引用。
在上面兩個例子中,問題出現的根源在於異步任務。咱們仔細思考後會發現,在討論異步任務的時候,咱們必須關注一個相當重要的問題,即條件失效問題。固然,這也是一個顯而易見的問題:當一個異步任務真正執行的時候(或者一個異步事件真正發生的時候),境況極可能已與當初調度它時不一樣,或者說,它當初賴以執行或發生的條件可能已經失效。
在第一個Service Binding的例子中,異步綁定過程開始調度的時候(bindService被調用的時候),Activity還處於Running狀態(在執行onResume);而綁定過程結束的時候(onServiceConnected被調用的時候),Activity卻已經從Running狀態中退出(執行過了onPause,已經又解除綁定了)。
在第二個網絡監聽的例子中,當異步重連任務結束的時候,外部對於ServerConnection實例的引用已經不復存在,實例立刻就要進行銷燬過程了。繼而形成中止監聽時的Run Loop也再也不是原來那一個了。
在開始下一節有關異步任務的正式討論以前,咱們有必要對iOS和Android中常常碰到的異步任務作一個總結。
網絡請求。因爲網絡請求耗時較長,一般網絡請求接口都是異步的(例如iOS的NSURLConnection,或Android的Volley)。通常狀況下,咱們在主線程啓動一個網絡請求,而後被動地等待請求成功或者失敗的回調發生(意味着這個異步任務的結束),最後根據回調結果更新UI。從啓動網絡請求,到獲知明確的請求結果(成功或失敗),時間是不肯定的。
經過線程池機制主動建立的異步任務。對於那些須要較長時間同步執行的任務(好比讀取磁盤文件這種延遲高的操做,或者執行大計算量的任務),咱們一般依靠系統提供的線程池機制把這些任務調度到異步線程去執行,以節約主線程寶貴的計算時間。關於這些線程池機制,在iOS中,咱們有GCD(dispatch_async)、NSOperationQueue;在Android上,咱們有JDK提供的傳統的ExecutorService,也有Android SDK提供的AsyncTask。無論是哪一種實現形式,咱們都爲本身創造了大量的異步任務。
Run Loop調度任務。在iOS上,咱們能夠調用NSObject的若干個performSelectorXXX方法將任務調度到目標線程的Run Loop上去異步執行(performSelectorInBackground:withObject:除外)。相似地,在Android上,咱們能夠調用Handler的post/sendMessage方法或者View的post方法將任務異步調度到對應的Run Loop上去。實際上,無論是iOS仍是Android系統,通常客戶端的基礎架構中都會爲主線程建立一個Run Loop(固然,非主線程也能夠建立Run Loop)。它可讓長時間存活的線程週期性地處理短任務,而在沒有任務可執行的時候進入睡眠,既能高效及時地響應事件處理,又不會耗費多餘的CPU時間。同時,更重要的一點是,Run Loop模式讓客戶端的多線程編程邏輯變得簡單。客戶端編程比服務器編程的多線程模型要簡單,很大程度上要歸功於Run Loop的存在。在客戶端編程中,當咱們想執行一個長的同步任務時,通常先經過前面(2)中說起的線程池機制將它調度到異步線程,在任務執行完後,再經過本節提到的Run Loop調度方法或者GCD等機制從新調度回主線程的Run Loop上。這種「主線程->異步線程->主線程」的模式,基本成爲了客戶端多線程編程的基本模式。這種模式規避了多個線程之間可能存在的複雜的同步操做,使處理變得簡單。在後面第(三)部分——執行多個異步任務,咱們還有機會繼續探討這個話題。
延遲調度任務。這一類任務在指定的某個時間段以後,或者在指定的某個時間點開始執行,能夠用於實現相似重試隊列之類的結構。延遲調度任務有多種實現方式。在iOS中,NSObject的performSelector:withObject:afterDelay:,GCD的dispatch_after或dispatch_time,另外,還有NSTimer;在Android中,Handler的postDelayed和postAtTime,View的postDelayed,還有老式的java.util.Timer,此外,安卓中還有一個比較重的調度器——能在任務調度執行時自動喚醒程序的AlarmService。
跟系統實現相關的異步行爲。這類行爲種類繁多,這裏舉幾個例子。好比:安卓中的startActivity是一個異步操做,從調用後到Activity被建立和顯示,仍有一小段時間。再如:Activity和Fragment的生命週期是異步的,即便Activity的生命週期已經到了onResume,你仍是不知道它所包含的Fragment的生命週期走到哪一步了(以及它的view層次有沒有被建立出來)。再好比,在iOS和Android系統上都有監聽網絡狀態變化的機制(本文前面的第二個代碼例子中就有涉及),網絡狀態變化回調什麼時候執行就是一個異步事件。這些異步行爲一樣須要統一完整的異步處理。
本文在最後還須要澄清一個關於題目的問題。這個系列雖命名爲《Android和iOS開發中的異步處理》,可是對於異步任務的處理這個話題,實際中並不侷限於「iOS或Android開發」中,好比在服務器的開發中也是有可能遇到的。在這個系列中我所要表達的,更多的是一個抽象的邏輯,並不侷限於iOS或Android某種具體的技術。只是,在iOS和Android的前端開發中,異步任務被應用得如此普遍,以致於咱們應該把它當作一個更廣泛的問題來對待了。