Android内存泄露的排查

在一次项目开发中,测试提到了一个问题,在详情数据页左右反复滑动多次,页面会越来越卡顿,加载速度也会越来越慢,需要我排查一下问题。
由于这个详情数据页面是一个Fragment,数据量特别大,并且嵌套在ViewPage中可以左右滑动,并且ViewPage的页数可以非常多。那么首先想到的问题是ViewPage + Fragment,Fragment没有被正确回收导致的。
首先来排查下Fragment的生成逻辑:

1
2
3
4
5
6
7
8
@Override
public Fragment getFragment(Fragment fragment, int currentPage) {
DemoFragment fragment = new DemoFragment();
Bundle bundle = new Bundle();
bundle.putInt("page", currentPage);
fragment.setArguments(bundle);
return fragment;
}

每次翻页都会重新加载Fragment,这点确实可能会导致问题,但是检查自定义控件逻辑后发现,每次切换页面,都会remove旧的Fragment并清空,虽然这样处理并不是很友好,但是并不会导致越来越卡顿。尝试替换原生ViewPager,问题依旧,那么暂时可以排除问题不在这里。

内存泄露排查

那么这时候来看看手机真实的资源状态吧,是什么原因导致他为什么越来越卡顿,是CPU占用过高,还是内存溢出。于是打开了Android Profiler,在资源监控器下,滑动页面,查看占用情况。CPU占用还算正常,但是内存占用竟然随着滑动一直在飙升,最高占用到了1.5G,测试机是4G的内存,1.5G已经占用很多了,并且此时虚拟机已经开始在频繁GC,几乎是1-2秒GC一次,这不对,因为平时APP的内存占用只有200MB左右,正常情况下,GC后内存占用会回到正常水平,这一定是哪里发生了泄露。

首先点击Memory的折线图,点击上方的Dump java heap,此时系统开始读取手机的内存,读取速度取决于占用的大小,经过一段时间的等待,Hrap Dump的页面展示了出来,此时可以看到上方有一个黄色的警告,Leaks竟然有192处,点击Leaks按钮之后,会列出来系统判断出内存泄露的类,点击类会在下方显示没有被回收的实例,再次点击Instance实例,点击右边的References,可以看到是什么类持有了他的引用。那么通过这个引用,结合类中的代码,就可以判断出到底是哪里发生了泄露,进行解决即可。

简单介绍下页面的几个值的含义:

  • Allocations:堆中的实例数
  • Depth:从任意 GC root 到所选实例的最短 hop 数
  • Native Size:c/c++层中内存的大小
  • Shallow Size:此堆中所有实例的总大小
  • Retained Size:为此类的所有实例而保留的内存总大小(以字节为单位)

解决问题

问题已经定位到了,那么现在该进行解决了。

初步解决的方法,在页面onDestroy的位置主动取消订阅即可。经过这次修改,内存使用回到了正常,内存泄露被解决。但是这并不是根本问题,因为使用了RxLifecycle之后,应该不需要取消订阅才对,RxLifecycle会自动取消的,但是取消订阅功能未生效,这是为什么?

检查发现是PublishSubject持有了引用,查阅代码发现,是项目中使用RxBus导致的,RxBus是Rxjava中PublishSubject封装后类似EventBus的实现,由于Rxjava使用不当会造成内存泄露,项目中也使用了RxLifecycle来保证Rxjava的订阅会被及时释放。但是从内存泄露的地址来看,确实是因为PublishSubject中依旧持有引用,那么是什么原因导致的呢?

首先关注到的是RxFragment是否正常起作用了,因为项目是由android support升级到androidx的,但是RxLifecycle并没有进行升级,引用的仍然是support包的Fragment,而这是否会造成生命周期绑定错误呢?讲RxLifecycle升级到androidx版本后,问题依旧。

通过观察RxFragment的源码实现,看到其中是使用了BehaviorSubject类来对生命周期进行控制,并且绑定了Fragment的全部生命周期,当Rxjava的某个订阅到达了原本设定取消订阅的生命周期,便会取消订阅。
LifecycleTransformer中的apply方法,一但满足条件,就会执行takeUntil()来对订阅进行解绑。takeUntil是一个Rxjava操作符,含义为:发出值,直到提供的 observable 发出值,它便完成。以后有机会整理Rxjava知识的时候再细说。
判断是在compose中出现了问题,随后查询到一篇文章中,也提到了这个问题:

在compose的时候

  1. 如果until事件是FragmentEvent.DESTROY_VIEW,fragment的请求在onDestoryView之后发出,会出现问题。原因:在onDestoryView的时候,因为请求还没有发出,队列是空的,所以BehaviorSubject$BehaviorDisposable.emitNext不会调用到,而之后,DESTROY_VIEW事件被后面的DESTORY和DETACH事件依次冲掉,所以请求返回时由于事件不是DESTROY_VIEW,所以请求不会被取消,最后请求的回调依然被调用
  2. 如果until事件是FragmentEvent.DETACH,那么在onDestoryView和onDetach之间返回了请求结果的话也会发生问题。

而快速滑动翻页确实可能会造成这个问题。而检查代码发现,compose绑定的生命周期,正是FragmentEvent.DESTROY_VIEW,那么将手动订阅删除掉,将绑定的生命周期改为FragmentEvent.DESTROY,再次测试,问题解决。

1
.compose(bindUntilEvent(FragmentEvent.DESTROY))

总结

经过两天一头雾水的排查,各种内存检查工具的使用,Mat的使用,adb查询native的占用,viewpager的修改,甚至进入到PublishSubject,RxLifecycle看源码的实现逻辑,打断点找问题。实际上可以说的东西有很多。最后解决问题的方法反而异常的简单,越看越觉得RxLifecycle的实现方式非常精巧,很多部分甚至完全看不懂为什么要这么实现,甚至由于我对Rxjava的了解非常的浅薄,很多操作符,模式现在都无法领会。也了解到了还有一个takeUntil的操作符,这个操作符其实可以简化很多复杂的逻辑代码。甚至包括发现了AutoDispose这个类似RxLifecycle的库,依赖的是androidx新Lifecycle,后面项目库慢慢升级的时候,也会用到。

一个内存泄露问题,学到了很多,也发现了自己很多不足。

参考:

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×