【翻譯】configuration changes與handler.post

 

在前一部分裏面previous part ,咱們深刻挖掘了 looper 和 handler的處理機制,以及他們是怎麼與Anroid主線程關聯起來的。html

如今,咱們來深刻探討一下主線程與 安卓組件的生命週期之間是怎麼交互的。java

Activities 屏幕方向變化是一個常見的問題

咱們首先仍是從activity的生命週期,以及activity處理  configuration changes【好比屏幕方向的變化】的機制談起。
 

 

爲何要探討這一問題

本文的初衷來自於 在   Square Register  中真實發生的崩潰bug。下面是引發問題的相關代碼【簡化版】:
public class MyActivity extends Activity {
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Handler handler = new Handler(Looper.getMainLooper());
    handler.post(new Runnable() {
      public void run() {
        doSomething();
      }
    });
  }

  void doSomething() {
    // Uses the activity instance
  }
}
 
 
如咱們所見,當觸發   configuration change事件時, doSomething()是可能在 activity的 onDestroy()方法以後才被調用的,而這時你已經不能再使用activity實例了。

讓咱們再來複習一下orientation changes事件

設備的方向是可能在任什麼時候候被改變的. 咱們使用Activity#setRequestedOrientation(int) 方法來模擬orientation change 行爲。android

你知道下面的log輸出的是啥麼?git

public class MyActivity extends Activity {
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Log.d("Square", "onCreate()");
    if (savedInstanceState == null) {
      Log.d("Square", "Requesting orientation change");
      setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
    }
  }

  protected void onResume() {
    super.onResume();
    Log.d("Square", "onResume()");
  }

  protected void onPause() {
    super.onPause();
    Log.d("Square", "onPause()");
  }

  protected void onDestroy() {
    super.onDestroy();
    Log.d("Square", "onDestroy()");
  }
}

If you know the Android lifecycle, you probably predicted this:app

若是你知道Android lifecycle這篇文章,你可能這麼預測:oop

onCreate()
Requesting orientation change
onResume()
onPause()
onDestroy()
onCreate()
onResume()
created, resumed,而後才考慮orientation change行爲的觸發,這時, activity 再執行 paused, destroyed,而後新的activity從新執行,created 和 resumed方法。The Android Lifecycle goes on normally, the activity is created, resumed, and then the orientation change is taken into account and the activity is paused, destroyed, and a new activity is created and resumed. 正常來講應該是這樣的,activity
 

Orientation changes 和主線程

此處有一個重要的細節須要注意: orientation change行爲會致使activity被從新建立。而這一行爲僅經過post

post一個消息給主線程的方式來實現。
下面咱們能夠經過反射來檢測主循環隊列 中的內容

 

public class MainLooperSpy {
  private final Field messagesField;
  private final Field nextField;
  private final MessageQueue mainMessageQueue;

  public MainLooperSpy() {
    try {
      Field queueField = Looper.class.getDeclaredField("mQueue");
      queueField.setAccessible(true);
      messagesField = MessageQueue.class.getDeclaredField("mMessages");
      messagesField.setAccessible(true);
      nextField = Message.class.getDeclaredField("next");
      nextField.setAccessible(true);
      Looper mainLooper = Looper.getMainLooper();
      mainMessageQueue = (MessageQueue) queueField.get(mainLooper);
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  public void dumpQueue() {
    try {
      Message nextMessage = (Message) messagesField.get(mainMessageQueue);
      Log.d("MainLooperSpy", "Begin dumping queue");
      dumpMessages(nextMessage);
      Log.d("MainLooperSpy", "End dumping queue");
    } catch (IllegalAccessException e) {
      throw new RuntimeException(e);
    }
  }

  public void dumpMessages(Message message) throws IllegalAccessException {
    if (message != null) {
      Log.d("MainLooperSpy", message.toString());
      Message next = (Message) nextField.get(message);
      dumpMessages(next);
    }
  }
}
如你所見,消息隊列僅僅是個鏈表
下面是 orientation change 觸發以後的 消息隊列中的記錄:
public class MyActivity extends Activity {
  private final MainLooperSpy mainLooperSpy = new MainLooperSpy();

  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Log.d("Square", "onCreate()");
    if (savedInstanceState == null) {
      Log.d("Square", "Requesting orientation change");
      setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
      mainLooperSpy.dumpQueue();
    }
  }
}
Here is the output:
onCreate()
Requesting orientation change
Begin dumping queue
{ what=118 when=-94ms obj={1.0 208mcc15mnc en_US ldltr sw360dp w598dp h335dp 320dpi nrml land finger -keyb/v/h -nav/h s.44?spn} }
{ what=126 when=-32ms obj=ActivityRecord{41fd2b48 token=android.os.BinderProxy@41fcce50 no component name} }
End dumping queue

 

經過查看ActivityThread的實現,咱們能夠知道消息118,126表明的是什麼:this

public final class ActivityThread {
  private class H extends Handler {
    public static final int CONFIGURATION_CHANGED   = 118;
    public static final int RELAUNCH_ACTIVITY       = 126;
  }
}

當發起一個orientation change操做時,實際上就是把 CONFIGURATION_CHANGED 和 RELAUNCH_ACTIVITY消息添加到了主循環隊列中。google

 

下面咱們來一步步的分析:spa

當activity第一次啓動時,消息隊列是空的。 當前正在執行的消息是LAUNCH_ACTIVITY。

LAUNCH_ACTIVITY消息的處理過程是這樣的:首先建立一個

activity 實例,而後調用onCreate方法和onResume方法。而後LAUNCH_ACTIVITY消息就算完成了,主循環繼續去運行下一條消息。
When a device orientation change is detected, a  is posted to the queue.RELAUNCH_ACTIVITY
orientation change事件被監測到的時候,RELAUNCH_ACTIVITY消息被添加到隊列中來。
消息處理過程是這樣的:
  • 依次調用舊activity的 onSaveInstanceState()onPause()onDestroy() 方法
  • 建立一個新的activity實例,
  • 依次調用新activity實例的onCreate() 和onResume()方法。
這一過程都包含在一個消息中處理。也就是說,在上述過程當中,任何post調用都會在 方法以後被調用。onResume()

 

如今放到一塊兒來看下

若是你在onCreate中post一個消息, 而此時剛好orientation change事件也被觸發,會出現什麼樣的狀況?

看下面兩個例子,注意orientation change先後的log:

 

public class MyActivity extends Activity {
  private final MainLooperSpy mainLooperSpy = new MainLooperSpy();

  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Log.d("Square", "onCreate()");
    if (savedInstanceState == null) {
      Handler handler = new Handler(Looper.getMainLooper());
      handler.post(new Runnable() {
        public void run() {
          Log.d("Square", "Posted before requesting orientation change");
        }
      });
      Log.d("Square", "Requesting orientation change");
      setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
      handler.post(new Runnable() {
        public void run() {
          Log.d("Square", "Posted after requesting orientation change");
        }
      });
      mainLooperSpy.dumpQueue();
    }
  }

