注册

为什么会发生 Fragment not attached to Activity 异常?

事情是这样的,前两天有位大佬在群里提了个问题,原文如下


聊天截图.jpg


一个 Fragment 在点击按钮跳转一个新的 Activity 的时候,报崩溃异常:Fragment not attached to Activity


问:复现路径可能是什么样的呢?


一、回答问题前先审题


我们把这个问题的几个关键词圈出来


首先,可以点击 Fragment 上的按钮,证明这个 Fragment 是可以被看到的,那肯定是处于存活的状态的


其次,在跳转到新的 Activity 的时候发生崩溃,证明 Fragment 调用的是 startActivity() 方法


最后,来看异常信息:”Fragment not attached to Activity“


这个报错我们都已经很熟悉了,在 onAttach() 之前,或者 onDetach() 之后,调用任何和 Context 相关的方法,都会抛出 " not attached to Activity " 异常


发生的原因往往是因为异步任务导致的,比如一个网络请求回来以后,再调用了 startActivity() 进行页面跳转,或者调用 getResources() 获取资源文件等等


解决方案也非常简单:在 Fragment 调用了 Context 相关方法前,先通过 isAdded() 方法检查 Fragment 的存活状态就完事了


到这里,崩溃产生的原因找到了,解决方案也有了,似乎整篇文章就可以结束了


但是,楼主问的是:复现路径可能是什么样的呢?


这勾起了我的好奇心,我也想知道可能的路径是怎样的


于是,在接下来的两个晚上,笔者开始了一场源码之旅..


二、大胆假设,小心求证


审题结束我们就可以开始动手解答了,以下是群里的完整对话


大佬:一个 Fragment 在点击按钮跳转一个新的 Activity 的时候,报崩溃异常:Fragment not attached to Activity 。复现路径可能是什么样的呢?


我:这个问题之前在项目中也有碰到过,当时的解决方案是,通过调用 isAdded() 来检查 Fragment 是否还活着,来避免因为上下文为空导致的崩溃


当时忙于做业务没有深入研究,现在趁着晚上有时间来研究一下下


首先,打开 Fragment 源码,路径在:frameworks/base/core/java/android/app/Fragment.java


用 “not attached to Activity” 作为关键字搜索,可以发现 getResources()getLoaderManager()startActivity() 等等共计 6 处地方,都可能抛出这个异常


题目明确提到,是跳转 Activity 时发生的错误,那我们直接来看 startActivity() 方法


class Fragment {
void startActivity(){
if (mHost == null)
throw new IllegalStateException("Fragment " + this + " not attached to Activity");
}
}

从上面代码可以看出,当 mHost 对象为空时,程序抛出 Fragment not attached to Activity 异常


好,现在我们的问题转变为:



  1. mHost 对象什么时候会被赋值?

很显然,如果在赋值前调用了 startActivity() 方法,那程序必然会崩溃



  1. mHost 对象赋值以后,可能会被置空吗?如果会,什么时候发生?

我们都知道,Fragment 依赖 Activity 才能生存,那我们有理由怀疑:


当 Activity 执行 stop / destroy ,或者,配置发生变化(比如屏幕旋转)导致 Activity 重建,会不会将 mHost 对象也置空呢?


mHost 对象什么时候会被赋值?


先来看第一个问题,mHost 对象什么时候会被赋值?


平时我们使用 Fragment 开发时,通常都是直接 new 一个对象出来,然后再提交给 FragmentManager 去显示


创建 Fragment 对象的时候,不要求传入 mHost 参数,那 mHost 对象只能是 Android 系统帮我们赋值的了


得,又得去翻源码


打开 FragmentManager.java ,路径在:/frameworks/base/core/java/android/app/FragmentManager.java


class FragmentManager {

FragmentHostCallback mHost; // 内部持有 Context 对象,其本质是宿主 Activity

void moveToState(f,newState){
switch(f.mState){
case Fragment.INITIALIZING:
f.mHost = mHost; // 赋值 Fragment 的 mHost 对象
f.onAttach(mHost.getContext());
}
f.mState = newState;
}
}

我们发现源码里只有一个地方会给 mHost 对象赋值,在 FragmetnManager#moveToState() 方法中


