Android Fragment 常見問題及解決方案

Fragment(主要探討的是support庫中的Fragment)

  • Fragment的主要意義就是提供與Activity綁定的生命週期回調
  • Fragment不必定要向Activity的視圖層級中添加View. 當某個模塊須要得到Activity的生命週期回調的時候,就能夠考慮經過Fragment來實現.
    • 例如: DialogFragment, 調用show方法來顯示一個Dialog(這個一個子Window,並不在Activity的視圖層級中),當旋屏時,DialogFragment利用onDestroyView回調來dismiss Dialog,而後Activity重建以後,DialogFragment利用onStart回調再顯示Dialog
    • 固然,咱們也能夠建立一個徹底沒有UI的Fragment,好比BackgroundWorkerFragment,在onResume的時候執行一個Task,在onPause的時候暫停一個Task

Fragment 生命週期

Fragment的生命週期很是複雜,分爲如下幾種狀況:java

  • 若是是經過XML中的<fragment/>標籤實例化的,那麼第一個收到的回調將是onInflate
  • 若是setRetainInstance(true),那麼當Activity重建時,Fragment的onDestroy以及Activity重建後Fragment的onCreate回調不會被調用.(不管是否將其添加到了返回棧)
  • 若是當前顯示的是Fragment A,而後執行FragmentTransaction.replace(),那麼Fragment A會執行onPause()->onStop()->onDestroyView()->onDestroy()->onDetach(),若是執行FragmentTransaction.replace().addToBackStack(),那麼Fragment A會執行onPause()->onStop()->onDestroyView()
  • FragmentTransaction.hide(),將不會致使onPause(),而是會觸發onHiddenChanged()
  • FragmentTransaction.detach(),會致使onPause()->onStop()->onDestroyView(),注意:onDestroy()和onDetach()不會調用

FragmentTransaction

  • 對於Fragment的操做都是經過FragmentTransaction來進行的,一個FragmentTransaction能夠包含一個或者多個操做,經過commit或者commitAllowingStateLoss來提交.若是該FragmentTransaction被加入返回棧,那麼出棧的時候,該Transaction中的全部操做都會被撤銷
  • commit方法是異步的(handler post相應的message到MainLooper關聯的Message queue),若是須要馬上執行Transaction的操做,能夠調用executePendingTransactions()
  • FragmentTransaction的commit方法以及FragmentManager的popBackStack方法都是異步的,給調用者帶來了不少不便,雖然能夠經過調用executePendingTransactions()方法來當即執行,可是爲何默認是異步的呢??(我以爲是由於:提交一個Transaction,會致使Fragment的生命週期方法的執行,甚至是多個回調的執行,若是Fragment在這些回調中又提交新的Transaction,那麼可能會破壞當前Transaction的狀態,比方說這是一個pop操做)

Can not perform this action after onSaveInstanceState

在使用Fragment的過程當中,經常會遇到在Activity的onSaveInstanceState方法調用以後,操做commit或者popBackStack而致使的crash.
由於在onSaveInstanceState方法以後的操做狀態可能會丟失,所以Android framework默認會拋出一個異常.
對於commit方法來講,單純避免這個異常很簡單,使用commitAllowingStateLoss方法便可.可是popBackStack以及popBackStackImmediate也都會檢查state(checkStateLoss),特別須要注意的是Activity的onBackPressed方法android

public void onBackPressed() {
    if (!mFragments.popBackStackImmediate()) {//注意
        supportFinishAfterTransition();
    }
}

若是onBackPressed在onSavedInstanceState以後調用,那麼就會crash.
onBackPressed的調用時機:異步

* targetSdkVersion <= 5,在onKeyDown中調用
* targetSdkVersion > 5,在onKeyUp中調用

onSavedInstanceState的調用時機(若是調用的話):ide

* 必定在onStop以前
* 可能在onPause以前,也可能在onPause與onStop之間

須要注意的是: onSavedInstanceState方法不必定會調用,只有在Activity由於某些緣由而被Framework銷燬,而且以後還須要從新建立的狀況,才須要調用(例如:旋屏,或者內存不足而回收返回棧中的某些Activity)oop

舉例:
* Activity A在前臺時,屏幕逐漸變暗直至鎖屏,那麼A的onSavedInstanceState會被調用
* Activity A start Activity B,Activity A的onSavedInstanceState會被調用
* Activity A由於返回鍵或者finish調用而返回到上一個界面,那麼A的onSavedInstanceState不會被調用

所以,當onBackPressed在onSavedInstanceState方法以後調用,就必定會crash.解決方法主要有兩種:post

  1. 重寫Activity的onSavedInstanceState()方法,而且註釋掉super調用.
    這種方法能避免crash,可是它會致使整個Activity的狀態丟失.以DialogFragment爲例,正常狀況下,顯示的DialogFragment在旋屏Activity從新建立以後,不須要咱們處理,Dialog會自動顯示出來(參見DialogFragment.onStart()),可是註釋掉Activity的onSavedInstanceState()方法以後,Fragment狀態丟失,Activity從新建立以後,Dialog也就不會再顯示出來了.ui

  2. 更好且通用的作法:在調用commit,popBackStack以及onBackPressed方法以前,判斷onSavedInstanceState()方法是否已經執行,而且onResume方法尚未執行,若是不是,那麼直接操做,不然加入到pending隊列,等待onResumeFragments或者onPostResume以後再執行.this

注意:不要在onResume中操做,由於這時候FragmentManager中的mStateSaved依然多是true.(若是執行順序是onSavedInstanceState()->onPause()->onResume() 或者 onPause()->onSavedInstanceState()->onResume())spa

