Android開發之UI線程和非UI線程

這裏又是老生暢談的話了,前邊已經有多篇文章針對線程進行探究解釋,Android開發過程中線程的體現更是淋漓盡致。Android開發過程中涉及到的線程從大類上分可以歸爲兩類:UI線程和非UI線程。本篇就根據這兩類做一個總結。

談到線程,首先順帶講一下Android上進程的相關知識,進程和線程是相輔相成的,前邊我也寫過一篇針對進程和線程概括性的解釋——《什麼是進程,什麼是線程》,這裏就針對Android上面向開發的做一個記錄總結。

當某個應用組件啓動且該應用沒有運行其他任何組件時,Android 系統會使用單個執行線程爲應用啓動新的 Linux 進程。默認情況下,同一應用的所有組件在相同的進程和線程(稱爲“主”線程)中運行。 如果某個應用組件啓動且該應用已存在進程(因爲存在該應用的其他組件),則該組件會在此進程內啓動並使用相同的執行線程。 但是,您可以安排應用中的其他組件在單獨的進程中運行,併爲任何進程創建額外的線程。

進程

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

各類組件元素的清單文件條目、、和均支持 android:process 屬性,此屬性可以指定該組件應在哪個進程運行。此外, 元素還支持 android:process 屬性,以設置適用於所有組件的默認值。

如果內存不足,而其他爲用戶提供更緊急服務的進程又需要內存時,Android 可能會決定在某一時刻關閉某一進程。在被終止進程中運行的應用組件也會隨之銷燬。 當這些組件需要再次運行時,系統將爲它們重啓進程。

進程生命週期

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

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

  1. 前臺進程
    用戶當前操作所必需的進程。如果一個進程滿足以下任一條件,即視爲前臺進程:

    • 託管用戶正在交互的 Activity(已調用 Activity 的 onResume() 方法)
    • 託管某個 Service,後者綁定到用戶正在交互的 Activity
    • 託管正在“前臺”運行的 Service(服務已調用 startForeground())
    • 託管正執行一個生命週期回調的 Service(onCreate()、onStart() 或 onDestroy())
    • 託管正執行其 onReceive() 方法的 BroadcastReceiver

通常,在任意給定時間前臺進程都爲數不多。只有在內存不足以支持它們同時繼續運行這一萬不得已的情況下,系統纔會終止它們。 此時,設備往往已達到內存分頁狀態,因此需要終止一些前臺進程來確保用戶界面正常響應。

  1. 可見進程
    沒有任何前臺組件、但仍會影響用戶在屏幕上所見內容的進程。 如果一個進程滿足以下任一條件,即視爲可見進程:

    • 託管不在前臺、但仍對用戶可見的 Activity(已調用其 onPause() 方法)。例如,如果前臺 Activity 啓動了一個對話框,允許在其後顯示上一 Activity,則有可能會發生這種情況。
    • 託管綁定到可見(或前臺)Activity 的 Service。

可見進程被視爲是極其重要的進程,除非爲了維持所有前臺進程同時運行而必須終止,否則系統不會終止這些進程。

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

  2. 後臺進程
    包含目前對用戶不可見的 Activity 的進程(已調用 Activity 的 onStop() 方法)。這些進程對用戶體驗沒有直接影響,系統可能隨時終止它們,以回收內存供前臺進程、可見進程或服務進程使用。 通常會有很多後臺進程在運行,因此它們會保存在 LRU (最近最少使用)列表中,以確保包含用戶最近查看的 Activity 的進程最後一個被終止。如果某個 Activity 正確實現了生命週期方法,並保存了其當前狀態,則終止其進程不會對用戶體驗產生明顯影響,因爲當用戶導航回該 Activity 時,Activity 會恢復其所有可見狀態。 有關保存和恢復狀態的信息,請參閱 Activity文檔。

  3. 空進程
    不含任何活動應用組件的進程。保留這種進程的的唯一目的是用作緩存,以縮短下次在其中運行組件所需的啓動時間。 爲使總體系統資源在進程緩存和底層內核緩存之間保持平衡,系統往往會終止這些進程。

