第5章 Android中的進程與線程
當一個應用程式開始運行它的第一個組件時,Android會為它啟動一個Linux進程,並在其中執行一個單一的線程。預設情況下,應用程式所有的組件均在這個進程的這個線程中運行(就是我們常說的android app主線程)。然而,你也可以安排組件在其他進程中運行,而且可以為任意進程建立額外的線程。本章主要介紹android app下的線程和進程是如何工作的
5.1 進程
預設情況下,同一應用程式的所有組件運行在同一進程中。不過,如果你需要控制某個組件屬於哪個進程,也可以通過修改manifest檔案來實現。manifest檔案中的所有元件節點如<activity>,<service>,<receiver>,<provider>都支援android:process這個屬性並可以指定一個進程,這樣這些組件就會在指定的進程中運行。你可以設定這個屬性使每個組件運行於其自己的進程或只是其中一些組件共用一個進程。你也可以設定android:process讓不同應用中的組件可以運行在同一個進程,但這樣需要這些應用共用同一個Linux使用者ID並且有相同的數位憑證簽名。<application>元素也支援android:process屬性,用於為所有的組件指定一個預設值,並應用於所有組件,如果你有這樣的需求,可以省點事。Android系統可能在某些時刻決定關閉一個進程,比如記憶體很少並且其他進程更迫切的需要服務使用者而啟動的情況。進程被kill掉後,其中的組件們都被銷毀。如果再次需要這些組件工作時,進程又會被重新建立出來。當系統決定關閉哪個進程時,Android系統會衡量進程與使用者的相對重要性。例如,比起一個在前台可見的activity所在的進程,和那些在後台不可見的activity所在的進程相比,顯然後者更容易被系統關閉。是否決定一個終止進程,取決於進程中所運行組件的狀態。下面將詳細講述。
5.1.1進程的生命週期
Android系統會盡量維持一個進程的生命,直到最終需要為新的更重要的進程提供記憶體時,它才會移除不太重要的舊進程。為了決定哪個進程該終止,系統會根據這個進程內的組件狀態把進程置於不同的重要性等級。當需要系統資源時,重要性等級越低的先被kill掉。系統分為 5個重要性等級。下面列出了不同進程類型的重要性等級(第一個進程類型是最重要的,它最後才會被kill)
1. 前台進程
使用者當前正在做的事情所屬的這個進程。如果滿足下面的條件,一個進程就被認為是前台進程:
(1)這個進程擁有一個正在與使用者互動的Activity(這個Activity的onResume() 方法被調用)。
(2)這個進程擁有一個正在與使用者互動的activity之中並與之綁定的Service。
(3)這個進程擁有一個前台啟動並執行Service(即service已經調用了startForeground()方法)
(4)這個進程擁有一個正在執行一個生命週期回調方法(onCreate(),onStart(),
或onDestroy())的Service。
(5)這個進程擁有正在執行其onReceive()方法的BroadcastReceiver。
通常,在任何時間點,只有很少的前台進程存在。它們只有在萬不得已的情況下時才會被終止--如果記憶體太小而不能繼續運行時。通常,到了這時,裝置就達到了一個記憶體分頁調度狀態,所以需要終止一些前台進程來保證使用者介面的響應。
2. 可見進程
一個沒有任何前台的組件的進程,但是依然對使用者所見。滿足下列條件時,進程即為可見:
(1)這個進程擁有一個不在前台但仍可見的Activity(它的onPause()方法被調用)。例如當一個前台activity啟動一個對話方塊形式的activity時,就出了這種情況。
(2)這個進程擁有一個綁定到前台Activity的service。
一個可見的進程是極其重要的,通常不會被終止,除非記憶體不夠,需要釋放記憶體來讓前台進程運行。
3. Service進程
一個進程不在上述兩種之內,但它運行著通過startService()方法所啟動的service。儘管一個service進程可能對使用者不所見,但是它們通常做一些使用者關心的事情(比如播放音樂或下載資料),所以除非系統沒有足夠的空間運行前台進程和可見進程時才會終止一個service進程。
4. 後台進程
一個進程擁有一個當前不可見的activity(activity的onStop()方法被調用)。
這樣的進程不會直接影響到使用者體驗,所以系統可以在任意時刻kill掉它們從而為前台、可見、以及service進程提供記憶體空間。通常有很多後台進程在運行。它們被儲存在一個“最近最少使用”列表中來確保擁有最近剛被看到的activity的進程最後被kill。如果一個activity正確的實現了它的生命週期方法,並儲存了它的目前狀態,那麼殺死它的進程將不會對使用者的可視化體驗造成影響,因為當使用者返回到這個activity時,這個activity會恢複它所有的可見狀態。
5. 空進程
一個沒有任何activity組件的進程。保留這類進程的唯一理由是緩衝,這樣可以提高下一次一個組件要運行它時的啟動速度。系統經常在進程緩衝和底層的核心緩衝之間的為了平衡整體系統資源而終止它們。
根據進程中當前activity的組件的重要性,Android會把使用進程的優先順序中的最進階。例如,如果一個進程擁有一個service和一個可見的activity,進程會被定為可見進程,而不是service進程。另外,如果被其它進程所依賴,一個進程的層級可能會被提高——一個服務於其它進程的進程,其層級不可能比被服務進程低。因為擁有一個service的進程比擁有一個後台activitie的進程層級高,所以當一個activity啟動一個需長時間執行的操作時,最好是啟動一個service,而不是簡單的建立一個背景工作執行緒。尤其是當這個操作可能比activity的生命還要長時。例如,一個向網站上傳圖片的activity,應該啟動一個service,從而使上傳操作可以在使用者離開這個activity時繼續在後台執行。使用一個service保證了這個操作至少是在"服務進程"層級,而不用管activity是否發生了什麼情況。同樣broadcast receivers 應該使用service而不是簡單地使用一個線程。
5.2 線程
當一個應用程式啟動時,系統建立一個叫做"main"的執行線程。這個線程是十分重要的,因為它主管使用者介面控制項的分發事件,其中包含繪圖事件。這個線程也用於你的應用程式與介面工具包(android.widget和 android.view包中的組件)的互動。所以網路上經常說的Android UI線程就是main線程。系統不會為每個組件的執行個體分別建立線程。所有運行在一個進程中的組件都在UI線程中被執行個體化,並且系統對每個組件的調用都在這個線程中發送訊息。因此,你需要理解介面的操作都是在UI線程中進行的,如果不在UI線程中則會出異常,理解了這點你就知道handler的處理機制了。
例如,當使用者觸控螢幕幕上的一個按鈕時,你的應用程式UI線程把觸摸事件發送給按鈕控制項,然後按鈕控制項設定它的按下狀態並向事件隊列發出一個重新整理介面的請求,UI線程從隊列中取出這個請求並通知它重繪。
當你的應用在集中響應大量的使用者互動運算時,這種單線程的模型會帶來低效能,除非你能正確的最佳化你的程式。如果所有事情都在UI線程中執行,比如網路連接或資料庫請求這樣的耗時操作,那麼將會阻塞整個介面的響應(給使用者通俗的感覺就是死機)。當UI線程被阻塞時,就不能分發事件了,包括繪圖事件。從使用者的角度看,程式反應太慢了。那麼使用者可能會退出並刪掉你的應用。
此外,Andoid UI工具包不是安全執行緒的。所以你不能在一個自己開的線程中操作你的介面,你只能在UI線程中操作你的介面。所以,對於Android的單執行緒模式有兩個簡單的規則:
1. 不要阻塞UI線程
2. 不要在UI線程之外操作Android UI工具包。
5.2.1背景工作執行緒
由於上述的單執行緒模式,不要阻塞你的UI線程是非常重要的,那麼如果你有一個長時間才能完成的任務,你應把它們放在另一個線程中執行(後台線程或背景工作執行緒)。例如,下面是的代碼是監聽click事件,它開一個線程下載一個圖片並在一個ImageView中顯示它,如代碼清單5-1所示:
public void onClick(View v) { new Thread(new Runnable() { public void run() { Bitmap b = loadImageFromNetwork("http://example.com/image.png"); mImageView.setImageBitmap(b); } }).start();}
代碼清單5-1
大致看來,這好像沒什麼問題,因為它建立了一個新線程來進行耗時的網路操作。然而它違反了單執行緒模式的第二條規則:不要在UI線程之外操作介面。這段代碼在背景工作執行緒中修改了ImageView。這會導致一個異常發生。當然我們可以走其他路線,Android提供了很多從其它線程來操作介面的方法:
Activity.runOnUiThread(Runnable)
View.post(Runnable)
View.postDelayed(Runnable,long)
例如,我們可以用View.post(Runnable)來解決上面的問題,如代碼清單5-2所示:
public void onClick(View v) { new Thread(new Runnable() { public void run() { final Bitmap bitmap = loadImageFromNetwork("http://example.com/image.png"); mImageView.post(new Runnable() { public void run() { mImageView.setImageBitmap(bitmap); } }); } }).start();}
代碼清單5-2
現在終於是安全執行緒的了:網路操作在另一個線程中並且ImageView 在UI線程中改變。然而,由於操作複雜性的增長,這樣的代碼就變得複雜並難以維護,為了處理更複雜的互動,你可能需要在背景工作執行緒中用到Handler對象來傳遞訊息到UI線程中處理。還有個比較好的解決辦法是繼承AsyncTask類,這樣可以使得背景工作執行緒和UI互動這一類任務變得更簡化。
5.2.2使用AsyncTask
AsyncTask可以讓你在UI上執行非同步工作,它在一個背景工作執行緒中執行某些耗時的操作,之後將結果返回給UI線程。你可以不用管理背景工作執行緒和UI線程的互動,讓你省事不少。使用AsyncTask類時,你需要繼承AsyncTask類並實現doInBackground()回調方法。要更新UI介面,需要實現onPostExecute(),並從doInBackground()方法中傳遞結果,所以你能安全的更新你的介面,最後只需要在UI線程中調用execute()方法來執行任務。如代碼清單5-3所示,有一個使用AsynTask的例子:
public void onClick(View v) { new DownloadImageTask().execute("http://example.com/image.png");} private class DownloadImageTask extends AsyncTask<String, Void, Bitmap> { /** 系統會開一個線程來處理後台操作*/ protected Bitmap doInBackground(String... urls) { return loadImageFromNetwork(urls[0]); } /** doInBackground()方法返回的結果傳遞到UI線程中執行*/ protected void onPostExecute(Bitmap result) { mImageView.setImageBitmap(result); }}
代碼清單5-3
因為AsyncTask將工作分成了兩部分,一個部分在UI線程執行,另一個在背景工作執行緒執行,因此簡化了我們的工作,並且讓更新介面更安全。這裡簡單介紹下它是如何工作的:
AsyncTask定義了三種泛型型別 Params,Progress和Result。Params是啟動任務執行的輸入參數,比如HTTP請求的URL。Progress是背景工作執行的百分比。Result是後台執行任務最終返回的結果,比如String,Integer等。
1. doInBackground()會在背景工作執行緒中自動執行。
2. onPreExecute(), onPostExecute(), and onProgressUpdate()這三個方法都是在UI線程中調用。
3. doInBackground()方法返回的值會傳遞給onPostExecute()方法。
4. 你可以在doInBackground()方法中隨時調用publishProgress()方法以此在UI線程中更新執行進度。並且可以隨時在任何線程中取消任務。
關於它更詳細的例子,可以參考google上的一個開源項目Shelves(http://code.google.com/p/shelves/)
5.2.3安全執行緒方法
在某些情況下,你實現的方法可能會被多個線程同時調用,因此要保證該方法是安全執行緒的。一些遠程調用的方法——例如bound service。當我們在同一個進程中調用了某一個正在啟動並執行IBander中的方法,這個方法會運行在調用者的線程中。然而,當不在同一個進程中調用該方法時,系統會為這個進程開啟的線程池中的某一個線程來執行該方法。因為一個service可以有多個用戶端,多個池線程可以在同一時間調用相同的IBinder方法。因此,實現IBinder方法必須是安全執行緒的。同樣Content Provider類似。
5.3 處理序間通訊
Android提供了處理序間通訊(IPC)機制,使用遠端程序呼叫(RPC),其中一個方法被 Activity或其他應用程式組件調用,但是在另一個進程中執行這個方法,然後結果又要返回到調用者那去。首先需要分解這個方法為調用和資料,這就需要在系統能理解它的調用和資料並從本地進程和遠程進程的地址空間中來傳遞,然後再返回的時候重新組裝。android系統能完美支援處理序間通訊(IPC),這樣你就可以專註於制定和實現RPC編程介面。要實現處理序間通訊(IPC),應用程式必須使用bindService()來綁定一個service,使用bindService更詳細的內容請參閱Service那一章。
FAQ:QQ群213821767