Android的程式與執行緒使用總結

greenrobot發表於2014-12-12

當一個Android應用程式元件啟動時候,如果此時這個程式的其他元件沒有正在執行,那麼系統會為這個程式以單一執行緒的形式啟動一個新的Linux 程式。預設情況下,同一應用程式下的所有元件都執行再相同的程式和執行緒(一般稱為程式的“主”執行緒)中。如果一個應用元件啟動但這個應用的程式已經存在了(因為這個應用的其他元件已經在之前啟動了),那麼這個元件將會在這個程式中啟動,同時在這個應用的主執行緒裡面執行。然而,你也可以讓你的應用裡面的元件執行在不同的程式裡面,也可以為任何程式新增額外的執行緒。

這片文章討論了Android程式裡面的程式和執行緒如何運作的。

程式

預設情況下,同一程式的所有元件都執行在相同的程式裡面,大多數的應用都是這樣的。然而,如果你發現你需要讓你的程式裡面的某個元件執行在特定的程式裡面,你可以在manifest 檔案裡面設定。

manifest 檔案裡面為每一個元件元素—<activity><service><receiver>, 和<provider>—提供了 android:process 屬性。通過設定這個屬性你可以讓元件執行在特定的程式中。你可以設定成每個元件執行在自己的程式中,也可以讓一些元件共享一個程式而其他的不這樣。你還可以設定成不同應用的元件執行在同一個程式裡面—這樣可以讓這些應用共享相同的Linux user ID同時被相同的證照所認證。

<application> 元素也支援 android:process 屬性,設定這個屬性可以讓這個應用裡面的所有元件都預設繼承這個屬性。

Android 可能在系統剩餘記憶體較少,而其他直接服務使用者的程式又要申請記憶體的時候shut down 一個程式, 這時這個程式裡面的元件也會依次被kill掉。當這些元件有新的任務到達時,他們對應的程式又會被啟動。

在決定哪些程式需要被kill的時候,Android系統會權衡這些程式跟使用者相關的重要性。比如,相對於那些承載這可見的activities的程式,系統會更容易的kill掉那些承載不再可見activities的程式。決定是否終結一個程式取決於這個程式裡面的元件執行的狀態。下面我們會討論kill程式時所用到的一些規則。

程式的生命週期

作為一個多工的系統,Android 當然系統能夠儘可能長的保留一個應用程式。但是由於新的或者更重要的程式需要更多的記憶體,系統不得不逐漸終結老的程式來獲取記憶體。為了宣告哪些程式需要保留,哪些需要kill,系統根據這些程式裡面的元件以及這些元件的狀態為每個程式生成了一個“重要性層級” 。處於最低重要性層級的程式將會第一時間被清楚,接著時重要性高一點,然後依此類推,根據系統需要來終結程式。

在這個重要性層級裡面有5個等級。下面的列表按照重要性排序展示了不同型別的程式(第一種程式是最重要的,因此將會在最後被kill):

  1. Foreground 程式 一個正在和使用者進行互動的程式。 如果一個程式處於下面的狀態之一,那麼我們可以把這個程式稱為 foreground 程式:

    一般說來,任何時候,系統中只存在少數的 foreground 程式。 只有在系統記憶體特別緊張以至於都無法繼續執行下去的時候,系統才會通過kill這些程式來緩解記憶體壓力。在這樣的時候系統必須kill一些 (Generally, at that point, the device has reached a memory paging state,這句如何翻譯較好呢)foreground 程式來保證 使用者的互動有響應。

  2. Visible 程式 一個程式沒有任何 foreground 元件, 但是它還能影響螢幕上的顯示。 如果一個程式處於下面的狀態之一,那麼我們可以把這個程式稱為 visible 程式:
    • 程式包含了一個沒有在foreground 狀態的 Activity ,但是它仍然被使用者可見 (它的 onPause() 方法已經被呼叫)。這種情況是有可能出現的,比如,一個 foreground activity 啟動了一個 dialog,這樣就會讓之前的 activity 在dialog的後面部分可見。
    • 程式包含了一個繫結在一個visible(或者foreground)activity的 Service 。

    一個 visible 程式在系統中是相當重要的,只有在為了讓所有的foreground 程式正常執行時才會考慮去kill visible 程式。

  3. Service 程式 一個包含著已經以 startService() 方法啟動的 Service 的程式,同時還沒有進入上面兩種更高階別的種類。儘管 service 程式沒有與任何使用者所看到的直接關聯,但是它們經常被用來做使用者在意的事情(比如在後臺播放音樂或者下載網路資料),所以系統也只會在為了保證所有的foreground and visible 程式正常執行時kill掉 service 程式。
  4. Background 程式 一個包含了已不可見的activity的 程式 (這個 activity 的 onStop() 已經被呼叫)。這樣的程式不會直接影響使用者的體驗,系統也可以為了foreground 、visible 或者 service 程式隨時kill掉它們。一般說來,系統中有許多的 background 程式在執行,所以將它們保持在一個LRU (least recently used)列表中可以確保使用者最近看到的activity 所屬的程式將會在最後被kill。如果一個 activity 正確的實現了它的生命週期回撥函式,儲存了自己的當前狀態,那麼kill這個activity所在的程式是不會對使用者在視覺上的體驗有影響的,因為當使用者回退到這個 activity時,它的所有的可視狀態將會被恢復。檢視 Activities 可以獲取更多如果儲存和恢復狀態的文件。
  5. Empty 程式 一個不包含任何活動的應用元件的程式。 這種程式存在的唯一理由就是快取。為了提高一個元件的啟動的時間需要讓元件在這種程式裡執行。為了平衡程式快取和相關核心快取的系統資源,系統需要kill這些程式。