主線程(UI線程)

應用啓動時,系統會爲應用創建一個名爲“主線程”的執行線程。 此線程非常重要,因爲它負責將事件分派給相應的用戶界面小部件,其中包括繪圖事件。 此外,它也是應用與 Android UI 工具包組件(來自 android.widget 和 android.view 軟件包的組件)進行交互的線程。因此,主線程有時也稱爲 UI 線程。

系統不會爲每個組件實例創建單獨的線程。運行於同一進程的所有組件均在 UI 線程中實例化,並且對每個組件的系統調用均由該線程進行分派。 因此,響應系統回調的方法(例如,報告用戶操作的 onKeyDown() 或生命週期回調方法)始終在進程的 UI 線程中運行。

主線程是不安全的

android主線程是不安全的,這句話是不是經常聽到?上邊也說了所有相關的UI更新操作均在主線程(UI線程),爲什麼都要在主線程中完成呢?這裏就是主線程不全性導致的,其實主線程的不安全性就是指的UI刷新界面展示的不安全性。前邊一篇文章《Android開發Handler消息機制探究》開篇提到,從主線程中可以創建多個子線程來分配任務,一個activity的所有view都是唯一的,都有唯一的標識,如果在每個子線程中更新view,我們不能預知線程執行結果的先後順序,也就無法預知什麼時候才能更新view,所以造成結果就是view更新時的衝突問題。官方也是爲了規避這種多線程執行無序導致衝突的問題,所以從安卓2.0之後規定只能在主線程中更新界面了。

單線程模式下必須遵守兩條規則

現在又有一個新問題了,既然都要在主線程中去執行有關界面的更新操作,主線程勢必給人感覺比較“重”,在應用執行繁重的任務以響應用戶交互時,除非正確實現應用,否則這種單線程模式可能會導致性能低下。 具體地講,如果 UI 線程需要處理所有任務,則執行耗時很長的操作(例如,網絡訪問或數據庫查詢)將會阻塞整個 UI。 一旦線程被阻塞,將無法分派任何事件,包括繪圖事件。 從用戶的角度來看,應用顯示爲掛起。 更糟糕的是,如果 UI 線程被阻塞超過幾秒鐘時間(目前大約是 5 秒鐘),用戶就會看到一個讓人厭煩的“應用無響應”(ANR) 對話框。如果引起用戶不滿,他們可能就會決定退出並卸載此應用。

因此,您不得通過工作線程操縱 UI,而只能通過 UI 線程操縱用戶界面。 因此,Android 的單線程模式必須遵守兩條規則:

  1. 不要阻塞UI線程
  2. 不要在UI線程之外訪問Android UI 組件(工具包)

工作線程(非UI線程)

根據上述單線程模式,要保證應用UI的響應能力,關鍵是不能阻塞 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 線程:

  • 使用Handler實現線程之間的通信
  • 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();
}

AsyncTask

少量情況下非UI線程訪問UI線程可以採用上邊的Activity.runOnUiThread(Runnable)View.post(Runnable),多的情況下可以採用Handler+Thread方式,但是這種也不好,代碼量太大。好在Android官方給我們封了一個可以異步處理並在UI線程中回調的類——AsyncTask,AsyncTask可以正確,方便地使用UI線程。此類允許您執行後臺操作並在UI線程上發佈結果,而無需操作線程和/或處理程序。

AsyncTask的設計其實也是圍繞一個輔助類Thread和Handler,AsyncTask主要用於短時間內的異步回調操作,如果長時間執行線程,還是強烈建議採用各種API java.util.concurrent包,如ExecutorThreadPoolExecutorFutureTask

AsyncTask的泛型參數

