这篇文章得从一个 BUG 说起,UI 是由 Viewpager2 嵌套两个 Fragment 构成的。然而,就是这再平常不过的功能以及接近模版化的代码,竟然因为偷懒产生了异常。为了省去繁琐的 Arguments 传值,在 Fragment 的实例化过程中增加了有参构造函数。正是这一操作为后续的应用崩溃埋下了坑,Fragment 源码正在等待合适的时机将异常抛出。
1. 崩溃信息
采用有参构造函数实例化 Fragment 后,进行了功能测试。由于在测试过程中应用一直在前台,所以也就没有发现这一BUG,但当应用上线后,陆续收到了应用崩溃的反馈,查看 BUG 统计得到了如下的崩溃信息:
1 | ************* Crash Head **************** |
2. 查找原因
异常信息描述了 ItemFragment 实例化失败及原因,并给出了异常出现的具体位置。根据异常信息查看 Fragment 的 instantiate 方法就会发现为什么会抛出构造方法找不到的异常,原来这里的 Fragment 实例化是通过反射实现的,这个过程中会寻找默认的无参构造函数来进行初始化,如果找不到无参构造函数就会抛出 NoSuchMethodException 异常,而我们代码里的默认构造函数是有参的,所以会抛出异常。
1 | /** |
3. 解决办法
知道了错误原因,就可以根据报错信息进行修改了,删除有参构造函数,改为由 Arguments 方式进行传值,代码如下:
1 | internal class ItemFragment : Fragment() { |
4. 后续
问题虽然解决了,但在后续整理资料的过程中发现原来官方文档早就对这一问题进行过说明,只是我们没有注意。详情请移步 Fragment 说明。我将官方文档中的说明信息摘了出来,原文和译文如下:
原文 | Every fragment must have an empty constructor, so it can be instantiated when restoring its activity's state. It is strongly recommended that subclasses do not have other constructors with parameters, since these constructors will not be called when the fragment is re-instantiated; instead, arguments can be supplied by the caller with setArguments(Bundle) and later retrieved by the Fragment with getArguments(). |
译文 | 每个 fragment 都必须有一个空的构造函数,以便在恢复其活动状态时可以实例化。强烈建议子类不要有其他带参数的构造函数,因为这些构造函数在 fragment 重新实例化时不会被调用;相反,参数可以由调用者使用 setArguments(Bundle) 提供,然后由 Fragment 使用 getArguments() 检索 |
通过源码分析和文档说明可以得出这样的结论:当 Fragment 因为某种原因,例如旋转屏幕时 Activity 重建,此时 Fragment 也会重建,然后通过 onCreate( Bundle savedInstanceState) 传入之前保存的数据 savedInstanceState,然后通过反射无参构造实例化一个新的 Fragment,并且给 mArgments 初始化为原先的值,如果 Fragment 的构造函数不是无参的,就无法实例化 Fragment,进而导致程序崩溃。因此,在 Fragment 中,传递数据的正确方式应该是通过 setArguments(Bundle) 然后在需要的时候通过 getArguments() 来获取传入的数据。