Fragment的正确使用避免内存泄漏

1. 目的

Fragment的用法很多,体现在:

  • 你可以直接跟activity静态绑定;
  • 也可以动态;
  • 使用FragmentManager、FragmentTransaction管理时,根据Fragment的显示场景,可以replace(layoutId, newFragment, false); 也可以add,popStack,show, hide;
  • 有时为了避免多次数据加载(即onCreateView避免触发),缓存View的策略也不同。

但只有一个目的:随便折腾,别搞出内存泄漏(即替换Fragment要能及时回收)。这里记录下给出建议方案,只要保证了内存不泄漏,你可以随便折腾Fragment的用法。

2. LeakCanary

这里我们借助LeakCanary。
Leakcanary官网上提到的常见的三种内存泄漏:

Common causes for memory leaks¶
Most memory leaks are caused by bugs related to the lifecycle of objects. Here are a few common Android mistakes:
1.Adding a Fragment instance to the backstack without clearing that Fragment’s view fields in Fragment.onDestroyView() (more details in this StackOverflow answer).
2.Storing an Activity instance as a Context field in an object that survives activity recreation due to configuration changes.
3.Registering a listener, broadcast receiver or RxJava subscription which references an object with lifecycle, and forgetting to unregister when the lifecycle reaches its end.

LeakCanary最基本的使用:
集成
debugImplementation ‘com.squareup.leakcanary:leakcanary-android:2.7’
即可。
打印:D LeakCanary: LeakCanary is running and ready to detect leaks 表示成功启动。
日志过滤:Leakanary,在Fragment 或者Activity关闭destory的时候,是否有objects引用。
并在5s后,是否未释放,有泄漏的objects。

比如

