Android在橫豎屏切換時到底發生了什麼?

發表於2015-01-30

之前的一篇文章我們深入loopers和handler進行分析,看它們是如何同Android主執行緒相關聯的。

今天,我們將繼續深入Android主執行緒同Android元件生命週期的互動。

Activity同orientation changes之間的關係

首先來看看Activity的生命週期和它處理configuration changes 的神奇之處。

這篇文章主要來自於一段類似下面的程式碼:

通過上面的程式碼我們知道, doSomething() 方法會在Activity因為一個configuration change導致 onDestroy() 方法會被呼叫之後執行。之後,你也不能再使用Activity這個例項了。

基於orientation changes的refresher

裝置的 orientation 回來任意時刻發生改變。我們會在一個Activity被建立的時候通過Activity#setRequestedOrientation(int)方法來模擬一個orientation change。

你能預測當這個Activity處於portrait模式時log輸出嗎?

如果你瞭解 Android 生命週期, 你的預測結果可能是下面的答案:

Android的生命週期正常運轉,Activity被created,resumed,然後這個時候orientation change 發生了,Activity被 paused, destroyed,接著一個新的Activity被created 和 resumed。

Orientation changes 和Android主執行緒

此處有一個重要的細節:一個orientation change 導致Activity 被重新建立是通過向Android主執行緒的訊息佇列傳送了一個簡單的訊息。

請看下面通過反射來讀取主執行緒訊息佇列裡面內容的spy程式碼:

從上面可以看出,一個訊息佇列僅僅只是一個連結串列,每一個訊息都有一個指向下一個訊息的引用。

我們可以通過上面的程式碼來記錄orientation change發生之後訊息佇列裡面的內容:

最後的輸出是:

我們可以通過ActivityThread 類的原始碼知道數字 118 和 126 代表的訊息是:

一個orientation change 會往Android主執行緒的訊息佇列裡面新增一個 CONFIGURATION_CHANGED 和一個 RELAUNCH_ACTIVITY 訊息。

讓我們會退一步來看看到底發生了什麼:

當Activity第一次啟動的時候,主執行緒的訊息佇列是空的。當前要執行的訊息就是 LAUNCH_ACTIVITY,即建立一個Activity例項,先後呼叫 onCreate() 方法和onResume() 方法。接下來只有主執行緒的looper才能處理訊息佇列裡面下一個訊息。

當裝置檢測到有一個 orientation change 時,會有一個 RELAUNCH_ACTIVITY 訊息被推送到主執行緒的訊息佇列。

當這個訊息被處理的時候它會:

  • 在老的Activity例項上呼叫 onSaveInstanceState()onPause()onDestroy() 方法,
  • 建立一個新的Activity例項,
  • 在新的Activity例項上呼叫 onCreate() 和onResume() 方法。

上面這些都是一個訊息要處理的。與此同時發生所有訊息都會在新的Activity執行完 onResume() 方法後才被呼叫的。

全面測試

如果你在一個orientation change發生期間在 onCreate() 方法中向主執行緒訊息佇列傳送訊息會怎麼樣?這會有兩種情況,在orientation change發生之前和之後:

輸出結果為:

有上面的結果我們可得知:在執行到onCreate()方法最後時,訊息佇列裡面包含四個訊息。第一個就是orientation change 發生之前傳送的訊息,接著兩個訊息為orientation change相關的,最後一個訊息就是orientation change發生之後要傳送的訊息。最後的logs 表明這些訊息都是按順序依次被執行的。

而且,任何在orientation change 之前傳送的訊息都會在Activity離開時呼叫的 onPause() 之前被處理,任何在orientation change 之後傳送的訊息都會在新的Activity呼叫的 onResume() 之後被處理。

這個實驗告訴我們當你傳送一個訊息的時候,你不能保證當時存在的Activity例項在訊息處理完之後還會存在(即使你是在 onCreate() 或者 onResume()方法裡面傳送的)。如過你在Activity A傳送的訊息B裡面還持有一個view C或者一個 Activity D的引用,那麼這個Activity A在訊息 B被處理之前是不會被垃圾回收的。

那麼我們可以如何解決這樣的問題呢?What could you do?

The real fix解決方案

當你處於主執行緒的時候不要呼叫 handler.post() 方法。大部分情況下,handler.post() 被用來處理順序問題。為了保證你的應用架構穩定請慎用handler.post() 。

如果你真的需要使用handler.post()

保證你的訊息不要持有一個Activity例項的引用?Make sure your message does not hold a reference to an activity, as you would do for a background operation.

如果你真的需要使用handler.post()還要保持Activity引用

在Activity的onPause()方法裡面通過 handler.removeCallbacks() 方法從訊息佇列移除這個訊息。

如果你真的需要讓你的訊息被及時處理

使用 handler.postAtFrontOfQueue() 方法保證訊息在 onPause() 傳送,那麼這個訊息會在 onPause()之前被處理。但是你的程式碼將會變得很難懂。說真的,別這樣幹。

關於runOnUiThread()的一些話

你注意到我們建立一個handler然後使用 handler.post() 方法而不是直接呼叫Activity.runOnUiThread()方法嗎?

原因是這樣的:

不像 handler.post()runOnUiThread() 在當前執行緒為主執行緒的情況下是不會傳送這個runnable的,它會同步地呼叫 run() 方法。

Services

有一個誤解一直需要解除:service 不是 執行在一個後臺執行緒。

所有的 service 生命週期方法 (onCreate()onStartCommand(), 等等) 都是執行在主執行緒上面 (就是那個執行你的Activities各種有趣動畫的執行緒)。

無論你是出於一個 service 還是 activity,長時間任務必須要在後臺執行緒進行處理。這個後臺執行緒可以和你的應用生命週期一樣長,即使你的Activities已經銷燬了。

然而,Android可以在任意時刻決定 kill 一個應用程式。一個 service 是一種請求系統讓我們可以存活更久,同時在系統要kill 程式的時候可以讓這個service知道。

注意: 當onBind() 接受到一個其他程式的呼叫並返回一個IBinder ,那麼這個方法會在後臺執行緒裡面執行。

你可以花時間讀讀 Service documentation –相當不錯。

IntentService

IntentService 為在後臺執行緒依次執行一個Intent 佇列裡面所有Intent提供了一個簡單的方式。

在它內部,使用了一個 Looper 在一個專用 HandlerThread 的來處理這些Intents。當這個service 被銷燬的時候,這個Looper會讓你結束當前intent的處理,然後終結這個後臺執行緒。

總結

大部分的Android 生命週期方法都是在主執行緒被呼叫的。你可以把這些回撥函式當成傳送給一個looper 佇列的簡單訊息。

在此文結束之前還是要像大多數的Android開發部落格那樣強調:不要阻塞Android主執行緒。

你是不是想知道阻塞了主執行緒到底意味著什麼?哈哈,這是下一個主題了。

相關文章