事情是这样的,前两天有位大佬在群里提了个问题,原文如下
一个 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 反常
好,现在咱们的问题转变为:
mHost
目标什么时分会被赋值?
很显然,假如在赋值前调用了 startActivity()
办法,那程序必定会溃散
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 改为弱引证
四、参考资料
- android-7.1 – Fragment
- android-7.1 – FragmentManager