瞭解 Android 的程式和執行緒

PassersHowe發表於2019-01-18

前言:本文所寫的是博主的個人見解,如有錯誤或者不恰當之處,歡迎私信博主,加以改正!原文連結demo連結

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

程式

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

各類元件元素的清單檔案條目 <activity><service><receiver><provider> 均支援 android:process 屬性,該屬性可以指定元件應該執行在哪個程式。可以設定此屬性,使每個元件在各自的程式中執行,或者使一些元件共享一個程式,而其他元件則不共享。此外,還可以設定 android:process ,應用共享具有相同的 Linux 使用者 ID 和相同的證照籤署,使不同應用的元件在相同的程式中執行。

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

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

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

程式的生命週期

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

重要性層次結構一共有5級。下面的列表按照重要程度列出了各類程式(第一個程式最重要,會是最後被終止的程式):

  1. 前臺程式

    使用者當前操作所必需的程式。如果一個程式滿足以下任意一個條件,即視為前臺程式:

    • 託管於使用者正在互動的 Activity (已呼叫 Activity 的 onResume() 方法)
    • 託管於某個 Service,後者繫結到使用者正在互動的 Activity
    • 託管於正在前臺執行的 Service (服務已呼叫 startForeground())
    • 託管於正在執行一個生命週期回撥的 Service (onCreate()、onStart() 或 onDestroy())
    • 託管於正在執行 onReceive() 方法的 BroadcastReceiver

    通常在任意給定時間前臺程式都為數不多。只有在記憶體不足以支援它們同時繼續執行,不得已的情況下,系統才會終止它們。此時,,裝置往往已達到記憶體分頁狀態,需要終止一些前臺程式來確保使用者介面正常響應。

  2. 可見程式

    沒有任何前臺元件、但會影響使用者在螢幕上所見內容的程式。如果一個程式滿足以下任一條件,即視為可見程式:

    • 託管不在前臺、但仍對客戶可見的 Activity (已呼叫其 onPause()方法)。例如,如果前臺 Activity 啟動一個對話方塊,允許在其後顯示一個 Activity ,則有可能會發生這種情況。
    • 託管繫結到可見(或前臺) Activity 的 Service
      可見程式被視為極其重要的程式,除非是為了維持所有前臺程式同時執行而必須終止,否則系統不會終止這些程式。
  3. 服務程式

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

  4. 後臺程式

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

  5. 空程式
    不含任何活動應用元件的程式。 保留這種程式的唯一目的是用作快取,以縮短下次在其執行元件所需的啟動時間。為使總體系統資源在程式快取和底層內了快取之間保持平衡,系統往往會終止這些程式。

根據程式中當前活動元件的重要程度, 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 的單執行緒模式必須遵守兩條規則:

  1. 不要阻塞 UI 執行緒
  2. 不要在 UI 執行緒之外訪問 Android UI 工具包

工作執行緒

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

例如,下面演示了一個點選監聽器從單獨的執行緒下載圖片並將其顯示在 ImageView 中:

   @Override
   public void onClick(View v) {
       new Thread(new Runnable() {
           @Override
           public void run() {
               final Bitmap bitmap = loadImageFromNetwork(url);
               showImage.setImageBitmap(bitmap);
           }
       }).start();
   }複製程式碼

這段程式碼看起來似乎執行良好,因為它建立了一個新的執行緒來處理網路操作。但是它違背了單執行緒模式的第二條規則:不要在 UI 執行緒之外訪問 Android UI 工具包,此示例從工作執行緒(而不是 UI )執行緒修改了 ImageView 。這個可能導致出現不明確,不可預見的行為,但是要跟蹤這個行為既困難又費時。

未解決此問題,Android 提供了幾種途徑從其他執行緒訪問 UI 執行緒。以下列出幾種有用的方法:

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

例如你可以通過使用 Activity.runOnUiThread(Runnable) 方法修復上面的程式碼:

   @Override
   public void onClick(View v) {
       new Thread(new Runnable() {
           @Override
           public void run() {
               final Bitmap bitmap = loadImageFromNetwork(url);
               MainActivity.this.runOnUiThread(new Runnable() {
                   @Override
                   public void run() {
                       showImage.setImageBitmap(bitmap);
                   }
               });

           }
       }).start();
   }複製程式碼

上面的實現屬於執行緒安全型:在單獨的執行緒中完成網路操作,而在 UI 執行緒中操作 ImageView。

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

使用 AsyncTask

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

要使用它,必須建立 AsyncTask 的子類,並實現 odInBrackground() 回撥方法,該方法將在後臺執行緒池中執行,需要更新 UI 則要實現 onPostExecute(),傳遞 odInBrackground() 返回的結果並在 UI 執行緒中執行,使之更安全地更新 UI,可以通過從 UI 執行緒中呼叫 execute() 來執行任務。

例如,下面使用 AsyncTask 來實現上述的示例:

Activity 裡呼叫 execute() 方法

@Override
            public void onClick(View v) {
                new DownloadImageTask(showImage).execute(url);
            }複製程式碼

繼承 AsyncTask 實現非同步載入

public class DownloadImageTask extends AsyncTask<String, Void, Bitmap> {

    private ImageView mImageView;
    public DownloadImageTask() {
    }

    public DownloadImageTask(ImageView imageView) {
        mImageView = imageView;
    }
    @Override
    protected Bitmap doInBackground(String... params) {
        return DownImageUtil.getInstance().loadImageFromNetwork(params[0]);
    }

    @Override
    protected void onPostExecute(Bitmap bitmap) {
        if (mImageView != null) {
            mImageView.setImageBitmap(bitmap);
        }
    }
}複製程式碼

現在 UI 是安全的,程式碼也得到簡化,任務分解成了兩部分:一部分在工作執行緒內完成,另一部分在 UI 執行緒內完成。

下面簡單的介紹 AsyncTask 的工作方法:

  • 可以使用泛型指定引數型別、進度值和任務最終值
  • 方法 doInBackground() 會在工作執行緒上自動執行
  • onPreExecute() 、onPostExecute() 和 onProgressUpdate() 均在 UI 執行緒中呼叫
  • doInBackground() 返回值將傳送到 onPostExecute()
  • 可以隨時在 doInBackground() 中呼叫 publishProgress() ,以在 UI 執行緒中執行 onProgressUpdate()
  • 可以隨時取消任何執行緒中的任務

注意:使用工作執行緒時有可能遇到另一個問題,即:執行時配置變更(例如,使用者更改了螢幕方向),導致 Activity 意外重啟,這可能會銷燬工作執行緒。

執行緒安全方法

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

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

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

程式間通訊

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

需要執行 IPC,必須使用 bindService() 將應用繫結到服務上,想了解詳細的資訊,可以參考
淺談 Android Service

相關文章