内存优化

一. 前言

内存问题在 APP 中是个大问题,常见的问题有内存抖动、内存泄漏和内存溢出。

二. 常用工具

线下工具介绍:

  1. Memory Profiler
    Android Studio 自带的一个工具,用实时图表展示应用内存使用量,方便用来识别内存抖动、内存泄漏等,还提供捕获堆转储、强制 GC 以及跟踪内存分配的能力。


    Memory Profiler

    可以看到一共分为三块:CPU、MEMORY、NETWORK,我们主要分析MEMORY这一块。
    点击MEMORY这一块可以放大,


    MEMORY

    图中标注1的地方就是GC,如果点击一下这个按钮就会强制进行一个GC。
    图中标注2的地方是堆转储,就是将内存中的信息转成一个文件。
    图中标注3的地方是某个时间片内的内存使用情况。

    图中标注4的地方是到当前时间的内存使用情况。

现在我们来点击标注2的位置来dump一个文件:

左边的classname是类的名字,右边有Allocations表示分配了多少对象,Nativesize,ShallowSize表示自己的大小,RetainedSize表示支配的大小。

接下来我们看一个Bitmap:

点击之后,右边显示的是创建的对象,点击其中一个对象。
右下角出来一个Reference,可以看到它的一个使用情况。

再看一个Progressbar:

点击右下角的地方可以直接跳到代码中使用的位置。

  1. Memory Analyzer(MAT)
    强大的 Java Heap 分析工具,查找内存泄漏以及内存占用,生成整体报告分析问题等。
    下载地址: https://www.eclipse.org/mat/downloads.php
    (1)open overview pane是一个概览信息
    第一行有size、classes、objects、ClassLoader,还有Unreachable Objects Histogram(可以被回收的对象,但是仍然在内存当中)

(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

泄漏疑点
  1. LeakCanary
    自动内存泄漏检测。
    地址: https://github.com/square/leakcanary
    缺点:虽然使用了 IdleHandler 与多线程,但是 dumphprof 的 SuspendAllThread 的特性依然会导致应用卡顿。

三. 内存问题

3.1 内存抖动

  1. 表现:
    使用 Memory Profile 工具检测,内存曲线呈锯齿状。
  2. 定义:
    内存频繁分配和回收导致内存不稳定。
  3. 危害:
    导致卡顿、OOM。
  4. 为什么内存抖动会导致 OOM?
    频繁创建对象,导致内存不足及碎片,不连续的内存碎片无法被分配,导致 OOM。
  5. 解决方法
    使用 Memory Profiler 或 CPU Profiler,点击"record",查看当前内存分配情况,然后结合代码排查。
  6. 解决技巧
    找循环或频繁调用的地方。

3.2 内存泄漏

  1. 表现:
    可用内存逐渐变少。
  2. 定义:
    内存中存在已经没有用的对象。
  3. 危害:
    内存不足、OOM。
  4. 解决方法
    使用 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 即可。
  5. 常见的内存泄漏场景
    (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 内存溢出

  1. 表现:
    APP崩溃并报Out Of Memory异常。
  2. 定义:
    分配的内存被用光了。

四. 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 优化方式

  1. 统一图片库
    图片内存优化的前提是收拢图片的调用,这样我们可以做整体的控制策略,比如低端机使用 565 格式,而且需要进一步将所有 Bitmap.createBitmap、BitmapFactory 相关的接口也一并收拢。

  2. 对 bitmap 图片大小进行优化
    (1)继承 ImageView,复写实现计算大小
    缺点:侵入性强、不通用
    (2)ARTHook
    运行时插桩,修改大小

  3. 重复图片复用

五. 线上内存监控

上面优化都是线下进行的,但是对于线上的应用就需要做到监控。

  1. 方案
    当超过 APP 最大内存 80% 时,执行Debug.dumpHprofData() 下载文件,然后回传文件到服务器,之后用 MAT 手动分析。
  2. 缺点
    (1)dump 文件太大,和对象数正相关,可裁剪;
    (2)上传失败率高、分析困难;
    (3)配合一定策略,有一定效果;

你可能感兴趣的:(内存优化)