從零開始--系統深入學習android(實踐-讓我們開始寫代碼-Android框架學習-5.Android中的進程與線程)


第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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM