一. 前言
内存问题在 APP 中是个大问题,常见的问题有内存抖动、内存泄漏和内存溢出。
二. 常用工具
线下工具介绍:
-
Memory Profiler
Android Studio 自带的一个工具,用实时图表展示应用内存使用量,方便用来识别内存抖动、内存泄漏等,还提供捕获堆转储、强制 GC 以及跟踪内存分配的能力。
可以看到一共分为三块:CPU、MEMORY、NETWORK,我们主要分析MEMORY这一块。
点击MEMORY这一块可以放大,
图中标注1的地方就是GC,如果点击一下这个按钮就会强制进行一个GC。
图中标注2的地方是堆转储,就是将内存中的信息转成一个文件。
图中标注3的地方是某个时间片内的内存使用情况。
图中标注4的地方是到当前时间的内存使用情况。
左边的classname是类的名字,右边有Allocations表示分配了多少对象,Nativesize,ShallowSize表示自己的大小,RetainedSize表示支配的大小。
接下来我们看一个Bitmap:点击右下角的地方可以直接跳到代码中使用的位置。
- Memory Analyzer(MAT)
强大的 Java Heap 分析工具,查找内存泄漏以及内存占用,生成整体报告分析问题等。
下载地址: https://www.eclipse.org/mat/downloads.php
(1)open overview pane是一个概览信息
(2)histogram 直方图
这个条目里面详细的列出了某个详细的class有多少实例,以及某个实例的Shallow Heap和Retained Heap。
右键可以看到两个选项:
with outgoing references:这个类引用到了那些类
with incoming references:这个类被那些类所引用
一般用下面的。
(3)dominator_tree
(4)OQL
(5)thread_overview
(6)Top Consumers
(7)Leak Suspects
- LeakCanary
自动内存泄漏检测。
地址: https://github.com/square/leakcanary
缺点:虽然使用了 IdleHandler 与多线程,但是 dumphprof 的 SuspendAllThread 的特性依然会导致应用卡顿。
三. 内存问题
3.1 内存抖动
- 表现:
使用 Memory Profile 工具检测,内存曲线呈锯齿状。 - 定义:
内存频繁分配和回收导致内存不稳定。 - 危害:
导致卡顿、OOM。 - 为什么内存抖动会导致 OOM?
频繁创建对象,导致内存不足及碎片,不连续的内存碎片无法被分配,导致 OOM。 - 解决方法
使用 Memory Profiler 或 CPU Profiler,点击"record",查看当前内存分配情况,然后结合代码排查。 - 解决技巧
找循环或频繁调用的地方。
3.2 内存泄漏
- 表现:
可用内存逐渐变少。 - 定义:
内存中存在已经没有用的对象。 - 危害:
内存不足、OOM。 - 解决方法
使用 Memory Profiler 初步观察,然后点击“Dump Java Heap”生成 .hprof 文件,通过 "hprof-conv 原文件路径转换后文件路径" 命令进行转换,然后使用 MAT 结合代码确认问题。
具体实战:
(1)使用 AndroidProfiler 的 MEMORY 工具
运行程序,对每一个页面进行内存分析检查。首先,反复打开关闭页面 5 次,然后收到 GC(点击 Profile Memory 左上角的垃圾桶图标),如果此时 total 内存还没有恢复到之前的数值,则可能发生了内存泄漏。此时,再点击 Profile Memory 左上角的垃圾桶图标旁的 heap dump 按钮查看当前的内存堆栈情况,选择按包名查找,找到当前测试的 Activity,如果引用了多个实例,则表明发生了内存泄漏。
(2)使用 MAT 定位泄漏位置
打开 Overview 界面,最常用的就是 Histogram 和 Dominator Tree。
Dominator Tree:支配树,按对象大小降序列出对象和其所引用的对象,注重引用关系分析。选择 Group by package,找到当前要检测的类(或者使用顶部的 Regex 直接搜索),查看它的 Object 数目是否正确,如果多了,则判断发生了内存泄漏。然后,右击该类,选择 Merge Shortest Paths to GC Root 中的 exclude all phantom/weak/soft etc.references 选项来查看该类的 GC 强引用链。最后,通过引用链即可看到最终强引用该类的对象。
Histogram:直方图注重量的分析。使用方式与 Dominator Tree 类似。
(3)对比 hprof 文件,检测出复杂情况下的内存泄漏
通过对比方式:在 Navigation History 下面选择想要对比的 dominator_tree/histogram,右击选择 Add to Compare Basket,然后在 Compare Basket 一栏中点击红色感叹号(Compare the results)生成对比表格(Compared Tables),在顶部 Regex 输入要检测的类,查看引用关系或对象数量去进行分析即可。
针对于 Historam 的快速对比方式:直接选择 Histogram上方的 Compare to another Heap Dump 选择要比较的 hprof 文件的 Historam 即可。 - 常见的内存泄漏场景
(1)资源对象没有关闭造成的内存泄漏;
解决方法:当资源对象不再使用时,应该立即调用它的close() 函数,将其关闭,然后再置为 null。
(2)注册没有取消造成的内存泄漏;
(3)集合中对象没清理造成的内存泄漏;
(4)非静态内部类的静态实例;
原因:
首先,非静态内部类默认会持有外部类的引用,然后又使用了该非静态内部类创建了一个静态的实例,该静态实例的生命周期和应用的一样长,这就导致了该静态实例一直会持有该Activity的引用,导致Activity的内存资源不能正常回收。
解决方法:
一种是将非静态内部类改成静态内部类,第二种就是将内部类抽取出来,封装成一个单例,如果需要使用 Context,尽量使用 Application Context。
(5)Handler 引起的内存泄漏
原因:
当Handler发送消息的时候,这个Message还没有处理完成,这个message以及发送Message的对象handler都将一直被线程所持有,其中Handler是TLS变量,也就是Handler的生命周期和Activity的生命周期是不一致的,另外创建Handler的时候用的是匿名内部类,所以会持有Activity,当页面销毁的时候还会有handler存在,也就是Activity不能被回收。
解决方法:
(1)将Handler声明为静态的
(2)通过弱引用的方式引入Activity。
(6)Webview造成的内存泄漏
Webview在解析网页的时候,会申请native堆内存,用于保存页面的元素。
解决方法:
将webview所处的activity放在一个单独的进程当中,在检测到应用内存占用过大的时候,调用 android.os.Process.killProcess(android.os.Process.myPid());主动杀掉进程。
3.3 内存溢出
- 表现:
APP崩溃并报Out Of Memory异常。 - 定义:
分配的内存被用光了。
四. Bitmap 优化
4.1 Bitmap 内存模型
(1)API 10 之前 Bitmap 自身在 Dalvik Heap 中,像素在 Native(好处:这些像素不占用 Java 层的内存,缺点:Java 层的 bitmap 已经被回收掉了,但是 native 层不知道)
(2)API 10 之后像素也被放在 Dalvik Heap 中;
(3)API 26 之后像素在 Native(但是在 Java 层回收 bitmap 时,会通知 native 层回收像素)
(4)获取 Bitmap 占用内存
动态计算获得:getByteCount
静态获得:宽 * 高 * 一像素占用内存
4.2 优化方式
统一图片库
图片内存优化的前提是收拢图片的调用,这样我们可以做整体的控制策略,比如低端机使用 565 格式,而且需要进一步将所有 Bitmap.createBitmap、BitmapFactory 相关的接口也一并收拢。对 bitmap 图片大小进行优化
(1)继承 ImageView,复写实现计算大小
缺点:侵入性强、不通用
(2)ARTHook
运行时插桩,修改大小重复图片复用
五. 线上内存监控
上面优化都是线下进行的,但是对于线上的应用就需要做到监控。
- 方案
当超过 APP 最大内存 80% 时,执行Debug.dumpHprofData() 下载文件,然后回传文件到服务器,之后用 MAT 手动分析。 - 缺点
(1)dump 文件太大,和对象数正相关,可裁剪;
(2)上传失败率高、分析困难;
(3)配合一定策略,有一定效果;