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

Android中Fragment的解析和使用详解

现在Fragment的应用真的是越来越广泛了,之前Android在3.0版本加入Fragment的时候,主要是为了解决AndroidPad屏幕比较大,空间不能充分利用的问题,但现在即使只是在手机上,也有很多的场景可以运用到Fragment了,这篇文章给大家介绍了Android中Fragment的解析和使用。

前言

Android Fragment的生命周期和Activity类似,实际可能会涉及到数据传递,onSaveInstanceState的状态保存,FragmentManager的管理和Transaction,切换的Animation。

我们首先简单的介绍一下Fragment的生命周期。

大致上,从名字就可以判断出每个生命周期是干嘛的。

AppCompatActivity就是FragmentActivity的子类,如果想使用Fragment,是要继承FragmentActivity,因为考虑到兼容的问题,我们要使用getSupportFragmentManager,而这个方法是FragmentActivity中声明的。

Activity中同样也有个类似的方法,getFragmentManager,两个方法返回的都是FragmentManager,不过一个是v4包。

至于Android到底是如何为低版本兼容Fragment这个问题,这里就不研究了,因为涉及到的源码估计应该很多,而且可能会很深。

Fragment到底是如何将自己的生命周期和Activity绑定在一起呢?

这里有一个很关键的类:FragmentController。

在FragmentActivity的生命周期中,会调用FragmentController对应的方法,而这些方法会调用到FragmentManager对应的方法。

我们来看看FragmentActivity的onCreate方法。

mFragments.attachHost(null /*parent*/);
super.onCreate(savedInstanceState);

这里调用了attachHost方法,而attachHost方法又调用了FragmentManager的attachController方法。

attachController这个方法实际上,是将需要的FragmentHostCallback,FragmentContainer和Fragment传进来。

FragmentHostCallback是FragmentContainer的子类,实际上,它就是Fragment所要附加的Activity,它持有这个Activity的实例,Context和Handler。

FragmentContainer和FragmentHostCallback是同一个实例,就是要附加的Activity。

而Fragment传入的是null,参数名是parent,这里附加的是Activity,因此没有Parent Fragment是很正常的。

当我们使用FragmentManager的时候,如果要添加Fragment,是需要这样写:

FragmentManager manager = ((FragmentActivity) context).getSupportFragmentManager();
FragmentTransaction transaction = manager.beginTransaction();
transaction.add(fragment, context.getClass().getSimpleName());
transaction.commit();

这里出现了新的类:FragmentTransaction。

FragmentTransaction是用于处理Fragment的栈操作,具体的子类是BackStackRecord,它同时也是一个Runnable。

当我们调用FragmentTransaction的add时候,实际上是调用BackStackRecord的addOp方法,Op是自定义的数据结构:

static final class Op {
  Op next;
  Op prev;
  int cmd;
  Fragment fragment;
  int enterAnim;
  int exitAnim;
  int popEnterAnim;
  int popExitAnim;
  ArrayList removed;
 }

也就是Fragment栈里面的节点的数据结构。

当我们commit的时候,就会调用FragmentManager的allocBackStackIndex,方法内部使用了对象这是为了保证Fragment的正常写入顺序,实际上,内部是用一个BackStackRecord的ArrayList来保存传入的BackStackRecord。

执行Fragment的写入后,关键一步就是调用FragmentManager的enqueueAction,将我们的操作添加到操作队列中。

执行这个方法的时候,会先检查是否已经保存了状态,也就是是否处于onStop的生命周期,如果是的话,就会报异常信息。所以我们不能在Activity的onStop里面进行任何有关Fragment的操作。

为了保证操作是串行的,同样也使用了对象锁。

最关键的是运行了FragmentManager的mExecCommit这个Runnable,这里主要是把每一个Active的Fragment作为参数传给moveToState这个方法,判断Fragment的状态。

这里的逻辑比较复杂,会将Fragment的State和mCurState进行比较。一开始commit的每个Fagment的状态都是INITIALIZING。

