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

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

一个 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 改为弱引证

四、参考资料

  • android-7.1 – Fragment
  • android-7.1 – FragmentManager