Android程式設計師必須掌握的知識點-多程式和多執行緒

jamesehng發表於2016-12-07

當某個應用元件啟動且該應用沒有執行其他任何元件時,Android 系統會使用單個執行執行緒為應用啟動新的 Linux 程式。預設情況下,同一應用的所有元件在相同的程式和執行緒(稱為“主”執行緒)中執行。 如果某個應用元件啟動且該應用已存在程式(因為存在該應用的其他元件),則該元件會在此程式內啟動並使用相同的執行執行緒。 但是,您可以安排應用中的其他元件在單獨的程式中執行,併為任何程式建立額外的執行緒。

本文介紹程式和執行緒在 Android 應用中的工作方式。

程式

預設情況下,同一應用的所有元件均在相同的程式中執行,且大多數應用都不會改變這一點。 但是,如果您發現需要控制某個元件所屬的程式,則可在清單檔案中執行此操作。

各類元件元素的清單檔案條目—activity、service、receiver 和 provider—均支援 android:process 屬性,此屬性可以指定該元件應在哪個程式執行。您可以設定此屬性,使每個元件均在各自的程式中執行,或者使一些元件共享一個程式,而其他元件則不共享。 此外,您還可以設定 android:process,使不同應用的元件在相同的程式中執行,但前提是這些應用共享相同的 Linux 使用者 ID 並使用相同的證書進行簽署。

此外,application 元素還支援 android:process 屬性,以設定適用於所有元件的預設值。

如果記憶體不足,而其他為使用者提供更緊急服務的程式又需要記憶體時,Android 可能會決定在某一時刻關閉某一程式。在被終止程式中執行的應用元件也會隨之銷燬。 當這些元件需要再次執行時,系統將為它們重啟程式。

決定終止哪個程式時,Android 系統將權衡它們對使用者的相對重要程度。例如,相對於託管可見 Activity 的程式而言,它更有可能關閉託管螢幕上不再可見的 Activity 的程式。 因此,是否終止某個程式的決定取決於該程式中所執行元件的狀態。 下面,我們介紹決定終止程式所用的規則。

程式生命週期

Android 系統將盡量長時間地保持應用程式,但為了新建程式或執行更重要的程式,最終需要移除舊程式來回收記憶體。 為了確定保留或終止哪些程式,系統會根據程式中正在執行的元件以及這些元件的狀態,將每個程式放入“重要性層次結構”中。 必要時,系統會首先消除重要性最低的程式,然後是重要性略遜的程式,依此類推,以回收系統資源。

重要性層次結構一共有 5 級。

以下列表按照重要程度列出了各類程式(第一個程式最重要,將是最後一個被終止的程式):

前臺程式

使用者當前操作所必需的程式。如果一個程式滿足以下任一條件,即視為前臺程式: 託管使用者正在互動的 Activity(已呼叫 Activity 的 onResume() 方法) 託管某個 Service,後者繫結到使用者正在互動的 Activity 託管正在“前臺”執行的 Service(服務已呼叫 startForeground()) 託管正執行一個生命週期回撥的 Service(onCreate()、onStart() 或 onDestroy()) 託管正執行其 onReceive() 方法的 BroadcastReceiver 通常,在任意給定時間前臺程式都為數不多。只有在記憶體不足以支援它們同時繼續執行這一萬不得已的情況下,系統才會終止它們。 此時,裝置往往已達到記憶體分頁狀態,因此需要終止一些前臺程式來確保使用者介面正常響應。

可見程式

沒有任何前臺元件、但仍會影響使用者在螢幕上所見內容的程式。 如果一個程式滿足以下任一條件,即視為可見程式: 託管不在前臺、但仍對使用者可見的 Activity(已呼叫其 onPause() 方法)。例如,如果前臺 Activity 啟動了一個對話方塊,允許在其後顯示上一 Activity,則有可能會發生這種情況。 託管繫結到可見(或前臺)Activity 的 Service。 可見程式被視為是極其重要的程式,除非為了維持所有前臺程式同時執行而必須終止,否則系統不會終止這些程式。

服務程式

正在執行已使用 startService() 方法啟動的服務且不屬於上述兩個更高類別程式的程式。儘管服務程式與使用者所見內容沒有直接關聯,但是它們通常在執行一些使用者關心的操作(例如,在後臺播放音樂或從網路下載資料)。因此,除非記憶體不足以維持所有前臺程式和可見程式同時執行,否則系統會讓服務程式保持執行狀態。

後臺程式