如果当前 Fragment 的状态是 INITIALIZING ,那么就把 FragmentManager 自身的 mHost 对象,赋值给 Fragment 的 mHost 对象


这里多说一句,在 Android 系统中,一个 Activity 只会对应一个 FragmentManager 管理者。而 FragmentManager 中的 mHost ,其本质上就是 Activity 宿主。


所以,这里把 FragmentManager 的 mHost 对象,赋值给了 Fragment ,就相当于 Fragment 也持有了宿主 Activity


这也解释了我们之所以能在 Fragment 中调用 getResource()startActivity() 等需要 context 的才能访问方法,实际使用的就是 Activity 的上下文


废话说完了,我们来聊正事


FragmentManager#moveToState() 方法会先去判断 Fragment 的状态,那我们首先得知道 Fragment 有哪几种状态


class Fragment {

int INITIALIZING = 0; // Not yet created.
int CREATED = 1; // Created.
int ACTIVITY_CREATED = 2; // The activity has finished its creation.
... // 共6种标识

int mState = INITIALIZING; // 默认为 INITIALIZING
}

Google 为 Fragment 共声明了6个状态标识符,各个标识符的含义看注释即可


这里重点关注标识符下面的 mState 变量,它表示的是 Fragment 当前的状态,默认为 INITIALIZING


了解完 Fragment 的状态标识,我们回过头继续来看 FragmentManager#moveToState() 方法


class FragmentManager {
void moveToState(f,newState){
switch(f.mState){
case Fragment.INITIALIZING: // 必走逻辑
f.mHost = mHost; // 赋值 Fragment 的 mHost 对象
f.onAttach(mHost.getContext());
}
f.mState = newState;
}
}

moveToState() 方法中,只要当前 Fragment 状态为 INITIALIZING ,即执行 mHost 的赋值操作


巧了不是,前面刚说完,mState 默认值就是 INITIALIZING


