欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Android应用中使用Fragment组件的一些问题及解决方案总结

程序员文章站 2024-03-02 17:27:28
fragment的主要意义就是提供与activity绑定的生命周期回调。 fragment不一定要向activity的视图层级中添加view. 当某个模块需要获得acti...

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的生命周期图如下:

Android应用中使用Fragment组件的一些问题及解决方案总结

说明:总的来说,fragment和activity的生命周期类似。需要注意的是,它相比于activity,多了onattach(), ondetch(), oncreateview()和ondestroyview()这几个回调函数;但是,却少了onrestart()。
fragment的生命周期非常复杂,分为以下几种情况:

  • 如果是通过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方法

public void onbackpressed() {
  if (!mfragments.popbackstackimmediate()) {//注意
    supportfinishaftertransition();
  }
}

如果onbackpressed在onsavedinstancestate之后调用,那么就会crash.
onbackpressed的调用时机:

* targetsdkversion <= 5,在onkeydown中调用
* targetsdkversion > 5,在onkeyup中调用
onsavedinstancestate的调用时机(如果调用的话):

* 一定在onstop之前
* 可能在onpause之前,也可能在onpause与onstop之间
需要注意的是: onsavedinstancestate方法不一定会调用,只有在activity因为某些原因而被framework销毁,并且之后还需要重新创建的情况,才需要调用(例如:旋屏,或者内存不足而回收返回栈中的某些activity)

举例:
* activity a在前台时,屏幕逐渐变暗直至锁屏,那么a的onsavedinstancestate会被调用
* activity a start activity b,activity a的onsavedinstancestate会被调用
* activity a因为返回键或者finish调用而返回到上一个界面,那么a的onsavedinstancestate不会被调用
因此,当onbackpressed在onsavedinstancestate方法之后调用,就一定会crash.解决方法主要有两种:

重写activity的onsavedinstancestate()方法,并且注释掉super调用.
这种方法能避免crash,但是它会导致整个activity的状态丢失.以dialogfragment为例,正常情况下,显示的dialogfragment在旋屏activity重新创建之后,不需要我们处理,dialog会自动显示出来(参见dialogfragment.onstart()),但是注释掉activity的onsavedinstancestate()方法之后,fragment状态丢失,activity重新创建之后,dialog也就不会再显示出来了.

更好且通用的做法:在调用commit,popbackstack以及onbackpressed方法之前,判断onsavedinstancestate()方法是否已经执行,并且onresume方法还没有执行,如果不是,那么直接操作,否则加入到pending队列,等待onresumefragments或者onpostresume之后再执行.

注意:不要在onresume中操作,因为这时候fragmentmanager中的mstatesaved依然可能是true.(如果执行顺序是onsavedinstancestate()->onpause()->onresume() 或者 onpause()->onsavedinstancestate()->onresume())

例如:

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的可用区间:

1.activity: [integer.min_value, integer.max_value]
(1)当requestcode取值在[integer.min_value, -1]区间中,效果和startactivity()一样,不会收到onactivityresult()回调
(2)内置的fragment可用requestcode的区间和activity相同
2.support库: fragment,以及fragmentactivity:[-1, 65535]
(1)requestcode == -1,效果和startactivity()一样,不会收到onactivityresult()回调
(2)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()

1.illegalstateexception(fragment not attached to activity)的问题
这个异常通常的发生情况是:在fragment中启动一个异步任务,然后在回调中执行和resource相关的操作(getstring(...)),或者startactivity(...)之类的操作.但是这个时候fragment可能已经被detach了,所以它的mhost==null,因此在执行这些操作之前,需要先判断一下isadded().
注意: 这里不要使用isdetached()来判断,因为fragment被detach之后,它的isdetached()方法依然可能返回false

2.如果fragment a是因为被replace而detach的,那么它的isdetached()将返回false
3.如果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);
}