包含目前對使用者不可見的 Activity 的程式(已呼叫 Activity 的 onStop() 方法)。這些程式對使用者體驗沒有直接影響,系統可能隨時終止它們,以回收記憶體供前臺程式、可見程式或服務程式使用。 通常會有很多後臺程式在執行,因此它們會儲存在 LRU (最近最少使用)列表中,以確保包含使用者最近檢視的 Activity 的程式最後一個被終止。如果某個 Activity 正確實現了生命週期方法,並儲存了其當前狀態,則終止其程式不會對使用者體驗產生明顯影響,因為當使用者導航回該 Activity 時,Activity 會恢復其所有可見狀態。 有關儲存和恢復狀態的資訊,請參閱 Activity文件。

空程式

不含任何活動應用元件的程式。保留這種程式的的唯一目的是用作快取,以縮短下次在其中執行元件所需的啟動時間。 為使總體系統資源在程式快取和底層核心快取之間保持平衡,系統往往會終止這些程式。 根據程式中當前活動元件的重要程度,Android 會將程式評定為它可能達到的最高階別。例如,如果某程式託管著服務和可見 Activity,則會將此程式評定為可見程式,而不是服務程式。

此外,一個程式的級別可能會因其他程式對它的依賴而有所提高,即服務於另一程式的程式其級別永遠不會低於其所服務的程式。 例如,如果程式 A 中的內容提供程式為程式 B 中的客戶端提供服務,或者如果程式 A 中的服務繫結到程式 B 中的元件,則程式 A 始終被視為至少與程式 B 同樣重要。

由於執行服務的程式其級別高於託管後臺 Activity 的程式,因此啟動長時間執行操作的 Activity 最好為該操作啟動服務,而不是簡單地建立工作執行緒,當操作有可能比 Activity 更加持久時尤要如此。例如,正在將圖片上傳到網站的 Activity 應該啟動服務來執行上傳,這樣一來,即使使用者退出 Activity,仍可在後臺繼續執行上傳操作。使用服務可以保證,無論 Activity 發生什麼情況,該操作至少具備“服務程式”優先順序。 同理,廣播接收器也應使用服務,而不是簡單地將耗時冗長的操作放入執行緒中。

執行緒

應用啟動時,系統會為應用建立一個名為“主執行緒”的執行執行緒。 此執行緒非常重要,因為它負責將事件分派給相應的使用者介面小部件,其中包括繪圖事件。 此外,它也是應用與 Android UI 工具包元件(來自 android.widget 和 android.view 軟體包的元件)進行互動的執行緒。因此,主執行緒有時也稱為 UI 執行緒。

系統不會為每個元件例項建立單獨的執行緒。執行於同一程式的所有元件均在 UI 執行緒中例項化,並且對每個元件的系統呼叫均由該執行緒進行分派。 因此,響應系統回撥的方法(例如,報告使用者操作的 onKeyDown() 或生命週期回撥方法)始終在程式的 UI 執行緒中執行。

例如,當使用者觸控螢幕上的按鈕時,應用的 UI 執行緒會將觸控事件分派給小部件,而小部件反過來又設定其按下狀態,並將失效請求釋出到事件佇列中。 UI 執行緒從佇列中取消該請求並通知小部件應該重繪自身。

在應用執行繁重的任務以響應使用者互動時,除非正確實現應用,否則這種單執行緒模式可能會導致效能低下。 具體地講,如果 UI 執行緒需要處理所有任務,則執行耗時很長的操作(例如,網路訪問或資料庫查詢)將會阻塞整個 UI。 一旦執行緒被阻塞,將無法分派任何事件,包括繪圖事件。 從使用者的角度來看,應用顯示為掛起。 更糟糕的是,如果 UI 執行緒被阻塞超過幾秒鐘時間(目前大約是 5 秒鐘),使用者就會看到一個讓人厭煩的“應用無響應”(ANR) 對話方塊。如果引起使用者不滿,他們可能就會決定退出並解除安裝此應用。

此外,Android UI 工具包並非執行緒安全工具包。因此,您不得通過工作執行緒操縱 UI,而只能通過 UI 執行緒操縱使用者介面。 因此,Android 的單執行緒模式必須遵守兩條規則:

不要阻塞 UI 執行緒

不要在 UI 執行緒之外訪問 Android UI 工具包

工作執行緒

根據上述單執行緒模式,要保證應用 UI 的響應能力,關鍵是不能阻塞 UI 執行緒。 如果執行的操作不能很快完成,則應確保它們在單獨的執行緒(“後臺”或“工作”執行緒)中執行。