分为2种情况:

1.mCurState > State

说明Fragment开始创建。

onCreate最后会调用FragmentController和FragmentManager的dispatchCreate,将mCurState的状态改为CREATED,这时同样是调用moveToState方法,每个Fragment的状态都是INITIALIZING,就会开始读取保存的状态,并且分别调用Fragment的onAttach,onCreate,onCreateView和onViewCreate。

如果没有在commit之前就setArguments来传递数据,调用commit后是无法读取到的,因为setArguments传递过来的Bundle是在Fragment初始化的时候才会赋值给Fragment的mArguments,而Fragment的初始化动作是在FragmentManager的onCreateView中进行。我们使用Fragment的时候,都是在FragmentActivity的onCreate中commit,所以这时候Fragment实际上在commit的时候就会开始初始化了,如果放在commit后面setArguments,就根本没机会传递给Fragment。

这里我们要注意,上面都是在FragmentActivity的onCreate中进行,也就是说,这时候Activity根本还没创建好,所以关于Activity的资源在这里是无法获取到的。

2.mCurState

说明Fragment已经创建完毕。

所以,Fragment真正和Activity绑定是在commit调用的时候。

官方推荐我们通过setArguments来传递构造Fragment需要的参数,不推荐通过构造方法直接来传递参数,因为横竖屏切换的时候,是重新创建新的Activity,也就是重新创建新的Fragment,原先的数据就会全部丢失,但是setArguments传递的Bundle会保留下来。

我们只要看FragmentActivity的onCreate方法就知道,它会判断之前的配置和savedInstanceState是否不为null,而savedInstanceState会保存Fragment的数据,这些数据是以Parcelable的形式保存下来,这些数据就是FragmentManagerState,如果不为null,就会重新加载这些数据。

实际上,上面的生命周期的图是有问题的,onActivityCreated真正被调用是在FragmentActivity的onStart里面,这时mCurState就变成ACTIVITY_CREATED,而Fragment的状态变成CREATED,这时如果Fragment并不是布局文件中声明 ,采用的是动态添加的方式,那么Fragment就是在这里调用onCreateView和onViewCreated,并且将Fragment添加到FragmentActivity的布局上。

首先我们必须明确的是,onStart的时候,Activity虽然可见,但是还没有显示到前台,所以这时候才处理动态添加Fragment的情况是合理的,如果我们把动态添加Fragment的逻辑放在onCreate的时候,那时候Activity自身的布局都还没创建,怎么可能找到Container加载Fragment呢?

这同时也是提醒我们,不要在Fragment的onCreateView和onViewCreated处理耗时的逻辑,否则就会影响到FragmentActivity显示到前台的时间。

当FragmentActivity进入onResume的时候,已经显示到前台了,这时候发送一个消息给Handler,通知FragmentManager,mCurState变为RESUMED,这时Fragment就会开始进行监听事件等的设置。

当FragmentActivity进入onPause的时候,会先检查Fragment是否还没有设置监听事件,如果没有,就让它进行设置,然后修改mCurState为STARTED,这时就属于前面的第二种情况,Fragment进入onPause。

当FragmentActivity进入onStop的时候,首先通知FragmentManager修改mCurState为STOPPED,这时就会通知Fragment进入onStop,然后就是Handler接收到消息,通知FragmentManager将mCurState改为ACTIVITY_CREATED,通知Fragment调用performReallyStop,也就是真正的结束。

当FragmentActivity进入onDestroy的时候,会确认是否真的reallyStop,然后通知FragmentManager修改mCurState为CREATED,这时Fragment的状态为ACTIVITY_CREATED,开始保存视图数据,调用onDestroyView,父布局开始移除Fragment。

仔细看这段逻辑,就会发现,不管有没有设置Fragment是需要保留的,都会进入onDetach,表示该Fragment和FragmentActivity已经不再关联了。