Android是根據程式中元件的重要性儘可能高的來評級的。比如,如果一個程式包含來一個 service 和一個可見 activity,那麼這個程式將會被評為 visible 程式,而不是 service 程式。

另外,一個程式的評級可能會因為其他依附在它上面的程式而被提升—一個服務其他程式的程式永遠不會比它正在服務的程式評級低的。比如,如果程式A中的一個 content provider 正在為程式B中的客戶端服務,或者如果程式A中的一個 service 繫結到程式B中的一個元件,程式A的評級會被系統認為至少比程式B要高。

因為程式裡面執行著一個 service 的評級要比一個包含background activities的程式要高,所以當一個 activity 啟動長時操作時,最好啟動一個 service 來做這個操作,而不是簡單的建立一個worker執行緒—特別是當這個長時操作可能會拖垮這個activity。比如,一個需要上傳圖片到一個網站的activity 應當開啟一個來執行這個上傳操作。這樣的話,即使使用者離開來這個activity也能保證上傳動作在後臺繼續。使用 service 可以保證操作至少處於”service process” 這個優先順序,無論這個activity發生了什麼。這也是為什麼 broadcast receivers 應該使用 services 而不是簡單的將耗時的操作放到執行緒裡面。

執行緒

當一個應用啟動的時候,系統會為它建立一個執行緒,稱為“主執行緒”。這個執行緒很重要因為它負責處理排程事件到相關的 user interface widgets,包括繪製事件。你的應用也是在這個執行緒裡面與來自Android UI toolkit (包括來自 android.widget 和 android.view 包的元件)的元件進行互動。因此,這個主執行緒有時候也被稱為 UI 執行緒。

系統沒有為每個元件建立一個單獨的執行緒。同一程式裡面的所有元件都是在UI 執行緒裡面被例項化的,系統對每個元件的呼叫都是用過這個執行緒進行排程的。所以,響應系統呼叫的方法(比如 onKeyDown() 方法是用來捕捉使用者動作或者一個生命週期回撥函式)都執行在程式的UI 執行緒裡面。

比如,當使用者點選螢幕上的按鈕,你的應用的UI 執行緒會將這個點選事件傳給 widget,接著這個widget設定它的按壓狀態,然後傳送一個失效的請求到事件佇列。這個UI 執行緒對請求進行出隊操作,然後處理(通知這個widget重新繪製自己)。

當你的應用與使用者互動對響應速度的要求比較高時,這個單執行緒模型可能會產生糟糕的效果(除非你很好的實現了你的應用)。特別是,當應用中所有的事情都發生在UI 執行緒裡面,那些訪問網路資料和資料庫查詢等長時操作都會阻塞整個UI執行緒。當整個執行緒被阻塞時,所有事件都不能被傳遞,包括繪製事件。這在使用者看來,這個應用假死了。甚至更糟糕的是,如果UI 執行緒被阻塞幾秒(當前是5秒)以上,系統將會彈出臭名昭著的 “application not responding” (ANR) 對話方塊。這時使用者可能選擇退出你的應用甚至解除安裝。