例如,以下程式碼演示了一個點選偵聽器從單獨的執行緒下載影像並將其顯示在 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 工具包 — 此示例從工作執行緒(而不是 UI 執行緒)修改了 ImageView。 這可能導致出現不明確、不可預見的行為,但要跟蹤此行為困難而又費時。

為解決此問題,Android 提供了幾種途徑來從其他執行緒訪問 UI 執行緒。

以下列出了幾種有用的方法:

Activity.runOnUiThread(Runnable) View.post(Runnable) View.postDelayed(Runnable, long)

例如,您可以通過使用 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。

但是,隨著操作日趨複雜,這類程式碼也會變得複雜且難以維護。 要通過工作執行緒處理更復雜的互動,可以考慮在工作執行緒中使用 Handler 處理來自 UI 執行緒的訊息。當然,最好的解決方案或許是擴充套件 AsyncTask 類,此類簡化了與 UI 進行互動所需執行的工作執行緒任務。

使用 AsyncTask

AsyncTask 允許對使用者介面執行非同步操作。 它會先阻塞工作執行緒中的操作,然後在 UI 執行緒中釋出結果,而無需您親自處理執行緒和/或處理程式。

要使用它,必須建立 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 是安全的,程式碼也得到簡化,因為任務分解成了兩部分:一部分應在工作執行緒內完成,另一部分應在 UI 執行緒內完成。

下面簡要概述了 AsyncTask 的工作方法,但要全面瞭解如何使用此類,您應閱讀 AsyncTask 參考文件:

可以使用泛型指定引數型別、進度值和任務最終值 方法 doInBackground() 會在工作執行緒上自動執行 onPreExecute()、onPostExecute() 和 onProgressUpdate() 均在 UI 執行緒中呼叫 doInBackground() 返回的值將傳送到 onPostExecute() 您可以隨時在 doInBackground() 中呼叫publishProgress(),以在 UI 執行緒中執行 onProgressUpdate() 您可以隨時取消任何執行緒中的任務 注意:使用工作執行緒時可能會遇到另一個問題,即:執行時配置變更(例如,使用者更改了螢幕方向)導致 Activity 意外重啟,這可能會銷燬工作執行緒。 要了解如何在這種重啟情況下堅持執行任務,以及如何在 Activity 被銷燬時正確地取消任務,請參閱書架示例應用的原始碼。

執行緒安全方法

在某些情況下,您實現的方法可能會從多個執行緒呼叫,因此編寫這些方法時必須確保其滿足執行緒安全的要求。

這一點主要適用於可以遠端呼叫的方法,如繫結服務中的方法。如果對 IBinder 中所實現方法的呼叫源自執行 IBinder 的同一程式,則該方法在呼叫方的執行緒中執行。但是,如果呼叫源自其他程式,則該方法將在從執行緒池選擇的某個執行緒中執行(而不是在程式的 UI 執行緒中執行),執行緒池由系統在與 IBinder 相同的程式中維護。 例如,即使服務的 onBind() 方法將從服務程式的 UI 執行緒呼叫,在 onBind() 返回的物件中實現的方法(例如,實現 RPC 方法的子類)仍會從執行緒池中的執行緒呼叫。 由於一個服務可以有多個客戶端,因此可能會有多個池執行緒在同一時間使用同一 IBinder 方法。因此,IBinder 方法必須實現為執行緒安全方法。

同樣,內容提供程式也可接收來自其他程式的資料請求。儘管 ContentResolver 和 ContentProvider 類隱藏瞭如何管理程式間通訊的細節,但響應這些請求的 ContentProvider 方法(query()、insert()、delete()、update() 和 getType() 方法)將從內容提供程式所在程式的執行緒池中呼叫,而不是從程式的 UI 執行緒呼叫。 由於這些方法可能會同時從任意數量的執行緒呼叫,因此它們也必須實現為執行緒安全方法。

程式間通訊

Android 利用遠端過程呼叫 (RPC) 提供了一種程式間通訊 (IPC) 機制,通過這種機制,由 Activity 或其他應用元件呼叫的方法將(在其他程式中)遠端執行,而所有結果將返回給呼叫方。 這就要求把方法呼叫及其資料分解至作業系統可以識別的程度,並將其從本地程式和地址空間傳輸至遠端程式和地址空間,然後在遠端程式中重新組裝並執行該呼叫。 然後,返回值將沿相反方向傳輸回來。 Android 提供了執行這些 IPC 事務所需的全部程式碼,因此您只需集中精力定義和實現 RPC 程式設計介面即可。

要執行 IPC,必須使用 bindService() 將應用繫結到服務上。

相關文章