2016年7月25日 星期一

Fragment畫面轉向、View.post與getActivity() 聯手演出的問題

問題

某Fragment透過View.post() 待View建立完成後執行相關動作,例如常見的取得View大小,但若post出去的Runnable物件內恰巧包含了getActivity()的相關用法,可能會遇上傳回值為null的情況。

Fragment於Activity.onCreate() 新建並替換現有畫面,在畫面轉向時隨Activity銷毀與重建,但一再新建的Fragment會取代舊有(自動重建)的Fragment,使其與Activity的連結中斷,但已post至主執行緒MessageQueue中的待執行任務仍會被執行,若其中的操作基於getActivity(),將發生Null Point Exception。

這個問題個人不常遇到,但感到挺有趣的,所以花點時間研究一下,若有謬誤麻煩告訴我,有什麼看法也歡迎討論!

還原現場

在MainActivity的onCreate()新建Fragment物件,為求簡便直接以replace方法將畫面置換。

MainActivity.java
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    …
    Fragment fraMain = new FragmentMain();
    getFragmentManager().beginTransaction().replace(R.id.FrameLayoutContent, fraMain).commit();
    …
}

在Fragment的onActivityCreated事件中載入TextView,並以Runnable包裹待View建立完成後要執行的動作,動作內包含getActivity()的呼叫。

FragmentMain.java
public void onActivityCreated(Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);
    TextView txv = (TextView)currentFragment.findViewById(R.id.txv);
    txv.post( new Runnable() {
        @Override
        public void run() {
            getActivity()……
        }
    });
}

關鍵之一「畫面轉向」

Fragment 需要附加於 Activity,其生命週期也受 Activity 影響,這點在官方指南中的附圖可以明顯看出。實測畫面轉向時,Activity 確實執行 onDestroy() 與 onCreate(),而 Fragment 亦同。

Activity 與 Fragment 生命週期
(來源:https://developer.android.com/guide/components/fragments.html)


例在本問題中,轉向後會有兩個Fragment被建立:
  1. 隨Activity重建亦自動重建的Fragment → 原有重建的
  2. 當Activity再次執行onCreate所新建Fragment → 新建的
自動重建的 Fragment 一樣會依序呼叫 onAttach()、onCreate()、onCreateView() 與onActivityCreated(),且在這些方法內呼叫 getActivity() 仍會取得物件,因此不能在一開始的onAttach() 中判斷 getActivity() 是否為 null,作為是否繼續執行的依據。當然,在真正用到getActivity() 的當下,都判斷一次是否為 null 就可避免問題,但這很累人,讓我們繼續挖掘其原理。

關鍵之二「View.post()」

View.post 傳入的 Runnable 物件將被排入主執行緒 MessegeQueue 等待執行。然而隨著Activity 的 onCreate() 執行,一再新建並且 replace 的 Fragment 物件會使舊有 Fragment 銷毀,但已 post 去出的 runnable 仍在主執行緒中等待執行,當 runnable 被執行時,裡面的getActivity() 自然會取得 null,因為該 Fragment 已onDetach(參考前面的生命週期圖)。

解決方案

經過上述分析,大致有3方案可以使用:

  1. 直接在View.post() 判斷getActivity()是否為null,這是最簡單直覺的方法。
  2. 設法重用Fragment,在replace前檢查是已有存在的相同Fragment,這可利用Activity的onSaveInstanceState()來實踐。
  3. 利用Fragment的onAttach()方法,該方法自動傳入Activity參考,可在此先準備好要使用的資料與資源,但遷就其生命週期,程式的撰寫方式與執行流程可能需要隨之更動,若需暫存預先取出的資源,亦需考量耦合問題。

當然,直接鎖定Activity使其不能轉向亦可,但總有需要支援旋轉的情況。

參考資料

  1. Android官方文件Fragment
    https://developer.android.com/guide/components/fragments.html
  2. Android Fragment中getActivity()返回null的问题
    http://www.dss886.com/android/2015/08/11/01
  3. After the rotate, onCreate() Fragment is called before onCreate() FragmentActivity
    http://stackoverflow.com/questions/14093438/after-the-rotate-oncreate-fragment-is-called-before-oncreate-fragmentactivi
  4. 通过View.post()获取View的宽高引发的两个问题:1post的Runnable何时被执行,2为何View需要layout两次;以及发现Android的一个小bug
    http://blog.csdn.net/scnuxisan225/article/details/49815269
  5. 如何使用Handler
    http://givemepass-blog.logdown.com/posts/296606-how-to-use-a-handler
  6. Android中Thread、Handler、Looper、MessageQueue的原理分析
    http://blog.csdn.net/bboyfeiyu/article/details/38555547