也就是说,在第一次调用 moveToState() 方法时,不管接下来 Fragment 要转变成什么状态(根据 newState 的值来判断


首先,它都得从 INITIALIZING 状态变过去!那么,case = Fragment.INITIALIZING 这个分支必然会被执行!!这时候,mHost 也必然会被赋值!!!


再然后,才会有 onAttach() / onCreate() / onStart() 等等这些生命周期的回调!


因此,我们的第一个猜想:mHost 对象赋值前,有没有可能调用 startActivity() 方法?


答案显然是否定的


因为,根据楼主描述,点击按钮以后才发生的崩溃,视图能显示出来,说明 mHost 已经赋值过并且生命周期都正常走


那就只可能是点击按钮后,发生了什么事情,将 mHost 又置为 null


mHost 对象什么时候会被置空?


继续,来看第二个问题:mHost 对象赋值以后,可能会被置空吗?如果会,什么时候发生?


我们就不绕弯了,直接说答案,会!


置空 mHost 的逻辑,同样藏在 FragmentManager 的源码里:


class FragmentManager {

void moveToState(f,newState){
if (f.mState < newState) {
switch(f.mState){
case Fragment.INITIALIZING:
f.mHost = mHost; // mHost 对象赋值
}
} else if (f.mState > newState) {
switch (f.mState) {
case Fragment.CREATED:
if (newState < Fragment.CREATED) {
f.performDetach(); // 调用 Fragment 的 onDetach()
if (!f.mRetaining) {
makeInactive(f); // 重点1号,这里会清空 mHost
} else {
f.mHost = null; // 重点2号,这里也会清空 mHost 对象
}
}
}
}
f.mState = newState;
}

void makeInactive(f) {
f.initState(); // 此调用会清空 Fragment 全部状态,包括 mHost
}
}

看上面的代码,分发 Fragment 的 performDetach() 方法后,紧接着就会把 mHost 对象置空!


标记为 "重点1号" 和 "重点2号" 的代码都会执行了置空 mHost 对象的逻辑,两者的区别是:


Fragment 有一个保留实例的接口 setRetainInstance(bool) ,如果设置为 true ,那么在销毁重建 Activity 时,不会销毁该 Fragment 的实例对象


当然这不是本节的重点,我们只需要知道:执行完 performDetach() 方法后,无论如何,mHost 也都活不了了


那,什么动作会触发 performDetach() 方法?


1、Activity 销毁重建


不管因为什么原因,只要 Activity 被销毁,Fragment 也不能独善其身,所有的 Fragment 都会被一起销毁,对应的生命周期如下:



Activity#onDestroy() -> Fragment#onDestroyView() - > Fragment#onDestroy() - >Fragment#onDetach()



2、调用 FragmentTransaction#remove() 方法移除 Fragment


remove() 方法会移除当前的 Fragment 实例,如果这个 Fragment 正在屏幕上显示,那么 Android 会先移除视图,对应的生命周期如下:



Fragment#onPause() -> onStop() -> onDestroyView() - > onDestroy() - >onDetach()



3、调用 FragmentTransaction#replace() 方法显示新的 Fragment


replace() 方法会将所有的 Fragment 实例对象都移除掉,只会保留当前提交的 Fragment 对象,生命周期参考 remove() 方法


以上三种场景,是我自己做测试得出来的结果,应该还有其他没测出来的场景,欢迎大佬补充


另外,FragmentTransaction 中还有两个常用的 detach() / hide() 方法,它俩只会将视图移除或隐藏,而不会触发 performDetach() 方法


真相永远只有一个


好了,现在我们知道了 mHost 对象置空的时机,答案已经越来越近了


我们先来汇总下已有的线索


从 FragmentManager 源码来看,只要我们的 startActivity() 页面跳转逻辑写在:


onAttach() 方法执行之后 ,onDetach() 方法执行之前


那结果一定总是能够跳转成功,不会报错!


那么问题就来了


onAttach() 之前,视图不存在,onDetach() 之后,视图都已经销毁了,还点击哪门子按钮?


这句话翻译一下就是:


视图在,Activity 在,点击事件正常响应


视图不在,按钮也不在了呀,也就不存在页面跳转了


这样看起来,似乎永远不会出现楼主说的错误嘛


除非。。。


执行 startActivity() 方法的时候,视图已经不在了!!!


这听起来很熟悉,ummmmmm。。这不就是异步调用吗?


class Fragment {
void onClick(){
//do something
Handler().postDelayed(startActivity(),1000);
}
}

上面是一段异步调用的演示代码,为了省事我直接用 Handler 提交了延迟消息


当用户点击跳转按钮后,一旦发生 Activity 销毁重建,或者 Fragment 被移除的情况


等待 1s 执行 startActivity() 方法时,程序就会发生崩溃,这时候终于可以看到我们期待已久的异常:Fragment not attached to Activity


为什么会这样?熟悉 Java 的小伙伴这里肯定要说了,因为提交到 Handler 的 Runnable 会持有外部类呀,也就是宿主 Fragment 的引用。如果在执行 Runnable#run() 方法之前, Fragment 的 mHost 被清空,那程序肯定会发生崩溃的


那我们怎么样才能防止程序崩溃呢?




  • 要么,同步执行 Context 相关方法




  • 要么,异步判空,用到 Context 前调用 isAdded() 方法检查 Fragment 存活状态




三、结语


呼~ 这下总算是理清了,我们来尝试回答楼主的问题:发生 not attached to Activity,可能路径是怎样的?


首先,必然存在一个异步任务持有 Fragment 引用,并且内部调用了 startActivity() 方法。


在这个异步任务提交之后,执行之前,一旦发生了下面列表中,一个或多个的情况时,程序就会抛出 not attached to Activity 异常:



  • 调用 finishXXX() 结束了 Activity,导致 Activity 为空
  • 手动调用 Activity#recreate() 方法,导致 Activity 重建
  • 旋转屏幕、键盘可用性改变、更改语言等配置更改,导致 Activity 重建
  • 向 FragmentManager 提交 remove() / replace() 请求,导致 Fragment 实例被销毁
  • ...

最后,发生这个错误信息的本质,是在 Activity 、Fragment 销毁时,没有同步取消异步任务,这是内存泄漏啊


所以,除了使用 isAdded() 方法判空,避免程序崩溃外,更应该排查哪里可能会长时间引用该 Fragment


如果可能,在 Fragment 的 onDestroy() 方法中,取消异步任务,或者,把 Fragment 改为弱引用


作者:易保山
链接:https://juejin.cn/post/7162035153991106597
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

0 个评论

要回复文章请先登录注册