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);
}
相关文章
相关标签/搜索