08-12 11:16:18.942  7343  7343 D LeakCanary: Watching instance of androidx.constraintlayout.widget.ConstraintLayout (com.allan.fragmentstester.CombineFragment received Fragment#onDestroyView() callback (references to its views should be cleared to prevent leaks)) with key 7304ce05-e4e2-453c-9178-79cd1bdfc3e7
08-12 11:16:24.104  7343  7384 D LeakCanary: Found 1 object retained, not dumping heap yet (app is visible & < 5 threshold)

它监控了Fragment的onDestoryView,并推迟5s后,检查object的引用,给出提示。在多次反复测试以后,Found x object ,就证明有泄漏。最新版还会有详细报告。

3. 分析

但是LeakCanary对于Fragment的检测机制存在误区。看我的分析。

3.1 源码记录

//默认进入的第一个Fragment
FragmentManager fragmentManager = getSupportFragmentManager();
Fragment fragment = new AFragment();//fragmentManager.findFragmentByTag(TAG_A);
FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.replace(R.id.root_layout, fragment)
  .addToBackStack(null)
  .commit();

//跳转第二个Fragment
FragmentManager fragmentManager = getSupportFragmentManager();
Fragment toFragment = new BFragment();
FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction
  .replace(R.id.root_layout, toFragment)
  .addToBackStack(null)
  .commit();

A BFragment里面的代码,用全局变量保存View、Button等。很常规的操作对吧。replace + addToBackstack。

3.2 断点调试

接着断点调试,关注FragmentManager下面的

mFragmentStore(FragmentStore fragment1.2.6库,FragmentManagerImp 1.1.x库)下面的:
mAdded
**mNonConfig **这个ViewModel的mChildConfigs的size;
mActive的Size。
Fragment的正确使用避免内存泄漏_第1张图片
Fragment的正确使用避免内存泄漏_第2张图片

通过断点和置空前后对比,不论是否在onDestoryView给所有的成员View变量button = null,这些size都是在不断的膨胀!

经过分析和理解LeakCanary的设计,我认为是LeakCanary的“误报”,因为使用addBackStack的,Fragment onDestoryView以后,Fragment就是被设计成,存留在FragmentManager的缓存中。android的Fragment设计就是有缓存,会被设置为CREATE状态,后续用的时候,直接拿出来变变状态即可使用。

3.3 LeakCanary误区

我们平时开发,在Fragment的onDestoryView() 中置空View的引用(也即Leakcanary官网上第一条常见内存问题)能够达到内存泄漏解除。但是,这是为什么呢?
这是因为,置空了View的引用,导致LeakCanary的引用因子变低,它采用的是引用个数达到5个才认为泄漏;更甚,来回切换Fragment,不断增加fragment数量size,仍然会出现内存泄漏,LeakCanary反而不报objects占用了,岂不是掩耳盗铃

所以,把内存泄漏的锅,甩给到Fragment内部View的引用,我一直觉得奇怪,Fragment的生命周期存活才是问题的关键,Fragment的成员变量有什么错呢?这个锅,成员变量不背!
内存泄漏检查的是gcRoot,它只检测了fragment的子对象对于fragment本身的引用,但是没有统计基础库FragmentManager对于fragment的引用, 其实这是错误的。

3.3 源码分析

在于FragmentManager,FragmentStore的引用问题。

	//FragmentManager.class
    private final ArrayList<Fragment> mAdded = new ArrayList<>();
    private final HashMap<String, FragmentStateManager> mActive = new HashMap<>();
    
    	//FragmentStateManager中包含mFragment
		//FragmentManagerViewModel.class的成员
		private final HashMap<String, Fragment> mRetainedFragments = new HashMap<>();
  • mAdded是不会包含Destoryed的Fragment的;
  • 但是mActive里面的FragmentStateManager中包含mFragment这个hashMap。

从调试结果来看,可以看出mActive中包含了Destory的那个Fragment(即,用变量保存过View、Button的Fragment)。
当然不仅仅在mActive这个map中持有引用,其实还有FragmentManagerViewModel mRetainedFragments等等都会持有。

boolean beingRemoved = f.mRemoving && !f.isInBackStack();
if (beingRemoved) {
    makeInactive(fragmentStateManager);
}

mRemoveing是true了,但是mBackStackNesting = 2,因为被添加到了回退步骤中。所以无法被移除mActive。

所以添加了addBackStack,就不会被makeInactive,进而不会被mActive移除,进而持有着fragment的引用,导致size不断增加(如图中,来回切换了几次后,size都到了10,本来就是2个Fragment的事情)。由于我们在onDestoryView View =null,更加导致LeakCanary不报错了,掩耳盗铃

通过阅读fragment库的源码,了解到 mActive和mRetainedFragments在标记一个FragmentStore.java(老版本FragmentManagerImpl.java) makeInactive() 的时候,从map中移除或者设置null,才能让Fragment失去引用,也就能把内存回收了。

4. 正确使用Fragment

直接Replace : onDestroyView() – onDestroy() – onDetach()
Fragment会被回收。
Replace 并且 addToBackStack : onDestroyView()
Fragment不能回收。

根本原因是replace fragment和addBackStack的使用有误,和检测内存泄漏机制的问题。
回到前面写的代码。每次都是new新的Fragment,并且直接replace并将他添加addToBackStack!这才是内存泄漏的根本原因。还会导致backStack的步数增加!
做个试验,上面代码去除addToBackStack,size有1个。其他的老Fragment被移除了引用。

所以正确的使用是如下几条:

4.1 内存问题,通过tag缓存Fragment:

FragmentManager fragmentManager = getSupportFragmentManager();
Fragment fragment = fragmentManager.findFragmentByTag(TAG_COMBINE); //tag
if (fragment == null) {
  fragment = new CombineFragment(); //没有才新建
}
FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction
  .replace(R.id.root_layout, fragment, TAG_COMBINE) //tag
  .addToBackStack(null)
  .commit();

这样能够保证,不论你是否addToBackStack,最多只会有1对1的Fragment个数在内存中。即例子中,2个Fragment来回切换,缓存最多只有2个,不会随着切换操作到10个,100个;

所以强烈建议使用TAG缓存,即使你有必要2个同名Fragment一起显示的时候,也可以使用不同Tag做区分。解决内存泄漏的关键点。降低到1个Fragment的冗余。

4.2 addToBackStack与popBackStack 成对出现解决步数问题:

通过4.1tag的方法,已经解决了内存泄漏问题,但是没有解决我们backStack的步数问题;有addToBackStack,就一定要有popBackStack() 的反操作;
而且在popBackStack()以后,对应的Fragment也会被移除mActive,回收内存,有断点调试为证。

4.3 onDestoryView解除引用

在onDestoryView(),解除对于Fragment的子View引用 = null。
根据前面的分析,这仅仅是为了应对leakCanary和公司的类似检测机制而已。根本上来讲(addToBack的)Fragment是被FragmentManager所引用的,内部子对象,从链条上看,打破没有意义。仅仅降低了LeakCanary的内存泄漏因子。

4.4 数据重复加载问题:

因为,replace回来,就必定会onCreateView和其他生命周期,即使是使用Tag的缓存策略。
这样必然导致如果在onCreateView等进行了数据请求操作,会重复加载。

这里给出2种解决方案:

  1. 数据放在activity级别以上,LiveData,activity的变量,甚至单例都可以。然后在Fragment创建以后,加载保存过的数据直接显示;
  2. 通过add,show,hide来替代replace。
    记录一些代码:
//activity onCreate或者onNewIntent调用:
//主要目的是为空,就新建;不为空,需要判断,当前是不是第一个Fragment,要么回到他;要么就不用管了。
private void processIntent() {
          if (getIntent() != null) {
         Uri uri = getIntent().getData();
         String type = uri != null ? uri.getQueryParameter("type") : "";
         if (type满足条件) {
             jumpToBFragment();
             return;
         }
     }

     FragmentManager fragmentManager = getSupportFragmentManager();
     Fragment fragment = fragmentManager.findFragmentByTag(TAG_COMBINE);
     FragmentTransaction transaction = fragmentManager.beginTransaction();
     if (fragment == null) {
         fragment = new CombineFragment();
         transaction.replace(R.id.root_layout, fragment, TAG_COMBINE)
                 .commit();
     } else {
         List<Fragment> frs = getSupportFragmentManager().getFragments();
         boolean needPopAndShow = false;
         if (frs != null && frs.size() > 1) {
//                for (Fragment f : frs) { //double check
//                    if (fragment == f && !f.isVisible()) {
//                        needPopAndShow = true;
//                    }
//                }
             needPopAndShow = true;
         }
         if (needPopAndShow) {
             Log.d(TAG, "pop and show");
             fragmentManager.popBackStackImmediate();
             transaction.show(fragment).commit();
         }

         CombineFragment cf = (CombineFragment) fragment;
         cf.onNewIntent();
     }
}

//跳转到下一个Fragment.  
private void jumpToBFragment() {

FragmentManager fragmentManager = getSupportFragmentManager();
Fragment toFragment = fragmentManager.findFragmentByTag(TAG_ONE_TOP);
 if (toFragment != null && toFragment.isAdded() && fromNewIntent) {
     OneTopFragment oneTopFragment = (OneTopFragment) toFragment;
     oneTopFragment.onNewIntent(); //刷新数据
 } else {
     toFragment = new OneTopFragment(); //add显示
     Fragment current = fragmentManager.findFragmentByTag(TAG_COMBINE);
     FragmentTransaction transaction = fragmentManager.beginTransaction();
     if (current != null) {
         transaction.hide(current);
     }
     transaction.add(R.id.root_layout, toFragment, TAG_ONE_TOP)
             .addToBackStack(null)
             .commit();
 }
}

//backTo第一个Fragment
private void backToFirst() {

FragmentManager fragmentManager = getSupportFragmentManager();
     fragmentManager.popBackStackImmediate();

     Fragment fragment = fragmentManager.findFragmentByTag(TAG_COMBINE);
     Log.d(TAG, "backToCombine: combineFragment exist " + (fragment != null));

     FragmentTransaction transaction = fragmentManager.beginTransaction();
     if (fragment == null) {
//            fragment = new CombineFragment();
//            transaction.replace(R.id.root_layout, fragment, TAG_COMBINE)
//                    .commit();
         //如果返回找不到CombineFragment,直接就关闭啦。因为说明oneTop是被其他单独拉起的。但是通过event key回去被系统popBack却没有实现。
         finish();
     } else {
         transaction.show(fragment).commit();
     }
     return true;
}

 //处理下OnBack按键的逻辑
 @Override
 public void onBackPressed() {
     super.onBackPressed();
     FragmentManager fragmentManager = getSupportFragmentManager();
     Fragment fragment = fragmentManager.findFragmentByTag(TAG_COMBINE);
     Log.d(TAG, "onBackPressed: combineFragment exist " + (fragment != null));
     if (fragment == null) {
         finish();
     }
 }

这样的目的是,其实是隐藏了一下,图层而已。内存消耗没有减少。因为Fragment是可以多个,往上贴的。通过控制第一个Fragment的show,hide来达到隐藏的目的。
这个就看你的使用场景了。比如我的CombineFragment就是希望常驻,oneTopFragment只是偶尔弹出,最后回退到CombineFragment希望它不会变。
其他部分代码,是决策Fragment的backStack。自行查看。

5. toolbar导致的内存泄漏

引用一下:(https://blog.csdn.net/ganduwei/article/details/82844848)

((AppCompatActivity) getActivity()).setSupportActionBar(mToolbar)这句代码导致Activity中引用了Fragment的mToolbar,如果Fragment关闭后,没有去掉这个引用就会导致无法释放Fragment。

LoginFragment中有创建菜单,而它的上一级Fragment没有创建菜单,这样导致从LoginFragment返回到上一级后,AppCompatActivity中的FragmentManangerImpl没有执行dispatchCreateOptionsMenu方法,所有mCreatedMenus中还是保存了LoginFragment的实例。如果上一级Fragment有创建菜单不会有此问题;

5.1 Fragment中的菜单由自己来创建,不交给Activity,代码如下:

    mToolbar.setNavigationIcon(R.drawable.ic_back);
    mToolbar.setNavigationOnClickListener(v -> {
    });
    mToolbar.inflateMenu(R.menu.toolbar_menu);
    mToolbar.setOnMenuItemClickListener(menuItem -> {
        return true;
    });

5.2 菜单还是交给Activity管理,如果上一级Fragment有创建菜单那不用处理,如果没有需要在上一级Fragment清除掉引用,代码如下:

    ((AppCompatActivity) getActivity()).setSupportActionBar(null);
    ( getActivity()).onCreatePanelMenu(0,null);

onCreatePanelMenu方法会使dispatchCreateOptionsMenu被调用,从而给mCreatedMenus重新赋值。

当然最好是使用第一个方法,每个Fragment中的菜单由自己来管理。

研究了以后,我发现,我敢使用Fragment了!

你可能感兴趣的:(Android,android,java,开发语言)