我们再来看一下onRetainNonConfigurationInstance这个方法,它会设置Fragment的mRetaining为true,这样就会使Fragment不会进入onDestroy,就算是重新创建新的FragmentActivity,也只是清除Fragment的mHost,mParentFragment,mFragmentManager和mChildFragmentManager,之前的数据都会保存下来,并且这个Fragment并没有被销毁,这就会导致一个问题:重新创建的FragmentActivity本身也会创建新的Fragment,因此会出现Fragment的重叠,因为这时Fragment的状态为STOPPED,会分别进入onStart和onResume,也就是重新显示到前台的过程。

我们在实际的测试中就会发现,在没做任何处理的情况下,FragmentManager中的Fragment是越来越多,所以实际上,考虑到这种情况:应用在后台如果被杀掉的话,重新启动应用,之前的Fragment就可能会重叠在界面上。

这种情况在处理Tab的时候是比较麻烦的,因为Tab是好几个Fragment同时显示在前台,如果Activity被干掉,重新创建的时候,进入的是第一个Fragment,但如果这时候是在另一个Fragment下被干掉的,就可能导致这两个Fragment重叠。

所以可以在onCreate中判断是否重新创建Activity,只要判断savedInstanceState是否为null,如果为null,说明该Activity没有被重建过,可以添加Fragment,就算是上面的Tab的情况也可以处理,只要不添加第一个Fragment就可以。

如果是基于这样的判断来解决这个问题,我们还可以在添加Fragment的时候,指定一个Id或者Tag,判断FragmentManager中对应的Id或者Tag的Fragment是否存在来决定是否要添加。

当然,如果项目实在没有需要,我们是可以强制竖屏的。

如果只是针对横竖屏切换,也有另一种解决方案,在AndroidManifest中对应的activity标签中设置android:cOnfigChanges="orientation|keyboardHidden" ,但是这个属性在Android 4.0以上就失效了,必须这样写才行:android:cOnfigChanges="orientation|keyboardHidden|screenSize" 。这样在横竖屏切换的时候,不会走onRetainNonConfigurationInstance,走的是onConfigurationChanged,切换时不会销毁当前的FragmentActivity,自然Fragment也同样能够保持下来。

如果我们想要为Fragment增加过场动画,针对v4和非v4,有两种做法。

 1.针对v4,使用的是View Animation,动画资源放在res\anim\目录下。

 2.针对非v4,使用的是属性动画,动画资源放在res\animator\目录下。

   一般我们使用的都是v4的Fragment,并且针对的转场动画,View Animation已经足够满足我们的要求。

我们再来看一下FragmentTransaction的addToBackStack这个方法。

如果我们想要实现这样的效果:点击返回键,返回的是上一个Fragment。那就得调用addToBackStack这个方法。这个方法要求传入一个String的参数,实际上我们只要传入null就行,如果我们不想指定栈(虽说是栈,实际上只是个ArrayList,并没有实现栈的结构)的名字。

仔细看源码,我们就会发现,如果不调用这个方法,在按返回键的时候,就直接finish当前的FragmentActivity。

Fragment的回退和Activity的回退是有很大的区别的,我们知道,Fragment的操作是FragmentTransaction,而BackStackRecord真是这些操作的具体子类实现。

这时问题就来了:如果我们是两次FragmentTransactiont添加Fragment,第一次添加A,第二次添加B和C,我们回退并不是Fragment,是BackStackRecord的Op,而Op中记录的是每次操作的Fragment,当我们回退第二次操作的时候,是把第二次添加的B和C都退出来。

如果我们只有一个Fragment,并且也不想实现Fragment的回退栈,就千万不要调用addToBackState,不然在Activity按返回键的时候,并不会马上退出Activity,而是返回一个空白,因为就算是null,也会添加到BackStackRecord的ArrayList中,因为这个参数是作为mName来标记BackStackRecord, 在实际的处理中,它是否为null根本不重要。