  protected void onResume() {
    super.onResume();
    Log.d("Square", "onResume()");
  }

  protected void onPause() {
    super.onPause();
    Log.d("Square", "onPause()");
  }

  protected void onDestroy() {
    super.onDestroy();
    Log.d("Square", "onDestroy()");
  }
}

Here is the output:

onCreate()
Requesting orientation change
Begin dumping queue
{ what=0 when=-129ms }
{ what=118 when=-96ms obj={1.0 208mcc15mnc en_US ldltr sw360dp w598dp h335dp 320dpi nrml land finger -keyb/v/h -nav/h s.46?spn} }
{ what=126 when=-69ms obj=ActivityRecord{41fd6b68 token=android.os.BinderProxy@41fd0ae0 no component name} }
{ what=0 when=-6ms }
End dumping queue
onResume()
Posted before requesting orientation change
onPause()
onDestroy()
onCreate()
onResume()
Posted after requesting orientation change

 

總結一下:在onCreate方法執行到最後時,消息隊列中含有4個消息。第一個消息時orientation change以前添加的post消息,緊接着的兩個消息是與orientation change相關聯的兩個消息,而後是orientation change以後添加的post消息。能夠在日誌中看到他們是有序進行的。
也就是說,任何在orientation change以前添加的消息都會在onPause調用以前被調用,任何在orientation change以後添加的消息都會在
onResume調用以後被調用。
實際的意義在於,當你發生一個post消息時,你不能保證activity實例在消息執行時還存在(即使你是在  or ).若是你的消息持有View或者activity的引用,則activty將不會被及時回收。
 onCreate()onResume()中調用的post

 

因此,咱們應該怎麼作呢?

完全的解決辦法

不要在主線程下啊調用handler.post(). 在大部分狀況下,handler.post() 被用於快速修復一些與時序相關的問題。

推薦的作法是修改你的程序邏輯,而不是亂調handler.post()方法。

  

若是確實不得不調用post

請肯定你在作後臺操做時,不持有activity的引用~

 

好吧,若是你確實須要引用activity

那麼請在activityonPause裏面調用

handler.removeCallbacks()來確保消息隊列清空。

 

固然,若是你想被開除的話,你能夠這麼作:

使用handler.postAtFrontOfQueue() 去確保消息老是在 onPause() 以前抵用. 而後你的代碼就會變得很難懂。。。

順便提一下runOnUiThread()

你有沒注意到咱們使用 handler.post()而不是直接調用Activity.runOnUiThread()?

看下面你就明白了:

public class Activity {
  public final void runOnUiThread(Runnable action) {
    if (Thread.currentThread() != mUiThread) {
      mHandler.post(action);
    } else {
      action.run();
    }
  }
}

Unlike handler.post()runOnUiThread() does not post the runnable if the current thread is already the main thread. Instead, it calls run() synchronously.

handler.post()不同runOnUiThread()下,若是當前線程就是主線程的時候,它是直接同步調用runnable方法的。

 

關於服務

這有一個常見的誤解須要澄清一下: service 並運行在後臺線程中

全部的,service 生命週期相關方法如onCreate()onStartCommand(),都是在主線程中運行的

不論是在service仍是在activity中,長時間的任務都應該交給後臺線程。後臺線程的存活期能夠和應用自己同樣長。

可是,有時候 系統可能會殺掉app進程。

使用service 有助於咱們儘量的延長引用的存活期。

邊注: 當IBinder接收到另外一個進程的調用時,方法將會在後臺線程中調用。

 

Take the time to read the Service documentation – it’s pretty good.

  

結論

大部分Android 生命週期相關的方法都在主線程中調用。 

必須再囉嗦一句:不要阻塞主線程!!!

不過,你有沒想過阻塞主線程是啥效果?請看下章

Have you ever wondered what blocking the main thread actually means? That’s the subject of the next part!

相關文章
相關標籤/搜索