实践 App 内存优化:如何有序地做内存分析与优化,flutter 免费视频教程
由于项目里之前线上版本出现过一定比例的 OOM,虽然比例并不大,但是还是暴露了一定的问题,所以打算对我们 App 分为几个步骤进行内存分析和优化,当然内存的优化是个长期的过程,不是一两个版本的事,每个版本都需要收集线上内存数据进行监控以及分析。
版本迭代过程中,内存增长过快,不仅会导致一定概率的 OOM,运行时若出现内存抖动,导致频繁 GC,则会对 App 的流畅度以及用户体验造成很大影响。
本文主要会根据实际项目中优化步骤分为以下几部分:
Android 内存分析基础
内存泄漏
静态内存分析优化
运行时内存分析优化
监控
1.Android 内存分析基础
这部分主要先介绍一些进行内存分析的基础方法以及工具,对这部分比较熟悉的同学可以先跳过哈。
一.App 的内存使用情况概览
每个 App 进程可以分配到的最大内存是有限的,当然不同手机每个 App 进程可以分配到的最大内存有可能不一样,可以通过以下命令进行查看:
adb shell getprop | grep dalvik.vm.heapsize
我们可以输出我们 App 的内存使用情况概览:
adb shell dumpsys meminfo 包名
我们就可以看到:
Pss
: 该进程独占的内存+与其他进程共享的内存(按比例分配,比如与其他 3 个进程共享 9K 内存,则这部分为 3K)
Privete Dirty
:该进程独享内存
Heap Size
:分配的内存
Heap Alloc
:已使用的内存
Heap Free
:空闲内存
二、Android Profiler
AndroidStduio3.0 后 Android Profiler 变得比之前更强大,内存分析页变得更加直观更加方便,下面是截图:
进程占用总内存
javaHeap
:这部分内存大小是有限制的,溢出则会 OOM,这部分内存也是我们分析优化的重点NativeHeap
:native 层的 so 中调用 malloc 或 new 创建的内存,对于单个进程来说大小没有限制,所以可以利用在 native 层分配内存来缓解 javaHeap 的压力(比如 2.3.3 之前 Android Bitmap 的内存分配就是在 native 层,之后移到 javaHeap, 8.0 又回到 native)Graphics
:这部分一般游戏 app 中用的较多,OpenGL 和 SurfaceFlinger 相关的内存,若没有直接调用到 OpenGL,则一般不会涉及到这块内存Stack
:栈,了解 jvm 内存模型的应该都知道Code
: 代码,主要是 dex 以及 so 等占用的内存Others
:就是 others 啦
所以我们可以看到事实上我们可以优化的点有:JavaHeap、NativeHeap、Stack、Code 所占用的内存
三、强大的 MAT
MAT 是做比较细致的内存分析的利器了,功能十分强大,其中的:
Hisogram
:Lists number of instances per class
Dominator Tree
:List the biggest objects and what they keep alive.
可以非常方便的排序查看当前内存中最占内存的 class 或者实体对象,而且有一条非常清晰的引用链来查看该对象的持有者,这对内存的分析以及内存泄漏的分析都是非常友好的。
同时 MAT 支持compare对比功能
,将两个.hprof 文件导入,都 Add to Compare Basket 之后即可进行对比,这对于对比某个页面相较与前一页面的内存增量来说是非常有意义的。
有一点比较不友好的是,MAT 需要标准的.hprof 文件,所以在 AndroidStduio 的 Profiler 中 GC 后 dump 出的内存快照还要自己手动利用 android sdk platform-tools 下的 hprof-conv 进行转换一下才能被 MAT 打开。
当然如果觉得麻烦的话也可以自己写个脚本执行几条命令来直接完成 GC->dump java heap->转换.hprof 文件 这个流程:
//adb and hprof-conv
ADB=${ANDROID_HOME}/platform-tools/adb
HPROF_CONV=${ANDROID_HOME}/platform-tools/hprof-conv
//GC
(PACKAGE_NAME)
//dump java heap
(PACKAGE_NAME) $(OUT_PATH)"
//conv hprof
{FILE_NAME} droid-${FILE_NAME}
2.内存泄漏
根据以往经验,其实做内存优化最先要搞定的应该是内存中的大头,这类大头对内存的占用很大,也是内存问题的主要祸首,相对来说比较容易定位问题,且优化后效果也非常明显,性价比非常高。
事实上很多优化都是这样,比如减包大小的优化,也是要先分析出主要大头祸首,比如可能你的包里包含了一张 3M 大小的无用图片,如果你没找到这种祸首,可能你做了大量的工作去想办法减少无用代码等,最终可能只有几百 K 的收益。
相对内存来说,这个大头就是:
内存泄漏
图片
所以首先你要确保你的应用里没有存在内存泄漏,然后再去做其他的内存优化。
内存泄漏检测
现在内存泄漏的检测已经变得非常简便了,使用 App 后在 Android Profiler 中先触发 GC 然后 dump 内存快照,之后点击按 package 分类,就可以迅速查看到你的 App 目前在内存中残留的 class,点击 class 即可在右边查看到对应的实例以及引用对象。
当然你也可以在 debug 下集成 LeakCanary 做内存泄漏
监控警告
排除内存泄漏后,图片就是另一个占用内存大头的对象了。
图片
对于图片来说一个是颜色模式
,检查一下项目里的图片的颜色模式,是否可以降低,比如从 RGB_8888 降到 RGB_565,则每张图片可以节省 1/2 的内存,如果没有使用到透明通道等的话基本上肉眼看不出差别。
还有一个是降低图片的大小
,可能你的 ImageView 只有你图片的一半大,则这部分内存就大大浪费了,我们项目服务端会根据前端的参数做动态切图。
前端也可以通过降低采样率(inSampleSize)
来达到降低图片占用内存大小的目的,但是这个采样率 InSampleSize 只能是整数(甚至只能是 2 的次方),如果 inSampleSize=2,则最终内存占用就会是原来的 1/4,适用于图片过大很多的情况,对于只是想做小幅度压缩的话,基本没用。
ok,接下来开始做具体的内存分析与稍微细致一点的内存优化。
3.静态内存分析优化
这边说的静态内存指的是在伴随着 App 的整个生命周期一直存在的那部分内存,也就是打底的,具体获取这部分内存快照的方式是:
打开 App 开始重度使用 App,基本打开每一个主要页面主要功能,然后回到首页,进开发者选项打开"不保留后台活动",然后将我们的 app 退到后台。最后 GC,dump 出内存快照。
下面是我们 app dump 出的内存快照,进行分析后制图如下:
通过对静态内存数据的分析,主要发现了以下几个问题:
问题 1:?App 首页的主图有两张(一张是保底图,一张是动态加载的图),都比较大,而且动态加载的图回来后,保底图并没有及时被释放
优化:首先是对首页的主图进行颜色通道的改变以及压缩,可以大大降低这两张图所占的内存,然后在动态加载图回来后及时释放掉保底图?-5M
问题 2:?首页底部的轮播背景图占用内存 1.6M,且在图片加载回来后,背景图一直没有置空
优化:首先一般来说对背景图的质量并没有很高的要求,所以这张背景图是可以被成倍压缩的,并且在图片加载回来后,背景图要及时的释放掉。同时首页的多张轮播图以及其他图片都可以进行颜色模式的改变以及质量压缩。?-1.6M -4M
问题 3:?项目会在 App 启动时拉一个接口获取一些实验配置,放进单例,在内存分析时发现,这些实验配置竟然接近 1M
优化:排查后发现,接口拉的是整个公司所有部门的实验配置,上千个,这也给遍历拿一个实验配置带来一定的性能损耗,推动接口去改进,只获取当前部门业务需要的实验配置,可节省内存 90%以上?-700K
问题 4:?发现几个 lottie 动画一直没有被回收,并且同一个 lottie 动画会有几个不同的实例存在,总共占用内存 450K
优化:首先要确定几个 lottie 动画为什么在页面退出后没有被回收,并且同一个动画有几个不同的实例,很容易就联想到内存泄漏,由于页面没有被销毁,所以导致几个 lottie 动画也没有被回收,排查下来是项目里的 RN 页面存在内存泄漏,解决后大概可以节省 3-5M 内存
问题 5:?SharePreference 在内存里占用了 700K 的内存
优化:由于 SP 中的东西是会一次性加载到内存里并且保存为静态的,直到 App 进程结束才会被销毁,所以 SP 中千万别放大的对象,别图一时方便把对象序列化成 json 后保存到 SP 里,优化点就是把已经保存在 SP 中的一些较大的 json 字符串或者对象迁移到文件或者数据库缓存。?-400K
问题 6:?埋点数据
优化:产品或者运营为了统计数据会在每个版本不断的增加新埋点,但是也需要定期去清理掉一些过时的不需要的埋点,来适当优化内存以及 CPU 的压力。
问题 7:?还有就是一些 App 里的单例以及一些静态缓存
评论