当然,我们也可以自己调用FragmentManager的popBackStack方法进行回退栈的操作,如果我们想要马上执行的话,就要调用popBackStackImmediate方法,实际上,默认调用的就是这个方法。

如果我们在添加Fragment的时候,并没有设置任何Tag,但是在弹出栈的时候,要求弹出最新的Fragment,增加新的Fragment。

Fragment的栈并不像是Activity的栈那么复杂,提供多种启动模式,如果看源码的话,就会发现,实际上它就只有一种:弹出最近的BackStackRecord中的所有Fragment。

如果我们调用popBackStack的时候,没有指定flag为POP_BACK_STACK_INCLUSIVE,源码中的实现虽然是用if-else分成两种判断情况,但实际的处理是差不多的,不过没有指定的话,它会处理比较麻烦,如果可能的话,我们还是指定一下。

回到我们上面的问题,我们该如何做呢?

replace并不会影响到回退栈,如果我们真的要使用replace来替代某个Fragment,并且想要实现回退栈,就要addToBackStack,但如果这时我们想要替换某个Fragment,回退栈中的记录并不会跟着被替换,也就是说,这时我们选择回退,会退回到我们被替换的Fragment,所以我们必须在替换前就弹出这个Fragment。

FragmentManager提供了getBackStackEntryCount方法告诉我们回退栈的数量,还有getBackStackEntryAt方法来获取到对应的BackStackRecord,这时我们就能以下的处理来实现弹出:

if(manager.getBackStackEntryCount()>0){ 
 int n = manager.getBackStackEntryCount();
 manager.popBackStack(manager.getBackStackEntryAt(n-1).getName(), FragmentManager.POP_BACK_STACK_INCLUSIVE);
}

然后我们就能使用replace了。

我们必须注意,add,remove和replace影响到的是Fragment在界面上的显示,它们跟回退栈一点关系都没有,实际上,如果我们没有调用addToBackStack,甚至根本就不会有回退栈,而且回退栈是在该方法每次调用后,就会添加一个,不论是否重复,它都不会进行任何判断,所以如果一次FragmentTransaction提交多个Fragment,但是只是调用一次addToBackStack,虽然界面上有多个Fragment,但是回退栈中只有一个记录。

Fragment说归到底,在源码上来看,就只是和Activity生命周期同步的View,它不可能做到和Activity一样复杂的功能,它的任何逻辑业务代码,实际上也属于Activity,只不过移动到另一个类中而已,当然,如果愿意的话,就算把它当做一个轻量级的ViewController也是可以的,毕竟它只是负责自己负责的View的一切业务功能。

FragmentTransaction为Fragment提供了add,remove,hide,show和replace几种操作,我们要注意的是,add和replace的区别。

replace实际上就是remove + add的结合,并且使用replace的话,每次切换的话,会导致Fragment重新创建,因为它会把被替换的Fragment从视图中移除,这样当替换回来的时候,就要重新创建了。

这样频繁切换,就会严重影响到性能和流量。

所以,官方的说法是:replace()这个方法只是在上一个Fragment不再需要时采用的简便方法。

正确的切换方式是add() ,切换时hide() add()另一个Fragment;再次切换时,只需hide()当前,show()另一个。

当然,在hide之前,我们还需通过isAdd来判断是否添加过。

如果通过hide和show来实现切换,我们就不需要保存数据,因为Fragment并没有被销毁,如果是replace这种方式,我们就要保存数据,举个例子,如果界面中有EditText,我们如果想要保存之前在EditText的输入,就要保存这个值,不然使用replace的话,是会移除整个View的。

Fragment还涉及到和Activity以及其他Fragment的通信。

最好的方式就是只让Activity和Fragment进行通信,如果Fragment想要和其他Fragment进行通信,也得通过Activity。

我们可以利用回调Fragment的方法进行通信,当然,也可以在Fragment中声明接口,只要Activity实现这些接口,就能实现Activity和Fragment的通信。