另外,Android的UI 執行緒不是執行緒安全的。所以你不能在一個worker 執行緒操作你的UI—你必須在UI執行緒上對你的UI進行操作。這有兩條簡單的關於Android單執行緒模型的規則:

  1. 不要阻塞 UI 執行緒
  2. 不要在非UI執行緒裡訪問 Android UI toolkit

Worker 執行緒

由於上面對單一執行緒模型的描述,保證應用介面的及時響應同時UI執行緒不被阻塞變得很重要。如果你不能讓應用裡面的操作短時被執行玩,那麼你應該確保把這些操作放到獨立的執行緒裡(“background” or “worker” 執行緒)。

比如,下面這段程式碼在一個額外的執行緒裡面下載圖片並在一個 ImageView顯示:

public void onClick(View v){
    new Thread(new Runnable(){
        public void run(){
            Bitmap b = loadImageFromNetwork("http://example.com/image.png");
            mImageView.setImageBitmap(b);
        }
    }).start();}

起先這段程式碼看起來不錯,因為它建立一個新的執行緒來處理網路操作。然而,它違反來單一執行緒模型的第二條規則: 不在非UI執行緒裡訪問 Android UI toolkit—這個例子在一個worker執行緒修改了 ImageView 。這會導致不可預期的結果,而且還難以除錯。

為了修復這個問題,Android提供了幾個方法從非UI執行緒訪問Android UI toolkit 。詳見下面的這個列表:

那麼,你可以使用 View.post(Runnable) 方法來修改之前的程式碼:

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();}

現在這個方案的執行緒安全的:這個網路操作在獨立執行緒中完成後,UI執行緒便會對ImageView 進行操作。

然而,隨著操作複雜性的增長,程式碼會變得越來越複雜,越來越難維護。為了用worker 執行緒處理更加複雜的互動,你可以考慮在worker執行緒中使用Handler ,用它來處理UI執行緒中的訊息。也許最好的方案就是繼承 AsyncTask 類,這個類簡化了需要同UI進行互動的worker執行緒任務的執行。

使用 AsyncTask

AsyncTask 能讓你在UI上進行非同步操作。它在一個worker執行緒裡進行一些阻塞操作然後把結果交給UI主執行緒,在這個過程中不需要你對執行緒或者handler進行處理。

使用它,你必須繼承 AsyncTask 並實現 doInBackground() 回撥方法,這個方法執行在一個後臺執行緒池裡面。如果你需要更新UI,那麼你應該實現onPostExecute()這個方法從 doInBackground() 取出結果,然後在 UI 執行緒裡面執行,所以你可以安全的更新你的UI。你可以通過在UI執行緒呼叫 execute()方法來執行這個任務。

比如,你可以通過使用 AsyncTask來實現之前的例子:

public void onClick(View v){
    new DownloadImageTask().execute("http://example.com/image.png");
}
private class DownloadImageTask extends AsyncTask<String,Void,Bitmap>{
    /** The system calls this to perform work in a worker thread and
      * delivers it the parameters given to AsyncTask.execute() */
    protected Bitmap doInBackground(String... urls){
        return loadImageFromNetwork(urls[0]);
    }

    /** The system calls this to perform work in the UI thread and delivers
      * the result from doInBackground() */
    protected void onPostExecute(Bitmap result){
        mImageView.setImageBitmap(result);
    }}

現在UI是安全的了,程式碼也更加簡單了,因為AsyncTask把worker執行緒裡做的事和UI執行緒裡要做的事分開了。

你應該閱讀一下 AsyncTask 的參考文件以便更好的使用它。下面就是一個對 AsyncTask 如何作用的快速的總覽:

注意: 你在使用worker執行緒的時候可能會碰到的另一個問題就是因為runtime configuration change (比如使用者改變了螢幕的方向)導致你的activity不可預期的重啟,這可能會kill掉你的worker執行緒。為了解決這個問題你可以參考 Shelves 這個專案。

執行緒安全的方法

在某些情況下,你實現的方法可能會被多個執行緒所呼叫,因此你必須把它寫出執行緒安全的。

相關文章