最近和幾個朋友交流Android開發中的網路下載問題時,談到了用Thread開啟下載線程時會產生的Bug,其實直接用子線程開啟下載任務的確是很Low的做法,那麼原因究竟如何,而比較高大上的做法是怎樣?於是用這篇博文詳細分析記錄一下。 一、概念介紹 Thread是指在CPU運行的一個程式中,可以有多個 ...
最近和幾個朋友交流Android開發中的網路下載問題時,談到了用Thread開啟下載線程時會產生的Bug,其實直接用子線程開啟下載任務的確是很Low的做法,那麼原因究竟如何,而比較高大上的做法是怎樣?於是用這篇博文詳細分析記錄一下。
一、概念介紹
Thread是指在CPU運行的一個程式中,可以有多個執行路徑。運行的程式稱作進程,而這個執行路徑,就被稱為線程(如果對這兩個名詞不太理解的同學可以參考一下操作系統方面的書籍)。Java中的多線程是指多個Thread可以在一段內同步執行,這樣可以提高代碼的運行效率,Java中允許一個進程有多個線程,可以無限多,但是必須要有一個線程,也就是當前進程的主線程。
必須要明白的一點是,Thread是Java語言下的一個底層類,而Android是使用並封裝了Java語言的系統,所以Android中的AsyncTask只是使用了Java的多線程概念並優化封裝之後的一個抽象類。所以Thread和AsyncTask完全是兩個不同層次的概念,而不是簡單的替換。
再說說AsyncTask非同步任務,這個類是在Android中使用的,在編寫該類時就已經明確說明,“AsyncTask is designed to be a helper class around {@link Thread} and {@link Handler} and does not constitute a generic threading framework. AsyncTasks should ideally be used for short operations (a few seconds at the most.) If you need to keep threads running for long periods of time, it is highly recommended you use the various APIs”,後邊的不重要就不用粘貼了,可以看出非同步任務進行長時間操作時使用的。因為Android中對每一個App的運行都看做一個進程,而這個進程中的主線程,就是UI線程,也就是我們打開一個App時可以看到界面的這個線程。而像下載這種耗時操作,如果放到UI線程執行,則會使得UI線程負荷過大產生ANR應用無響應異常,所以創建了AsyncTask類,用來專門進行一些耗時的非UI更新操作。
通過上面的介紹,很容易想到AsyncTask是使用了Java中的多線程技術的,但是他不是單純的Thread,具體是怎麼實現非同步任務的,我們可以看源碼比較。
Thread類是在java.lang包下的,所以他的使用不需要另外導包,而且Thread是實現Runnable介面的類,也就是說他可以實例化;由於Thread是底層代碼,具體源碼就不再分析了,所以主要說一下AsyncTask怎麼用Thread實現的非同步任務。
AsyncTask類是在android.os包下的抽象類,在使用之前必須導包。AsyncTask是使用線程工廠創建新的線程在後臺執行非同步任務的,之前我們說個Android中有一個UI線程作為主線程,那麼再創建的線程都是子線程了,至於新創建的這些子線程做了什麼事情,就要看我們的意願了。
二、下載分析:
介紹了半天兩個類的對比,感覺還是直接上Demo來的快一點。下邊我分別用開啟子線程和開啟非同步兩種方式實現下載,同時簡單分析一下這兩種方式下的CPU執行順序。
1. 在當前Activity中開啟子Thread執行下載
(1)創建下載子線程:
1 /** 2 * 下載線程 3 */ 4 private Thread myThread =new Thread(new Runnable() { 5 @Override 6 public void run() { 7 Object data=download(PATH); 8 Message msg=Message.obtain(); 9 msg.what=101; 10 msg.obj=data; 11 handler.sendMessage(msg); 12 } 13 });
(2)在Handler中執行下載之後的任務:
1 private Handler handler=new Handler(new Handler.Callback() { 2 @Override 3 public boolean handleMessage(Message msg) { 4 if(msg.what==101){ 5 data=msg.obj; 6 //下麵執行對data的操作 7 } 8 return false; 9 } 10 });
(3)在需要下載的地方開啟當前下載線程:
1 myThread.start();
只需要上邊三步就可以輕鬆完成下載網路請求,是不是看起來很簡單?那麼問題來了,下載任務是在myThread的子線程中執行的,如果下載任務還在執行的過程中時,用戶執行了頁面跳轉的操作,也就是說當前Activity所在的UI線程已經銷毀,但是並沒有銷毀myThread子線程吧,那麼當myThread執行完下載任務download()這個方法之後,他接著調用handler來發送信息以執行data操作,而執行data操作的handler是在當前Activity中定義的,隨著當前Activity的銷毀,當前handler也跟隨著銷毀了,這樣在myThread中就無法調用執行data的handler了,那麼他必然會報NullPointException了吧。所以這樣使用子線程進行下載任務是不安全的。
2. 使用非同步任務AsyncTask執行下載任務
所以在Android中可以使用原生的AsyncTask進行像下載網路請求這樣的耗時操作,具體方法就是創建一個下載任務繼承AsyncTask抽象類,同時重寫該類中的doInBackground(),這個方法是在要下載的子線程中執行的,點開AsyncTask的源碼,我們可以看到在doInBackground()這個方法的前邊有個註釋@WorkThread,可以想到這個方法是在工作線程中執行的,那麼有沒有在主線程中執行的方法呢?當然是有的,我們還會看到有這樣幾個方法,他們的方法體內都沒有執行語句,說明是可以用子類來重寫這些方法的,有構造方法,execute(),onPreExecute(),onCancelled()等都是在MainThread中執行的。
那麼可能有同學要提問了,這樣還是在子線程中執行要下載的任務,難道這樣再發生上邊我們說到的那種臨界事件,子線程下載結束之後就不會有空指針異常了嗎?
當然可以很肯定的說,使用AsyncTask絕對不會發生上述Bug了,為什麼呢?我們接著分析。
在工作線程中執行的除了當前執行下載的doInBackground()之外,就只有publishProgress()這一個方法了,而doInBackground()是個抽象方法,所以要想知道工作線程到底有什麼門道,只能從publicProgress()找線索了。我們知道這個方法是發佈進度的時候使用,下麵是這個方法的源碼,
1 @WorkerThread 2 protected final void publishProgress(Progress... values) { 3 if (!isCancelled()) { 4 getHandler().obtainMessage(MESSAGE_POST_PROGRESS, 5 new AsyncTaskResult<Progress>(this, values)).sendToTarget(); 6 } 7 }
很明顯這個getHandler()就是獲得當前AsyncTask類中的Handler對象,也就是說在工作線程中發佈的進度會將信息發送到當前AsyncTask的Handler中處理,那麼我們不管工作線程中具體怎麼發佈的進度,只需要看看在當前AsyncTask中怎麼處理接收的信息就可以了。
1 private static Handler getHandler() { 2 synchronized (AsyncTask.class) { 3 if (sHandler == null) { 4 sHandler = new InternalHandler(); 5 } 6 return sHandler; 7 } 8 }
這個方法明顯是在sHandler不為空的時候返回了一個InternalHandler對象,整個過程都是對AsyncTask加鎖的,而這裡加鎖才是關鍵,畢竟要保證發送消息時的安全性,在獲得一個InternalHandler對象後,整個AsyncTask都是加鎖狀態的。那麼我們接著去看這個InternalHandler是乾什麼用的。
首先我們可以確定這是一個繼承Handler類的子類,在他的handlerMessage()中只執行了下邊幾行代碼,這裡應該快要找到我們問題的根源了。
1 AsyncTaskResult<?> result = (AsyncTaskResult<?>) msg.obj; 2 switch (msg.what) { 3 case MESSAGE_POST_RESULT: 4 // There is only one result 5 result.mTask.finish(result.mData[0]); 6 break; 7 case MESSAGE_POST_PROGRESS: 8 result.mTask.onProgressUpdate(result.mData); 9 break; 10 }
發現這裡對發送的信息類型進行了判斷,只有兩種類型,第二種MESSAGE_POST_PROGRESS,不正是剛剛發佈進度的方法publicProgress()裡邊發送的信息類型嗎。那麼第一種MESSAGE_POST_RESULT,不難想到,這就是在工作線程中執行完doInBackground()之後的發送的信息類型了,而且人家已經有註釋說明,“There is only one result”,“只有一個唯一的結果”,在得到這個信息也就是我們的下載任務執行完成之後,會調用下邊那個方法,其實不用再往下找了,因為執行的這個方法就是當前AsyncTask自身的finish()這個方法。而這正是說明瞭在正常執行完工作線程的doInBackground()之後再在主線程中執行finish(),所以我們的思路也就理順了。
好吧,也許看完上邊的代碼加我的分析,有些同學感覺更是雲里霧裡了,似乎這裡邊並沒有解釋中途跳轉的問題啊。那你可要仔細想想了,在之前直接開啟子線程下載之後的中途跳轉發生空指針異常的根本原因在哪裡?是在子線程中無法使用主線程中的handler對象才產生的空指針異常吧。那麼我們的非同步任務AsyncTask是怎麼解決發送信息這個handler的?
在使用handler發送信息時,系統會先調用getHandler(),獲得一個InternalHandler對象,如果之前沒有就創建,如果有就用之前的,而且由於整個過程中當前非同步任務AsyncTask都是加鎖狀態的,所以其他線程無法使用,同樣的在使用AsyncTask的主線程中也無法隨意銷毀。這樣再將得到的handler返回使用發送信息,就能順利的跨過空指針異常了。
三、總結
這麼解釋,相信還在摸不著頭腦的同學們應該明白一點了,下邊我再簡單做一下總結。
AsyncTask是作為非同步任務,執行除了UI界面更新的任務之外的其他耗時操作的。UI界面的更新是在主線程,也就是UI線程中執行的,而在這個非同步任務中,開啟了一個工作線程來執行耗時操作。而這個工作線程和UI線程的執行順序是不同步的,也就是說只有執行完工作線程中的下載之後,才會調用UI線程中的onPostExecute()執行後續UI操作,這樣就實現了非同步下載。如果UI線程銷毀之後工作線程再發送下載結束的信息,由於工作線程再使用過程中是與AsyncTask綁定的,所以他也會隨著當前AsyncTask的銷毀而銷毀,不會執行後續的下載操作,自然也不會執行發送下載結束的信息。
而簡單的開啟子線程執行下載,子線程與UI線程只是保持簡單的同步關係,所以只是單純的在子線程中執行下載耗時操作是不安全的。事實證明,儘管Java中的多線程是個很好的機制,但是在使用時要註意它的副作用,學會使用對他進行封裝之後的類和方法。