想到setArguments是通过Bundle的形式来保存数据,那么我们是否可以利用这点,在传参上做一点文章呢?

在软件设计上,为了减少依赖,提议利用一个高层抽象来负责组件之间的通信,这样各个组件之间就不需要互相依赖了,也就是所谓的依赖倒置原则。

那么,我们这里是否也可以利用这个原则来做点事情呢?

依赖倒置在很多框架中的表现是采取注解的形式,我们可以考虑一下注解的方式来解决这个问题。

如果仅仅是为了构建Fragment而传输的参数,问题倒是比较简单,只要合理的利用反射,我们就可以获取到Fragment的字段,然后赋值。

类似的表现形式如下:

class FragmentA extends Fragment{
  @Arg
  private int age;
  public void onCreate(){
   FragmentInject.inject(this);
  }
}
class ActivityA extends Activity{
  
  public voi onCreate(){
   FragmentA a = new FragmentA();
   Bundle bundle = new Bundle();
   bundle.putString("text", "你好");
   a.setArguments(bundle);
   FragmentManager manager = getSupportFragmentManager();
   FragmentTransaction transaction = manager.beginTransaction();
   transaction.add(R.id.container, a);
   transaction.commit();
  }
}

实际上,这种方式无非就是代码组织方式上的改变,因为我们完全可以在Fragment的onCreate中获取到Bundle,同样也可以进行相同的操作,并且总的代码量会更少,但如果单纯只是从Fragment来看,我们只需要调用FragmentInject.inject方法和声明Arg注解,其他的东西根本不用考虑,相关的解析Bundle和字段赋值都放在FragmentInject这个抽象中,我们就不用每个Fragment都要写同样的代码,只要交给FragmentInject就行。

当然,上面只是简单的实现,真的是要实现一个成熟的东西是要考虑很多方面的,我们这里就把这个简单的项目放在Github上:https://github.com/wenjiang/FragmentArgs.git,如果有新的想法,欢迎补充。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作能带来一定的帮助,如果有疑问大家可以留言交流。


