了解 Android 的進程和線程

前言:本文所寫的是博主的個人見解,如有錯誤或者不恰當之處,歡迎私信博主,加以改正!原文鏈接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

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,578評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,701評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,691評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,974評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,694評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,026評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,015評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,193評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,719評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,442評論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,668評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,151評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,846評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,255評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,592評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,394評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,635評論 2 380

推薦閱讀更多精彩內容