内存泄漏案例5-某App121个内存泄漏#
视频通话App是屏端实时通讯功能的App,提供语音通话、视频通话、好友列表查询、好友添加等功能。
笔者在工作之余编写内存泄漏案例分析的时候,抓取了一份某设备的视频通话App内存1Hprof1文件,发现有该App存在121个`Fragment/Activity1类型内存泄漏点,下面我们来分析下这些内存泄漏问题。
影响范围#
某App以下页面
下面我们来分析下666型号的内存问题。
复现步骤#
- 屏端进入视频通话App,每个页面快速进入退出10次,在每个页面随机点10次该页面内的按钮
- 手机智家App发起音频通话10次,发起视频通话10次,发起群组通话10次;
- 操作结束后抓取hprof文件。
或者也可monkey执行特定App的事件
1 | adb shell monkey -p com.haier.fridge.multideviceschat --throttle 300 --ignore-crashes --ignore-timeouts --ignore-native-crashes --monitor-native-crashes --ignore-security-exceptions --pct-touch 5 --pct-motion 15 --pct-appswitch 40 --pct-trackball 10 --pct-nav 5 --pct-majornav 20 --pct-syskeys 5 --bugreport -v -v -v 100000 0>E:/monkey_log0.log 2>E:/monkey_log2.log 1>E:/monkey_log1.log |
内存日志抓取指令#
由笔者曾经编写的软件故障日志提取步骤可知——抓取App内存Hprof日志指令如下:
1 | adb shell am dumpheap com.haier.fridge.multideviceschat /data/anr/multideviceschat-多个页面点至少10次.hprof |
问题现场录屏/截图#
App所用内存情况:App占用内存$315,053,776$byte
由笔者之前写过的案例《内存泄漏案例1》可知,Memory Profile
能显示出Activity/Fragment
类型的泄漏点,我们查阅文件,可以清楚的看到App有121个内存泄漏点。
值得重视的是,这还未包含非
Activity/Fragment
类型的内存泄漏。
- 选择对象类型为
Activity/Fragment
- 查看直接泄漏点,可以看到
hprof
文件里有121处Activity/Fragment
相关的内存泄漏 - 点击
Allocations
,选择按分配数量排序,从上至下我们先来看泄漏数量较多的类
笔者默认读者已经阅读过内存泄漏案例系列1-4,已经熟悉Memory Profile
的工具使用,接下来分析每个类型以及每个类型的多个实例。
原因分析#
FriendDetailActivity#
泄漏点1#
老规矩查查看实例是否符合泄漏的条件:1、实例是否处于生命周期销毁阶段 2、内存确实未回收
- 在
classname
区域选择要观察的类型FriendDetailActivity
- 在
Instance
区域选择要观察的具体实例 - 在
Fields
区域选择观察实例的属性,这是判断实例是否应该被回收的重要依据
可以看到FriendDetailActivity#mDestroyed
为true
,表明页面已经离开了窗口,此实例本应该回收,却未回收,符合内存泄漏的条件.
- 在
References
区域,查看GC root
,找寻谁持有了FriendDetailActivity
实例
第三步,我们先来看FriendDetailActivity
问题源码:
1 | private void initCallDevice() { |
此处代码调用视频通话SDKMDVService.get().queryPermission2Friend
,传入了匿名内部类DeviceListCB
,此类是SDK内部定义的抽象类,定义了一个CallBack
事件
1 | public abstract class DeviceListCB extends BaseCallback { |
我们跟随SDK代码,追踪调用链
1 | MDVService.queryPermission2Friend |
每一次调用SDK接口,SDK都会初始化一个unsetDinyDevicesDisposable
实例,此实例持有页面传入的DeviceListCB
实例,而DeviceListCB
实例又持有了页面Activity
实例。
我们分析出了导致内存泄漏的原因,下面有2个解决思路:
- 阻断
DeviceListCB
对页面Activity
的引用 - 页面退出时,释放
SDK
对DeviceListCB
引用
查询SDK文档后,未能找到满足思路2需求的API接口,遂转而采用思路1,实现思路1的方法又很多种,前面的案例也都有涉及,我们在这里采取静态内部类+弱引用的方式。
修改前:
1 | private void initCallDevice() { |
修改后:
1 | static class MyDeviceList extends DeviceListCB{ |
我们用上述方法分析FriendDetailActivity剩余的18个实例,定位到所有的泄漏点,都是调用视频通话SDK相关的,下面我们逐一优化视频通话SDK的使用
泄漏点2#
优化前:
1 | private void initCallRecording() { |
优化后:
1 | static class MyFriendCallRecordCB extends CallRecordCB{ |
泄漏点3#
优化前:
1 | MDVService.get().setPermission4Friend(friendId, deviceId, isChecked, new DeviceListCB() { |
优化后:
1 | static class MyFriendDeviceListCB extends DeviceListCB{ |
泄漏点4#
优化前
1 | private void deleteFriend() { |
优化后:
1 | static class MyFriendNormalCB extends NormalCB{ |
泄漏点5#
优化前:
1 | private void changeNickName(String newName) { |
优化后
1 | static class MyChangeNormalCB extends NormalCB{ |
优化结果#
我们按照同样的步骤,进入好友详情,随机点击该页面按钮,退出好友详情,重复十次。
在相同的操作次数下,皆进入、退出10次该页面
结论:由图GC Root=null,depth=null
可知,FriendDetailActivity
不再被视频通话SDK引用,FriendDetailActivity
页面销毁后,内存会被回收,该页面内存泄漏优化成功。
优化前单个FriendDetailActivity
的retained Size
为$322,815$byte
,优化后为$304$byte
,单个实例内存占用减少为原先的$1/1061$。
优化前FriendDetailActivity
总占据内存$4,149,181$byte
;优化后FriendDetailActivity
总占据内存$912$byte,
内存占用减少为原先的$1/4549$,内存节省了$4$mb
DeviceReminderSettingsActivity#
第二个类型DeviceReminderSettingsActivity
,是视频通话的设备提醒页面,用于设置设备之间的来电提醒开关。
根据前面的案例,我们已经很熟练的能够找到泄漏点源码位置,这个页面有2个泄漏点,泄漏点的原因也是非静态的匿名内部类持有当前类的引用,下面介绍下优化前后的效果对比
泄漏点1#
优化前
1 | MDVService.get().setTotalPermission(bean.getDeviceId(), !bean.getIsDeny(), |
泄漏点1优化后
1 | MyUpdateDevicesList myUpdateDevicesList = new MyUpdateDevicesList( |
泄漏点2#
优化前
1 | MDVService.get().queryMyDeviceList(new DeviceListCB() { |
泄漏点2优化后
1 | private void updateDevicesList() { |
两个泄漏点共同使用同一个静态内部类+弱引用绑定页面的元素
下面是一个极端示例,静态内部类传入过多的参数,实际上我们可以只传一个Activity
,通过Activity.
的方式引用Activity
的成员属性,读者可以自行取舍
1 | static class MyUpdateDevicesList extends DeviceListCB { |
优化结果#
优化后,由图GC Root
为null
,Depth
为null
可知,DeviceReminderSettingsActivity
不再被视频通话SDK引用,页面销毁后,内存会被回收,该页面内存泄漏优化成功
优化前,3022634byte
;优化后948byte
,内存占用减少为原先的 $1/3188$,内存节省了3mb
NewFriendActivity#
NewFriendActivity
是申请好友页面,用于二维码申请添加好友,也会展示出申请添加记录。
按图索骥找到问题代码
泄漏点1#
优化前:
1 | private void getQrCode() { |
泄漏点1优化后:
1 | static class MyQrCb extends QrCB{ |
泄漏点2#
优化前:
1 | private void addFriend(String targetAccount, String message) { |
泄漏点2优化后:
1 | static class MyAddNormalCb extends NormalCB{ |
泄漏点3#
优化前:
1 | MDVService.get().handleFriendReq(recordId, agree, new NormalCB() { |
泄漏点3优化后:
1 | static class ReqNormalCB extends NormalCB{ |
泄漏点4#
优化前:
1 | MDVService.get().queryAllFriendReqRecord(new AllFriendReqRecordCB() { |
泄漏点4优化后:
1 | static class MyAllFriendReqRecordCB extends AllFriendReqRecordCB{ |
泄漏点5#
优化前:
1 | static class DeleteNormalCB extends NormalCB{ |
优化结果#
相同的操作步骤和操作次数$10$次,优化前,该页面总共持有Retained size
为$7,597,880$byte
;
相同的操作步骤和操作次数10次,优化后,该页面总共持有Retained size
为$328$byte
,可知内存占用减少为原先的$1/23164$,
ChatActivity#
ChatActivity
是视频通话App的单人聊天页面,用于当前设备与其他设备1对1聊天
单人聊天页面有两个典型业务——语音聊天和视频聊天。
首先来看测试了语音聊天情况:
- 拨打单人聊天
- 接通
- 挂断
重复10次,得到一份hprof文件如图所示
老步骤,如图所示进入问题代码区域
1 | case R.id.btn_accept: |
优化后:
1 | MyOnCallActionResultCallback myOnCallActionResultCallback = new MyOnCallActionResultCallback(ChatActivity.this, mUiHandler); |
值得注意的是,此页面语音通话相关的流程有20多处泄漏点,凡是涉及视频通话SDK接口导致的泄漏点就不一一举例了,省略不写;
下面介绍一下ChatActivity
非视频通话SDK相关的内存泄漏点
泄漏点1-定时器#
优化前:
实例mCountDownTimer
的作用是用于开启通话后,在界面上显示通话时间。
1 | mCountDownTimer = new CountDownTimer(MAX_CALL_TIME_LIMIT, 1000) { |
优化后:
1 | mCountDownTimer = new MyTimer(MAX_CALL_TIME_LIMIT, 1000,ChatActivity.this); |
优化后Retained size
内存如图所示,连续开启、结束10次语音聊天,ChatActivity
类型只占据$982$byte
内存,相比优化前的$695,512$byte
,内存占用变为原来的$1/708$
泄漏点2-View与Context#
优化前:
定位到问题代码:hashMap
缓存了HaierOpenGlView
,HaierOpenGlView
实例持有了Activity
的引用,导致页面引用的内存无法释放
1 | private Map<String, HaierOpenGlView> viewMap = new HashMap<>(); |
泄漏点2优化后代码:
在创建view实例的时候,传入application context,而非Activity;
1 | HaierOpenGlView view = new HaierOpenGlView(getApplicationContext()); |
在页面退出的时候,清空map
持有的view
,进而释放view
持有的Activity
1 |
|
泄漏点3-匿名线程#
优化前:
1 | onCreate(...){ |
view所在的布局参数
1 | <!-- 预览的view,一般设置1px即可,采集数据无需显示--> |
三个问题点
view
的定义是有问题的,构造函数默认传的是Activity
,view
不会主动释放持有的Activity
,导致Activity
内存泄漏- 匿名
Runnbale
持有当前Activity
的引用——静态内部类+弱引用 - 视频通话
sdk
持有view
,且无法释放 ——排查SDK
是否提供释放view
的接口
泄漏点3优化后:
移除view
在xml
的定义,改为动态创建,传入BaseApplicationContext
;在onDestroy
中释放view
对context
的引用
1 | private HaierCaptureTextureView mHctvTextureData; |
优化结果#
解决完该页面所有处泄漏点后,我们来总结下ChatActivity页面的内存优化效果:
ChatActivity
页面优化前,retainedSzie为$695,703$byte
,Activity
多个实例无法被回收
优化完,retainedSize
为$491$byte
,内存节省为原先的$1/1416$,Activity
只剩一个实例,depth
为0,表明过一会垃圾回收器会回收
此外,连续多次拨打视频类型通话,10次通话前后的视频通话App
内存一直稳定在$149$mb左右,从内存变化曲线也可知,表明此次ChatActivity
内存优化成功。
GroupChatActivity#
GroupChatActivity
是多人通话页面,内存泄漏情况有视频通话SDK导致的,也有其他原因的,原因都与ChatActivity
类似,就不一一举例了,解决内存泄漏的方法参考同上。
DeviceSetFragment#
泄漏点1#
老步骤,按图所示,查看问题代码
可知内存泄漏的原因在于SDK
不正确的使用,非静态内部类持有当前DeviceSetFragment
实例的引用,导致DeviceSetFragment
内存泄漏
1 | MDVService.get().queryMyDeviceList(new DeviceListCB() { |
优化后:
1 | MyDeviceListCB myDeviceListCB = new MyDeviceListCB(this); |
上述问题优化后,持续复测此页面10次,又发现了新的泄漏点。
泄漏点2-内部类广播#
泄漏点问题代码:未释放、未解绑的广播,广播是Fragment的内部类,持有了当前Fragment
的实例引用,造成Fragment
内存泄漏
1 | private DeviceLocalBroadcastReceiver deviceBroadcastReceiver; |
泄漏点2优化后:
1 | deviceBroadcastReceiver = new DeviceLocalBroadcastReceiver(this); |
泄漏点1和2优化完后,增加了一些新步骤持续测试10次,又发现了新的泄漏点。
泄漏点3-MVP架构#
可以看到引用链为MDVService->MainPresenter->Activity—>Fragment
,点击查看源码,看到又是视频通话SDK持有了MainPresenter
,Activity
作为MainPresente
r实现类,也被视频通话SDK引用,造成了内存泄漏。此处是典型的MVP内存泄漏场景。
泄漏点3问题代码
1 | MainPresenter.java |
泄漏点3优化后:
1 | private void login(MDVConfig config) { |
泄漏点3优化完后,继续复测10次,又出现了新的泄漏点,看来此Fragment的泄漏问题非常严重。
点击查看源码,发现我们自定义的Application里持有了Activity,间接持有了Fragment,导致了内存泄漏
泄漏点4#
问题代码:
1 | UMApplication.java |
可以清晰的看到Application
中定义了一个非静态内部类ActivityLifecycleCallbacks
,它持有了Application
也持有了Activity
,这样导致Activity
即使调用finsh
,处于Destroyed
状态,Activity
申请的内存依然无法释放,导致Activity
的内存泄漏。
同时我也注意到,上一任开发者已经关注此处的内存泄漏问题了,已经再内部类中的另外一个内部方法onActivityDestroyed
释放了Activity
和networkStateChangeListener
,可依然会导致Activity内存泄漏,这是为什么呢?
答案是:Activity
的与Fragment
的引用并未斩断,Activity
进入onDestroyed
的时候,Fragment
还没来得及释放,导致Fragment
仍然引用着Activity
,此时Activity
的内存也就泄漏了。
怎么办?
- 在
Activity#onDestroy
·释放Fragment
1 | Activity.java |
Application
里判断Activity
的状态,处于Destroyed
状态且内存未释放时,再次释放持有的fragment
1 | Application.java |
优化结果#
优化后Fragment
只存在一份实例,对比优化之前存在多份实例,表明内存优化成功;由于该Fragment
是App首页,所以并不会销毁:
优化前:$702,609$byte
;优化后$1,734$byte
,内存减少至原先的$1/405$
另外一个FriendListFragment
也存在此类SDK调用传入匿名内部类问题,
FriendListFragment#
本页面有3处SDK调用,存在3处内存泄漏点
泄漏点1#
优化前:
匿名内部类持有Fragment
的实例引用
1 | /** |
泄漏点1优化后
1 | static class MyAllFriendReqRecordCB extends AllFriendReqRecordCB{ |
泄漏点2#
优化前:
1 | /** |
泄漏点2优化后:
1 | private void queryAllFriends() { |
泄漏点3#
优化前:
1 | private void searchFriends() { |
泄漏点3优化后:
1 | static class MyFriendsCB extends FriendsCB{ |
优化结果#
优化前,点击切换10次,FriendListFragment#retainedSzie
为$470019$byte
,内存中会有多份实例
优化后,点击切换10次,FriendListFragment#retainedSzie
为$453,257$byte
,当显示FriendListFragment
时,内存中只有一份实例,表明此次 优化成功:
全部泄漏点优化结果#
总结一下视频通话App的内存优化结果:
「每个页面点击十次,页面内按钮随机点击10次,在同样的操作步骤下」
优化前,App占用内存$315,053,776$byte
;优化后,App占用内存$97,970,395$byte
,App总内存节省至原先的$1/3$,极大的节省了系统资源
优化前内存泄漏数为121,优化后内存泄为0,表明优化过的地方不再出现泄漏,121个Activity/Fragment
泄漏点已经完全解决。