推荐阅读
  • Java服务问题快速定位与解决策略全面指南 ... [详细]
  • 开发笔记:深入解析Android自定义控件——Button的72种变形技巧
    开发笔记:深入解析Android自定义控件——Button的72种变形技巧 ... [详细]
  • 在使用Block时,正确的声明方法和确保线程安全是至关重要的。为了保证Block在堆中分配,应使用`copy`修饰符进行声明,因为栈中的Block与栈的生命周期绑定,容易导致内存问题。此外,还需注意Block捕获外部变量的行为,以避免潜在的循环引用和数据不一致问题。建议深入研究相关文档,以掌握更多高级技巧和最佳实践。 ... [详细]
  • 在TypeScript中,我定义了一个名为 `Employee` 的接口,其中包含 `id` 和 `name` 属性。为了使这些属性可选为空,可以通过使用 `| null` 或 `| undefined` 来扩展其类型定义。例如,`id: number | null` 表示 `id` 可以是数字或空值。这种类型的灵活性在处理不确定的数据时非常有用,可以提高代码的健壮性和可维护性。 ... [详细]
  • 设计模式详解:模板方法模式的应用与实现
    模板方法模式是一种行为设计模式,通过定义一个操作中的算法骨架,将具体步骤的实现延迟到子类中。本文详细解析了模板方法模式的类图结构、实现方式以及挂钩机制,并结合实际案例进行了深入探讨。此外,文章还提供了丰富的参考资料,帮助读者更好地理解和应用这一设计模式。对于手机用户,建议横屏阅读以获得更佳的阅读体验。 ... [详细]
  • 探索聚类分析中的K-Means与DBSCAN算法及其应用
    聚类分析是一种用于解决样本或特征分类问题的统计分析方法,也是数据挖掘领域的重要算法之一。本文主要探讨了K-Means和DBSCAN两种聚类算法的原理及其应用场景。K-Means算法通过迭代优化簇中心来实现数据点的划分,适用于球形分布的数据集;而DBSCAN算法则基于密度进行聚类,能够有效识别任意形状的簇,并且对噪声数据具有较好的鲁棒性。通过对这两种算法的对比分析,本文旨在为实际应用中选择合适的聚类方法提供参考。 ... [详细]
  • 在 openSUSE Tumbleweed 系统上搭建 51 单片机开发环境并进行编程实践。首先,通过 `sudo zypper in emacs` 命令安装文本编辑器 Emacs。接着,使用 `sudo zypper in sdcc` 安装 SDCC 编译器。最后,利用 `wget` 下载 sdcflash Python 脚本,以便于单片机的烧录和调试。此外,还介绍了如何配置开发环境,确保各组件协同工作,提高开发效率。 ... [详细]
  • 掌握PHP编程必备知识与技巧——全面教程在当今的PHP开发中,了解并运用最新的技术和最佳实践至关重要。本教程将详细介绍PHP编程的核心知识与实用技巧。首先,确保你正在使用PHP 5.3或更高版本,最好是最新版本,以充分利用其性能优化和新特性。此外,我们还将探讨代码结构、安全性和性能优化等方面的内容,帮助你成为一名更高效的PHP开发者。 ... [详细]
  • 中国学者实现 CNN 全程可视化,详尽展示每次卷积、ReLU 和池化过程 ... [详细]
  • npm 安装出错,求助高手分析原因并提供解决方案 ... [详细]
  • 在尝试对从复杂 XSD 生成的类进行序列化时,遇到了 `NullReferenceException` 错误。尽管已经花费了数小时进行调试和搜索相关资料,但仍然无法找到问题的根源。希望社区能够提供一些指导和建议,帮助解决这一难题。 ... [详细]
  • 在Ubuntu 20.04 Linux系统中部署Git的详细步骤与最佳实践
    在Ubuntu 20.04 Linux系统中部署Git时,首先确保您的操作系统版本正确,并已以具备sudo权限的用户身份登录。推荐使用APT软件包管理器进行安装,这是最简便且可靠的方法。此外,遵循最佳实践,如定期更新Git版本和配置全局设置,可以进一步提升使用体验和安全性。 ... [详细]
  • 分布式开源任务调度框架 TBSchedule 深度解析与应用实践
    本文深入解析了分布式开源任务调度框架 TBSchedule 的核心原理与应用场景,并通过实际案例详细介绍了其部署与使用方法。首先,从源码下载开始,详细阐述了 TBSchedule 的安装步骤和配置要点。接着,探讨了该框架在大规模分布式环境中的性能优化策略,以及如何通过灵活的任务调度机制提升系统效率。最后,结合具体实例,展示了 TBSchedule 在实际项目中的应用效果,为开发者提供了宝贵的实践经验。 ... [详细]
  • 考前准备方面,我的考试时间安排在上午11点至12点,只需提前20分钟到达考场的接待休息区即可。由于我居住在福田区,交通便利,可以选择多种方式前往考场。为了确保顺利通过考试,我建议考生提前熟悉考试流程和环境,并合理规划出行时间,以保持良好的心态和状态。此外,考前复习应注重理论与实践相结合,多做模拟题,加强对重点知识点的理解和掌握。 ... [详细]
  • Node.js 教程第五讲:深入解析 EventEmitter(事件监听与发射机制)
    本文将深入探讨 Node.js 中的 EventEmitter 模块,详细介绍其在事件监听与发射机制中的应用。内容涵盖事件驱动的基本概念、如何在 Node.js 中注册和触发自定义事件,以及 EventEmitter 的核心 API 和使用方法。通过本教程,读者将能够全面理解并熟练运用 EventEmitter 进行高效的事件处理。 ... [详细]
author-avatar
_大盗坂崎由莉nyS
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有