AsyncTask<Params,Progress,Result>是一個抽象類,通常用於被繼承.繼承AsyncTask需要指定如下三個泛型參數:

  • Params:啓動任務執行時輸入的參數類型.
  • Progress:後臺任務執行中返回進度值的類型.
  • Result:後臺任務執行完成後返回結果的類型.

AsyncTask主要方法

AsyncTask主要有如下幾個方法:

  • doInBackground:必須重寫,異步執行後臺線程要完成的任務,耗時操作將在此方法中完成.
  • onPreExecute:執行後臺耗時操作前被調用,通常用於進行初始化操作.
  • onPostExecute:當doInBackground方法完成後,系統將自動調用此方法,並將doInBackground方法返回的值傳入此方法.通過此方法進行UI的更新.
  • onProgressUpdate:當在doInBackground方法中調用publishProgress方法更新任務執行進度後,將調用此方法.通過此方法我們可以知曉任務的完成進度.

AsyncTask使用遵循規則及缺點

使用AsyncTask時必須遵循如下規則:

  • 必須在UI線程中創建AsyncTask的實例
  • 必須在UI線程中調用AsyncTask的execute()方法
  • 重寫的四個方法是系統自動調用的,不應手動調用
  • 每個AsyncTask只能被執行一次,多次調用將會引發異常

AsyncTask使用時雖然很簡單,並且塊化好管理,但是AsyncTask也有一定的缺點,使用的過程中也要格外在意:

  1. 線程池中已經有128個線程,緩衝隊列已滿,如果此時向線程提交任務,將會拋出RejectedExecutionException。過多的線程會引起大量消耗系統資源和導致應用FC的風險。
  2. AsyncTask不會隨着Activity的銷燬而銷燬,直到doInBackground()方法執行完畢。如果我們的Activity銷燬之前,沒有取消 AsyncTask,這有可能讓我們的AsyncTask崩潰(crash)。因爲它想要處理的view已經不存在了。所以,我們總是必須確保在銷燬活動之前取消任務。如果在doInBackgroud裏有一個不可中斷的操作,比如BitmapFactory.decodeStream(),調用了cancle() 也未必能真正地取消任務。關於這個問題,在4.4後的AsyncTask中,都有判斷是取消的方法isCancelled()。
  3. 如果AsyncTask被聲明爲Activity的非靜態的內部類,那麼AsyncTask會保留一個對創建了AsyncTask的Activity的引用。如果Activity已經被銷燬,AsyncTask的後臺線程還在執行,它將繼續在內存裏保留這個引用,導致Activity無法被回收,引起內存泄露。
  4. 屏幕旋轉或Activity在後臺被系統殺掉等情況會導致Activity的重新創建,之前運行的AsyncTask會持有一個之前Activity的引用,這個引用已經無效,這時調用onPostExecute()再去更新界面將不再生效。

AsyncTask簡單使用

以下是一個AsyncTask創建聲明的例子:

 private class DownloadFilesTask extends AsyncTask<URL, Integer, Long> {
     protected Long doInBackground(URL... urls) {
         int count = urls.length;
         long totalSize = 0;
         for (int i = 0; i < count; i++) {
             totalSize += Downloader.downloadFile(urls[i]);
             publishProgress((int) ((i / (float) count) * 100));
             // Escape early if cancel() is called
             if (isCancelled()) break;
         }
         return totalSize;
     }

     protected void onProgressUpdate(Integer... progress) {
         setProgressPercent(progress[0]);
     }

     protected void onPostExecute(Long result) {
         showDialog("Downloaded " + result + " bytes");
     }
 }

聲明好後,只需要在主線程中簡單執行execute即可:

new DownloadFilesTask().execute(url1, url2, url3);

參考

  • https://developer.android.com/guide/components/processes-and-threads.html
  • https://developer.android.com/reference/android/os/AsyncTask.html
  • 《瘋狂Android講義》——李剛
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章