热门标签 | HotTags
当前位置:  开发笔记 > Android > 正文

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

这里我们讲的Fragment主要探讨的是support库中的Fragment,包括Fragment常遇到的crash崩溃问题,嵌套Fragment收不到onActivityResult()回调以及一些常用tips等,需要的朋友可以参考下

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

2016511113051864.png (317×847)

说明:总的来说,Fragment和Activity的生命周期类似。需要注意的是,它相比于Activity,多了onAttach(), onDetch(), onCreateView()和onDestroyView()这几个回调函数;但是,却少了onRestart()。
Fragment的生命周期非常复杂,分为以下几种情况:

  • 如果是通过XML中的标签实例化的,那么第一个收到的回调将是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);
}


推荐阅读
  • Android LED 数字字体的应用与实现
    本文介绍了一种适用于 Android 应用的 LED 数字字体(digital font),并详细描述了其在 UI 设计中的应用场景及其实现方法。这种字体常用于视频、广告倒计时等场景,能够增强视觉效果。 ... [详细]
  • 优化ListView性能
    本文深入探讨了如何通过多种技术手段优化ListView的性能,包括视图复用、ViewHolder模式、分批加载数据、图片优化及内存管理等。这些方法能够显著提升应用的响应速度和用户体验。 ... [详细]
  • 在计算机技术的学习道路上,51CTO学院以其专业性和专注度给我留下了深刻印象。从2012年接触计算机到2014年开始系统学习网络技术和安全领域,51CTO学院始终是我信赖的学习平台。 ... [详细]
  • Linux 系统启动故障排除指南:MBR 和 GRUB 问题
    本文详细介绍了 Linux 系统启动过程中常见的 MBR 扇区和 GRUB 引导程序故障及其解决方案,涵盖从备份、模拟故障到恢复的具体步骤。 ... [详细]
  • 本文介绍了如何使用jQuery根据元素的类型(如复选框)和标签名(如段落)来获取DOM对象。这有助于更高效地操作网页中的特定元素。 ... [详细]
  • 本文将详细介绍如何使用剪映应用中的镜像功能,帮助用户轻松实现视频的镜像效果。通过简单的步骤,您可以快速掌握这一实用技巧。 ... [详细]
  • 深入理解Cookie与Session会话管理
    本文详细介绍了如何通过HTTP响应和请求处理浏览器的Cookie信息,以及如何创建、设置和管理Cookie。同时探讨了会话跟踪技术中的Session机制,解释其原理及应用场景。 ... [详细]
  • 本文介绍如何在 Xcode 中使用快捷键和菜单命令对多行代码进行缩进,包括右缩进和左缩进的具体操作方法。 ... [详细]
  • 本文介绍了一款用于自动化部署 Linux 服务的 Bash 脚本。该脚本不仅涵盖了基本的文件复制和目录创建,还处理了系统服务的配置和启动,确保在多种 Linux 发行版上都能顺利运行。 ... [详细]
  • 在Linux系统中配置并启动ActiveMQ
    本文详细介绍了如何在Linux环境中安装和配置ActiveMQ,包括端口开放及防火墙设置。通过本文,您可以掌握完整的ActiveMQ部署流程,确保其在网络环境中正常运行。 ... [详细]
  • Android 渐变圆环加载控件实现
    本文介绍了如何在 Android 中创建一个自定义的渐变圆环加载控件,该控件已在多个知名应用中使用。我们将详细探讨其工作原理和实现方法。 ... [详细]
  • 如何在WPS Office for Mac中调整Word文档的文字排列方向
    本文将详细介绍如何使用最新版WPS Office for Mac调整Word文档中的文字排列方向。通过这些步骤,用户可以轻松更改文本的水平或垂直排列方式,以满足不同的排版需求。 ... [详细]
  • 在当前众多持久层框架中,MyBatis(前身为iBatis)凭借其轻量级、易用性和对SQL的直接支持,成为许多开发者的首选。本文将详细探讨MyBatis的核心概念、设计理念及其优势。 ... [详细]
  • 本文总结了在使用Ionic 5进行Android平台APK打包时遇到的问题,特别是针对QRScanner插件的改造。通过详细分析和提供具体的解决方法,帮助开发者顺利打包并优化应用性能。 ... [详细]
  • 理解存储器的层次结构有助于程序员优化程序性能,通过合理安排数据在不同层级的存储位置,提升CPU的数据访问速度。本文详细探讨了静态随机访问存储器(SRAM)和动态随机访问存储器(DRAM)的工作原理及其应用场景,并介绍了存储器模块中的数据存取过程及局部性原理。 ... [详细]
author-avatar
QQ文科
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有