天天看点

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

1. 目的

因为Fragment的用法实在太多,这里为了记录下,给出建议方案,避免造成内存泄漏,只要保证了内存不泄漏,你可以随便折腾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. 对比帖子

针对上述第一点Fragment的介绍,也就是如下链接中的泄漏了:

https://www.jianshu.com/p/15ad41477f34 这个帖子和LeakCanary的检测机制其实

误导

了很多人。看我的分析。

3.1 源码记录

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

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

A BFragment里面的代码,可以用全局变量保存一些View、Button。

3.2 源码分析

在于FragmentManager,FragmentStore的引用问题。

private final ArrayList<Fragment> mAdded = new ArrayList<>();
    private final HashMap<String, FragmentStateManager> mActive = new HashMap<>();
    //FragmentStateManager中包含mFragment

	FragmentManagerViewModel
	private final HashMap<String, Fragment> mRetainedFragments = new HashMap<>();
           

其中mAdded是不会包含已经Destory的Fragment的;但是mActive里面的FragmentStateManager中包含mFragment这个hashMap。从调试结果来看,可以看出mActive中包含了Destory的那个Fragment(即,用变量保存过View、Button的Fragment)。

当然不仅仅在mActive这个map中持有引用,其实还有FragmentManagerViewModel mRetainedFragments等等都会持有。

通过阅读fragment库的源码,大致了解清楚,mActive和mRetainedFragments在标记一个FragmentStore.java(老版本FragmentManagerImpl.java)

makeInactive()

的时候,从map中移除或者设置null,才能让Fragment失去引用,也就能将内存回收了。

直接Replace : onDestroyView() – onDestroy() – onDetach()

Fragment会被回收。

Replace 并且 addToBackStack : onDestroyView()

Fragment不能回收。

3.3 断点调试

所以我们断点来调试,要关注,FragmentManager下面的

mFragmentStore(FragmentStore fragment1.2.6库,FragmentManagerImp 1.1.x库)下面的:

mAdded;

**mNonConfig **这个ViewModel的mChildConfigs的size;

mActive的Size。

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

通过断点和置空前后对比,

不论是否在onDestoryView给所有的成员View变量button = null,这些size都是在不断的膨胀!

经过分析,这是只能说是LeakCanary的“一种误报”,因为在addBackStack的,Fragment退出之后,Fragment会存留在FragmentManager的缓存中。

而原简书帖子之所以后面没有报,即在onDestoryView之后,去除View的引用,也是官网上的第一条常见内存问题。只是因为没有达到LeakCanary的内存占用比例因子。而且,在

onDestoryView,设置View = null,

,来回切换Fragmnet的时候,出现不断增加size,仍然是出现了内存泄漏(虽然可能部分View内存被回收,但是在这些mActive的对象仍然是不断增加的),

LeakCanary还不报objects占用了,岂不是掩耳盗铃

所以网上的帖子把内存泄漏的锅,给到Fragment内部View的引用,我就觉得十分奇怪,Fragment的生命周期存活才是问题的关键,成员变量有什么错呢?这个锅,成员变量不背!
LeakCanary的设计理念认为Fragment onDestoryView后就不该有内存。其实这是错误的。android的Fragment设计就是有缓存,会被设置为CREATE状态,后续用的时候,直接拿出来变变状态就好了。出现这种情况,也是因为他的收集原理是根据GCroot来算的。

3.4 size增长的继续分析

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不报错了,更是掩耳盗铃!

4. 答案-正确使用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个;

所以这个代码,必须使用,从缓存中直接拿去,除非你有必要2个Fragment一起的时候,才考虑新建,但是仍然建议通过不同的Tag来区分;

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

上面1.已经解决了内存泄漏问题,但是没有解决我们之前写的代码的backStack的步数问题,在返回的时候会非常多步;有addToBackStack,就一定要有popBackStack() 系列的操作;

而且在popBackStack()以后,对应的Fragment也会被移除mActive,回收内存,断点调试为证;

  • 我就是想,不看见这个Fragment以后,不做缓存:

    这个2.也已经解释。

4.3 数据重复加载问题:

因为,replace回来,就必定会onCreateView和其他生命周期,即使是使用1.的缓存策略。

这样必然导致如果在onCreateView等进行了数据请求操作,会重复加载。

这里给出2种解决方案:

  • 自行缓存数据到activity级别,使用LiveData也好,在activity里面保存也好,然后在Fragment创建以后,加载报错过的数据直接显示。代码就略了;
  • 通过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。自行查看。

  • 错误示范

网上也有例子说,通过onCreateView,来缓存View,if mView != null return mView;

我认为是有问题的;生命周期都是新创建的;每次mView都是空。不建议如此操作。交给FragmentManager来管理不好嘛。

4.4 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了!