例如:
public void onDataReceived() {
	if(isStateSaved()) {//isStateSaved()由BaseActivity提供
		addPendingFragmentOperation(new Runnable() {
            @Override
            public void run() {
                getSupportFragmentManager().popBackStackImmediate();
            }
        });
	} else {
		getSupportFragmentManager().popBackStackImmediate();
	}
}

@Override
protected void onPostResume() {
    super.onPostResume();
    if(pendingFragmentOperation != null && !pendingFragmentOperation.isEmpty()) {
        for(Runnable operation : pendingFragmentOperation) {
            operation.run();
        }
        pendingFragmentOperation.clear();
    }
}

startActivityForResult

requestCode的可用區間:調試

  • Activity: [Integer.MIN_VALUE, Integer.MAX_VALUE]
    • 當requestCode取值在[Integer.MIN_VALUE, -1]區間中,效果和startActivity()同樣,不會收到onActivityResult()回調
    • 內置的Fragment可用requestCode的區間和Activity相同
  • support庫: Fragment,以及FragmentActivity:[-1, 65535]
    • requestCode == -1,效果和startActivity()同樣,不會收到onActivityResult()回調
    • requestCode 在 [Integer.MIN_VALUE, -2]或者[65536, Integer.MAX_VALUE]之間,會拋出異常(requestCode只能使用低16比特)

建議: requestCode的取值統一限制在[-1, 65535]之間

嵌套Fragment

首先要說的是儘可能不要使用嵌套Fragment.
當在嵌套Fragment中使用startActivityForResult()時,會遇到的問題:

  • 全部的Fragment都收不到onActivityResult()
  • 某個level 1 的Fragment收到了onActivityResult()

總之那個發起startActivityForResult()的嵌套Fragment是必定不會收到onActivityResult()回調的.

緣由以下:(可參考上面說的requestCode)
FragmentActivity.startActivityFromFragment()會改動requestCode,用高16比特存儲Fragment在FragmentManager中的index,而低16比特做爲Fragment可用的requestCode.在FragmentActivity.onActivityResult()中,根據高16比特,從FragmentManager中找到對應的Fragment,而後將低16比特的值做爲requestCode,調用Fragment.onActivityResult().

那麼requestCode中只能存儲一個index,即root FragmentManager中的Fragment index.所以就會出現上面所列出的情形:

  • 當嵌套Fragment在childFragmentManager中的index,大於rootFragmentManager中的全部index時, rootFragmentManager將找不到與此index對應的Fragment,因此沒有Fragment能收到onActivityResult()
  • 當嵌套Fragment在childFragmentManager中的index,小於等於rootFragmentManager中的全部index時,那麼隸屬於rootFragmentManager的一個Fragment將會收到onActivityResult()
  • 總之即便能有Fragment能收到onActivityResult(),那也是頂層的某個Fragment,而不是發起請求的嵌套Fragment

解決方案:

  • 不使用嵌套Fragment :)

  • 依然利用requestCode,將其低16位拆分,其中的高8位用來存儲childFragmentManager中的index,低8位留給ChildFragment使用.(若是嵌套層級不深,那麼此方案仍是不錯的,若是層級較深,那麼留給Fragment的requestCode的可用值區間將很是侷限)

  • Android 4.2(Api 17)之後,可使用內置的Fragment,以及ChildFragmentManager,內置Fragment再也不須要藉助requestCode的高16比特來記錄它的index.而是由Framework收到Fragment.startActivityForResult()時,記錄該Fragment的標識(android:fragment:${parentIndex}:${myIndex}),派發result時,就根據這個標識找到那個Fragment.所以就不會出現ChildFragment收不到onActivityResult()回調的問題了.能夠參考Activity.dispatchActivityResult()

Tips

  • 開發的時候,能夠打開Fragment相關的調試信息
    FragmentManager.enableDebugLogging(BuildConfig.DEBUG);
  • Activity的onResume被調用時,Fragment的onResume還未被調用.
protected void onPostResume() {
    super.onPostResume();
    mHandler.removeMessages(MSG_RESUME_PENDING);
    onResumeFragments();
    mFragments.execPendingActions();
}
protected void onResumeFragments() {
    mFragments.dispatchResume();
}

若是須要在Fragment的onResume都執行完後再執行某個操做,能夠重寫onPostResume()方法,必定要調用 super.onPostResume()

  • IllegalStateException(Fragment not attached to Activity)的問題
    這個異常一般的發生狀況是:在Fragment中啓動一個異步任務,而後在回調中執行和resource相關的操做(getString(...)),或者startActivity(...)之類的操做.可是這個時候Fragment可能已經被detach了,因此它的mHost==null,所以在執行這些操做以前,須要先判斷一下isAdded().

注意: 這裏不要使用isDetached()來判斷,由於Fragment被detach以後,它的isDetached()方法依然可能返回false

  • 若是Fragment A是由於被replace而detach的,那麼它的isDetached()將返回false
  • 若是Fragment A對應的FragmentTransaction被加入到返回棧中,由於出棧而detach,那麼它的isDetached()將返回true
final public Resources getResources() {
    if (mHost == null) {
        throw new IllegalStateException("Fragment " + this + " not attached to Activity");
    }
    return mHost.getContext().getResources();
}
public void startActivity(Intent intent, @Nullable Bundle options) {
    if (mHost == null) {
        throw new IllegalStateException("Fragment " + this + " not attached to Activity");
    }
    mHost.onStartActivityFromFragment(this /*fragment*/, intent, -1, options);
}
相關文章
相關標籤/搜索