作者:杨超,腾讯移动客户端开发 工程师
商业转载请联系腾讯 WeTest 获得授权,非商业转载请注明出处。
原文链接:http://wetest.qq.com/lab/view/359.html
历时五天的内存优化已经结束,这里总结一下这几天都做了什么,有哪些收获。优化了,或可以优化的地方都有哪些。(因为很多事还没做,有些结论需要一定样本量才能断定,所以叫一期)一期优化减少 JavaHeap 内存占用约 26.5M。
在任何性能优化之前,要做的第一件事就是找到性能瓶颈!而找到性能瓶颈通常需要强大的 debug 工具辅助。内存方面 Android 有 AndroidStudio 的 Android Profiler、Allocation Tracker,以及 Eclipse 的 MAT 用于分析 java 的内存占用,相当强大。而偏向 native 层面的内存占用则找不到太好的工具,因此这里在做优化前,先造了几个工具。
1. 线程创建分析工具
该工具使用 native hook 的方式,直接 hook 了 pthread_create 调用,并记录每一个线程创建时的堆栈,并打印日志。同时维护一个 running thread 的集合,必要时 dump 下来所有 running thread 的创建堆栈,用于分析野蛮线程创建的场景。以及对应的日志分析工具。
2、 Linux /proc//smaps 文件分析脚本
主要用于跟踪进程的 Code 部分内存(见下文)占用,分析出占用内存较多的 dex,so 文件。排查第三方 SDK 占用过多内存场景。网上只能找到一个 perl 脚本,功能不是很强大,鉴于笔者不熟悉 perl 的语法规范,改起来会比较困难,因此直接用 python 重写了一个。
代码在这里:https://gist.github.com/LanderlYoung/aedd0e1fe09214545a7f20c40c01776c
3、 快速 Dump Android java heap 脚本
因为分析内存需要很多 dump 操作,所以干脆写了个 Bash 脚本。
Bash 脚本链接:https://gist.github.com/LanderlYoung/9cd0f49e49e42746622cc8e7b4bbcc8a
(顺便提一下,android 提供的 hprof-conv 工具有个参数 -z 用于排除 zygote 的内存,十分便利。)
通常我们在系统的内存管理页面看到的内存占用是进程的 PSS,也就是整个进程的内存占用,因此我们做优化的要考虑到所有的内存,不仅仅是 Java Heap。
使用 Android Studio(3.0 beta)的 Android Profiler 工具。
我们可以很清晰的看到
1)进程总内存占用: 180M
2)JavaHeap: 48M
3)NativeHeap:native 层的 so 中调用 malloc 或 new 创建的内存 —— 28M
4)Graphics:OpenGL 和 SurfaceFlinger 相关内存 ——58M
5)Stack:线程栈——1.89M
6)Code:dex+so 相关代码占用内存——37.75M
7)Other:蜜汁存在
上述 6 中内存占用除了两种不需要考虑,其他 5 中通通需要优化。不需要考虑的是:
1)Other:暂时无从分析
2)Graphics:若应用没有直接调用 OpenGL,则可以确定这部分内存是由 Android Framework 操控的,可以忽略。(当然对于游戏类应用,这里肯定是优化重点。)
下面按照内存分类分开逐一介绍分析方法,和结论:
JavaHeap
这里必然是内存优化的重点,无需多言。但是企鹅 FM 的业务,UI,代码已经比较庞大,分析起来会显得力不从心。因此这里主要从两个方面入手,希望能总结出一套分析方法。
1、分析应用 静息态 内存占用。
所谓静息态,是笔者自行定义的概念:
应用在退后台之后,不保留活动的场景下的内存占用。
为什么要考察这个维度?因为这个是一个应用内存占用最低点的时候,后续打开任何 Activity 内存只会更多,不会更少!
2、分析方法
1)开发者选项开启 “不保留活动”
2)进入 MainActivity,滑动页面,操作一下
3)退后台,Android Studio 中强制执行 GC
4)dump java heap(注意上面提到的 hprof-conf 加上 -z 参数排除 zygote 的干扰)
5)MAT 分析 dump 下来的 JavaHeap
重点介绍一下 MAT:
这里可以直接打开 domanitor_tree 看占用内存最多的实例。
从这里按照 RetainedHeap 倒序排列,一点一点的排查内存占用。很容易发现不正常的内存情况。
在企鹅 FM 中发现:
1)图片的内存级缓存退后台没清空(此处属于 onTrimMemory 回调的处理有误),占用 10M 内存
2)ImageMisc — 280k
①
② 是一个 buffer,可以在不用的时候释放内存
③ 优化目标,彻底干掉
3)播放页应用动画的关系,UI 是单例。其中相关 View 占用数百 K 内存,而 button 的 icon 直接引用住了 5-6M 的 bitmap 资源。
4)播放列表存储了 103 个 ShowInfo,每个 ShowInfo 22k,总计内存约 2.24M,ShowInfo 冗余信息很多,可以考虑优化数据结构
5)DanmuManager — 510k
● mDanmuItemManager 内含众多弹幕
● 每条弹幕 6k
● UI 相关数据,离开播放页后应该清理弹幕(因为无需展示了)
● 优化目标,彻底干掉。
6)FileCacheService — 362k
①
② 其中缓存了每一个 cache entry,其中图片缓存较多
③ 每一个 entry 记录完整文件路径其比较长,因此路径的字符串占用了很多内存
④ 优化方案:
● 文件 Parent 可以共用同一个 File 对象。
● entry = new File(parent, “entry_name”)
⑤ 优化目标 到 100k
7)LiveRoomShowListManager -- 287k
①
② 优化目标:UI 相关数据,离开界面应该彻底干掉
8) DB InsertHelper, Sql Statement clearBinding
① 700K 到 2M
② InsetHelper 中会引用住最后一次执行 DB insert 调用的 数据(占位符)
③ InsertHelper 的占位数据可以在 insert 完成之后清掉
针对上面提到的 ShowInfo 的数据结构优化
拟定优化方案:
1)ShowList 存储的 ShowInfo 数量过多,30 个足矣。
2)ShowInfo 中 Album 字段占用 10k 内存,其实同一个 ShowList 中大多数 album 是完全一致的(比如专辑类型的 ShowList,主播类型的,自选集类型的,本地专辑的,etc...)。
预计内存占用 2M -> 30*12K = 360K
3)静息态内存优化总结:
上述几点加起来预期可以减少内存占用:
10M + 280K + 5M + 2M + 510K + 260k + 287k + 1M = 约 20M
3、 MainActivity 操作一段时间之后内存增量
上面分析的是静息态内存,下面看一下 MainActivity 操作一点时间之后,内存有怎样的变化。
这里采用的方式是:
1)dump 静息态内存
2)进入 MainActivity,立即 dump 内存
3)操作一段时间之后再 dump 内存
一共有三次 dump,可以利用 MAT 对比 heap 的功能对比内存增量。
打开 MAT 的 historgram 视图
工具栏最右边有个双箭头的 icon,点击可对比 dump:如下图
增量最多的还是 Bitmap(底层用 byte[] 存储),借助 MAT 的 Finer 工具可以直接看到 Bitmap 的图片。
这里发现的几个问题是(时间关系,应该多次测试的,会发现更多问题):
① Banner 的大图没有 Clip 导致 分辨率 很高
② 分类页的 配置区域 没有 Clip
③ onRecycle 没有清除掉已经引用的 Bitmap,导致引用住不能 gc
主要说一下第 3 点,是 Banner 每一个 Item 有一个大图做背景,当 item 的 view 被回收的时候,相应的 ImageView 仍然持有着大图,导致其不能回收。这里发现了 4 张 1M+ 的大图,其实理论上应该只有 1 张。
这个问题可以推广到所有的 ListView 场景,建议方式是:
替换为 RecyclerView,在 view 回收的时候,ScarpView 释放图片引用。
此外,MainActivity 有 5 个 tab,各个 tab 之间其实会用到相同的 View(listview 的 item),如果使用RecyclerView 可以做到 5 个 tab 的 RecyclerView 共同复用同一个 RecyclerPool,在节省内存的同时还能显著提高性能。
这里不方便直接测试内存占用,预估可以节省内存 5-10M。
4、 正常操作应用,观察内存占用图表是否有突起
这里主要用来测试异常内存分配的场景。
这里仍然需要很大人力,过很多页面。
目前发现问题有:
1)service 进程,发送 wns 请求的时候,内存异常增长 2-3M。
这时可以使用 AllocationTracker 工具(点击下图工具栏的红点),记录峰值那一段内存的分配,如图:
这里可以直接看到分配的栈,定位过去看,发现是这样的代码,因为 head 是一个 65536 长的数组(在 com.tencent.wns.session.Session 的构造函数写死的长度),这里创建 string 就浪费了超大量的内存。建议可以改成下图弹窗里的样子
2)另外一个问题是播放进程,在切换节目的时候内存会突然增长 2-3M,简单跟进去看是 exo 创建 buffer。似乎有问题,需要再多分析一下~
Native Heap
目前能看到的 NativeHeap 大小是
应用启动:26M 此时已经初始化了 X5 内核和 IM SDK
UGC 录音:26M->34M 退出之后时 32M,还有部分没释放,疑似内存泄漏
发起直播:32M->72M 退出之后 42M,同样没有完全释放
具体内部占用情况还没测。。。(都说了是一期)。
官方文档:
https://source.android.com/devices/tech/debug/native-memory
Code
这一段明显看到占用了很多内存。各个场景下的使用情况是:
1)刚进入应用:38M
2)再使用 UGC 录音:38.28M
3)再使用视频直播(发起直播):46M
4)打开应用内 WebView(X5 内核):56M
以上是主进程的内存,占用相当多。需要注意的是 code 内存占用一般是通过 read-only 方式 mmap 映射到内存中的的 dex、odex、so 等文件,因此在内存紧张的情况下,系统会回收这些内存,只是在 oom-killer 中仍然会计算在内。
另外播放进程 2.27M,service 进程 1.1M 还属于比较正常的水平。
显然主进程的 Code 内存占用太多了,需要分析。这里通过解析 Linux 标准的 /proc//smaps 文件,这个文件记录了进程内每一段虚拟内存的文件映射情况,这个文件只有进程自己有读权限,所以要么用 root 的机器,要么就自己写段代码 copy 出来。结合上面提到的工具。分析结果如下:
● 应用 so 占用 app so map Rss = 3984 kB(其中 IM SDK 2576k)
● 应用的 dex 占用 app dex map Rss = 15101 kB
● X5 内核的 so+dex 内存占用 tbs mem map Rss = 29048 kB
● 直播 so 相关 avlive mem map Rss = 3092 kB
● 其中X5 内核的代码没有打进 apk,因此可以比较独立的统计出来,占用有 29M 之多,让人惊讶!
● 其次直播的 java 代码打进了 apk 不方便单独统计内存用量,但是 so 是独立加载的,内存占用 3M 也是不少的。
● 最后是应用自身的 dex 占用有 15M 之多,因为自身代码量很大,似乎可以理解,但是仍然很多啊!
这里需要考虑的是 X5 内核能否延时加载?因为没打开 WebView 的时候就已经占用了数 M 了。另外 WebView 关闭之后是否可以销毁。
直播相关 SO,可以考虑直播退出之后从内存中卸载掉。(java 规范是加载 so 的 classloader 被 GC,相关 so 即可卸载)。
应用自身 dex 占用。android 8.0 对 art 优化一个叫做 DexLayout 的能力,应为 mmap 映射的文件不会被立即加载进内存,在用到的时候是按照页大小(4k)加载的,当用到的类在 dex 中分布很分散的时候,就会导致盲目加载很多页,DexLayout 就是把热点类集中放到一起。这里 FaceBook 推出了 ReDex 工具,可以参考一下。
PS:关于 DexLayout
在 AndroidStudio 的 Memory Profiler 中没有线程数这个维度。但是运行中,主进程的线程数量通常会在 100 个左右,这是个惊人的数字,要知道 Mac 版的 AndroidStudio 也不过 77 个线程。。。。请自行体会一下。
关于线程的创建和内存占用,请参考笔者的另一篇文章:《Android 创建线程源码与 OOM 分析》。
这里分析用的自制工具,dump 下载所有 running 的线程,和他们创建时的堆栈。
结果是:
● X5:25 个线程(简直。。。)
● IMSDK:17 个线程
● StackBlur:8 个线程
● WNS:7 个线程
● ImageLoader:6 个线程
● magnifiersdk:5 个线程
需要注意,这里的栈和线程名,是创建线程的时候的调用栈,以及对应的线程名(而不是子线程名)
事实上,用同样的方法,还可以分析一下进程历史中所有创建过的线程,统计哪里创建线程最多。
通常来说,所有线程应该有应用统一的线程池来管理,sdk 内部需要线程池,应该有外部注入一个线程池来提供给 sdk 使用。
如果有其他情况,如:不是在线程池创建的线程,在 sdk 自己的线程池里创建的线程,这种都可能导致线程数量的野蛮增长,需要联系 sdk 的开发人员杜绝这种情况。
以上就是这 5 天的工作结果:
java 内存占用基本合理,静息态 内存占用可以优化 20M,MainActivity 运行时的内存占用可以优化 5-10M。
code 内存占用太多,其中 X5 内核占用 29M 实在太多,需要考虑优化。
应用内的线程数量主要有 X5 内核,IMSDK 和 WNS 贡献,外网线程创建的 OOM crash 系 WNS 的 bug,需要联系相关 sdk 开发人员。
最后是 Native 内存占用还没有详细分析,暂时看不到使用情况。但是可以知道目前的结论是:Native 内存占用很多,且应该存在内存泄漏。
PS: 实际效果反馈
按照上述分析结果,进行了相关的代码调整。
执行的点包括:
1、IntelliShowList pageSize 50->20
2、IntelliShowList 公用 Album 结构
3、Afc-db clearBinding after insert, 数据库
4、Afc-FileCacheService cache Entry with fileName not full path, 文件缓存
5、 fix onTrimMemory bug,退后台清空图片内存缓存
6、播放页相关控件,退后台之后清掉 icon,释放 bitma 引用
未执行的点包括:
1、播放页的 bottomPannel 部分 icon 因为逻辑较为复杂,暂时未进行处理。预计内存占用 1M
2、PlayLogic 的 historyList 逻辑复杂暂时未处理,预计内存占用 500K
3、24h 直播间 LiveRoomShowListManager -- 287k
4、DanmuManager — 510k
5、经过 ice 提醒,下载节目的 record 也会全部加载进内存。每个 ShowInfo 22k,内存占用取决于用户下载的节目数。
效果对比:
before:39.32M
after:12.88M
优化内存占用 26.44M!
UPA—— 一款针对 Unity 游戏/产品的深度性能分析工具,由腾讯 WeTest 和 unity 官方共同研发打造,可以帮助游戏开发者快速定位性能问题。旨在为游戏开发者提供更完善的手游性能解决方案,同时与开发环节形成闭环,保障游戏品质。
目前,限时内测正在开放中,点击http://wetest.qq.com/cube/ 即可预约。
对 UPA 感兴趣的开发者,欢迎加入 QQ 群:633065352
如果对使用当中有任何疑问,欢迎联系腾讯 WeTest 企业 QQ:800024531