Android性能优化系列:内存优化

内存的理论知识

在了解如何优化内存前,我们需要先知道内存从分配到回收的整个流程,从理论开始了解内存的由来和引起内存问题的原因,这涉及到 JVM 相关的知识,掌握了理论知识才有优化内存的方向:

JVM 运行时数据区(栈和堆)

JVM GC

JVM Hotspot 虚拟机与 Dalvik&ART 虚拟机堆栈的区别

JVM 字节码文件与类加载

Android性能优化系列:Bitmap

App 的内存限制

Android 给每个 app 分配了一个虚拟机(具体指的是 ART 虚拟机),让 app 运行在 Dalvik/ART 上,这样即使 app 崩溃也不会影响到系统。系统给虚拟机分配了一定的内存大小,app 可以申请使用的内存大小不能超过此硬性逻辑限制,就算物理内存富余,如果 app 超过虚拟机最大内存,就会出现 OOM。

由程序控制操作的内存空间在 heap (堆)上,分 Java heapsize 和 native heapsize:

  • Java 申请的内存在虚拟机的 heap,所以如果 Java 申请的内存大小超过虚拟机的逻辑内存限制,就会出现 OOM

  • native 层内存申请不受其限制,native 层受 native process 对内存大小的限制

Android 为 App 分配多少内存

Android 为我们提供了 adb 命令可以直接查看设备可分配的内存信息,而该内存信息也是读取的系统配置信息数据文件 /system/build.prop,通过命令 adb shell cat /system/build.prop | grep heap 查看每个 ART 虚拟机可分配的内存大小:

// 当 zygote fork 一个 app 进程分配一个虚拟机时
// 虚拟机的内存限制就是从这个文件读取的
dalvik.vm.headstartsize=16m // 起始内存大小
dalvik.vm.heapgrowthlimit=192m
dalvik.vm.heapsize=512m // 最大内存大小
dalvik.vm.heaptargetutilization=0.75 // 内存扩容指标
dalvik.vm.heapminfree=512k
dalvik.vm.heapmaxfree=8m

我们也可以在 AndroidManifest.xml 的 application 节点添加 android:largeHeap=“true” 属性申请提供给当前 app 更多的内存。

代码获取分配的 app 内存大小:

ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
am.getMemoryClass(); // 单位 MB

ActivityManager.java

static public int staticGetMemoryClass() {
    // Really brain dead right now -- just take this from the configured
    // vm heap size, and assume it is in megabytes and thus ends with "m".
    String vmHeapSize = SystemProperties.get("dalvik.vm.heapgrowthlimit", "");
    if (vmHeapSize != null && !"".equals(vmHeapSize)) {
        return Integer.parseInt(vmHeapSize.substring(0, vmHeapSize.length()-1));
    }
    return staticGetLargeMemoryClass();
}

static public int staticGetLargeMemoryClass() {
    // Really brain dead right now -- just take this from the configured
    // vm heap size, and assume it is in megabytes and thus ends with "m".
    String vmHeapSize = SystemProperties.get("dalvik.vm.heapsize", "16m");
    return Integer.parseInt(vmHeapSize.substring(0, vmHeapSize.length() - 1));
}

如果有做 ROM 需要对系统内存做性能调优,可以修改源码的 AndroidRuntime.cpp 对内存的配置:

/frameworks/base/core/jni/AndroidRuntime.cpp

int AndroidRuntime::startVm(JavaVM** pJavaVM, JNIEnv** pEnv, bool zygote)
{
	...
    /*
     * The default starting and maximum size of the heap.  Larger
     * values should be specified in a product property override.
     */
    parseRuntimeOption("dalvik.vm.heapstartsize", heapstartsizeOptsBuf, "-Xms", "4m");
    parseRuntimeOption("dalvik.vm.heapsize", heapsizeOptsBuf, "-Xmx", "16m"); // 修改这里
    ...
}

内存问题

在 Android 中主要需要解决的内存问题有三类:

  • 内存抖动:频繁创建销毁对象触发 GC 内存不稳定,通常伴随着 app 卡顿(GC 会引发 STW【Stop The World】)。在 Memory Profiler 体现为运行的内存波动图形呈现锯齿状。

  • 内存泄露:

    • 简单理解就是长生命周期的对象持有短生命周期的引用,在该回收的时候短生命周期对象引用因为被其他对象持有引用而无法释放内存,这就导致了内存泄露

    • 如果 app 频繁卡顿时通常也伴随着可能存在内存泄露

    • 因内存泄露导致的可用内存变少,系统在申请内存时因内存不足而会频繁 GC 产生内存抖动

  • 内存溢出:长期的内存泄露和创建大对象无法分配足够的内存时就会触发 OOM

内存优化实际上主要是解决内存抖动和内存泄露问题,而 OOM 只是这两者引发的果。

发生 OOM 的条件

OOM 的发生往大的方向解释是长期的内存泄露和内存不足最终引发的结果,往细方向解释发生 OOM 的条件可以分为以下五点:

  • Java 堆内存溢出、无足够连续内存空间:内存泄露和创建大对象内存不足的场景会引发的 OOM

  • fd 数量超出限制:其中 epoll 机制就是会持有的 fd,fd 其实可以等同于文件,也可以理解对 fd 的操作就是 IO 操作,只是这个 IO 更偏向于内核。fd 数量过多也就是 IO 挂起比较多也可能会导致 OOM

  • 线程数量超出限制:线程的数量影响内存是因为开启线程是会占用内存空间的(分配虚拟机栈),系统对线程的数量是有限制的,例如 Linux 系统的线程数量限制就是 1000 个左右,但是基本上你开辟的线程数量还没达到系统上限就已经无法维持

  • 虚拟内存不足:其中发生 OOM 会分为物理内存和虚拟内存,如果是物理内存不足就是整个系统的内存不足,而我们常说的内存其实是虚拟内存,例如分配给 app 的每个虚拟机内存就是虚拟内存,虚拟内存不足时主动抛出 OOM

造成卡顿的原因(内存抖动)

造成卡顿的原因有多种,但不外乎以下几点:

  • 数据加载事件过久(在主线程处理耗时操作)

  • 渲染事件过久(主要是布局嵌套过多绘制时间拉长)

  • 线程阻塞

  • 内存抖动:频繁 GC 触发 STW 导致的帧率下降

出现内存抖动的核心原因主要有两点:

  • 在绘制期间有大量数据产生(例如在 onMeasure、onLayout、onDraw 直接 new 对象)

  • 在循环或轮询的代码里产生对象过多,且处于阈值上

卡顿的主要因素是线程运行时间问题,内存最直接影响卡顿的问题就是内存抖动频繁 GC 触触发 STW 影响了 16.66ms 一帧绘制导致跳帧,最终结果就是卡顿

内存泄露常见场景及解决方案

内存泄露是短生命周期的对象持有了长生命周期的对象没有及时释放导致的,所以 解决内存泄露其实就是打断引用链。下表列举了在日常开发中常见的内存泄露场景和解决方案:

场景问题原因
资源性对象未关闭对于资源性对象不再使用时,应该立即调用它的关闭函数将其关闭,然后再置为 null,例如 Bitmap 等资源,应该在 Activity 销毁时及时关闭
注册对象未注销BroadcastReceiver、EventBus 等未注销,应该在 Activity 销毁时及时注销
类的静态变量持有大数据对象尽量避免使用静态存储数据,不使用时从静态变量移除
单例造成的内存泄露优先使用 Application 的 Context,如需使用 Activity 的 Context,可以在传入 Context 时使用弱引用 WeakReference 封装,使用时通过弱引用获取 Context,获取不到直接 return
非静态内部类的静态实例该实例的生命周期和 app 一样长,这会导致该静态实例一直持有 Activity 引用,Activity 的内存资源不能正常回收。我们可以将该内部类设为静态内部类或将内部类抽取出来封装成一个单例;如果需要使用 Context,尽量使用 Application 的 Context,如果需要使用 Activity 的 Context,要记得用完后置 null 让 GC 可以回收,否则还是会内存泄露
Handler 临时性内存泄露Handler 消息没有及时移除导致内存泄露。解决方案是使用一个静态 Handler 内部类,对 Handler 持有的对象(一般是 Activity)使用弱引用,这样在回收时,也可以回收 Handler 持有的对象;在 Activity 销毁时移除消息队列中的消息
容器中的对象没清理造成的内存泄露在退出程序之前,将集合里的东西 clear,然后置为 null,再退出程序
WebViewWebView 都存在内存泄露的问题,在应用中之遥使用一次 WebView,内存就不会被释放掉。我们可以为 WebView 开启一个独立的进程,使用 aidl 与主进程通信,WebView 所在的进程可以根据业务的需要选择合适的时机销毁,达到正常释放内存的目的。具体方案参考 https://www.jianshu.com/p/b66c225c19e2
系统或第三方造成的泄露不由我们内部控制,在退出时直接进行 hook 或反射打断引用链

常用内存分析工具:LeakCanary、Memory Profiler、MAT

要解决内存问题,就必须要有强大的内存分析工具,让我们更快更方便的定位内存问题。目前主流的内存分析工具主要有 LeakCanary、Memory Profiler、MAT。

LeakCanary

LeakCanary 是 Square 开源的一个内存泄露监控框架,在应用运行时出现的内存泄露会被 LeakCanary 监控记录。

在这里插入图片描述
上图是 LeakCanary 内存泄漏的 trace 分析,主要看 Leaking:NO 到 Leaking:YES 这段的 trace,可以发现 TextView 出现了内存泄漏,因为它持有了被销毁的 Activity 的上下文 Context。

更具体的 trace 分析,具体可以查看官方文档 Fixing a memory leak

使用 LeakCanary 虽然很方便,但是也有一定弊端:

  • 直接引用依赖使用的 LeakCanary 一般用于线下调试,应用发布到线上时需要关闭

  • 应用调试时有时候会引起卡顿

所以 一般使用 LeakCanary 只是一种简便定位内存泄露的方式,但如果需要更好的做内存优化,比如定位内存抖动、Bitmap 优化等还是需要其他的分析工具,主要常用的有 Memory Profiler 和 MAT。

NativeSize、Shallow Size、Retained Size、Depth

后续说明 Memory Profiler 和 MAT 时,会经常出现几个比较重要的指标:Shallow Size 和 Retained Size。在 Memory Profiler 还会提供 Native Size 和 Depth。Google 在 使用 Android Studio Profiler 工具解析应用的内存和 CPU 使用数据 讲解了这几个指标的概念,下面会引用原文说明。java文档 Shallow Size 和 Retained Size 同样做了详细说明。

当您拿到一段 Heap Dump 之后,Memory Profiler 会展示出类的列表。对于每个类,Allocations 这一列显示的是它的实例数量。而在它右边则依次是 Native Size、Shallow Size 和 Retained Size:

在这里插入图片描述
我们用下图来表示某段 Heap Dump 记录的应用内存状态。注意红色的节点,在这个示例中,这个节点所代表的对象从我们的工程中引用了 Native 对象;这种情况不太常见,但在 Android 8.0 之后,使用 Bitmap 便可能产生此类情景,因为 Bitmap 会把像素信息存储在原生内存中来减少 JVM 的内存压力。

先从 Shallow Size 讲起,这列数据其实非常简单,就是 对象本身消耗的内存大小,即为红色节点自身所占内存:

在这里插入图片描述
Native Size 同样也很简单,它是类对象所引用的 Native 对象 (蓝色节点) 所消耗的内存大小:

在这里插入图片描述
Retained Size 稍复杂些,它是下图中所有橙色节点的大小:

在这里插入图片描述
由于一旦删除红色节点,其余的橙色节点都将无法被访问,这时候它们就会被 GC 回收掉。从这个角度上讲,它们是被红色节点所持有的,因此被命名为 Retained Size。

还有一个前面没有提到的数据维度。当您点击某个类名,界面中会显示这个类实例列表,这里有一列新数据 —— Depth

在这里插入图片描述
Depth 是从 GC Root 到达这个实例的最短路径,图中的这些数字就是每个对象的深度 (Depth)。

一个对象离 GC Root 越近,它就越有可能与 GC Root 有多条路径相连,也就越可能在垃圾回收中被保存下来

以红色节点为例,如果从其左边来的任何一个引用被破坏,红色节点就会变成不可访问的状态并且被垃圾回收回收掉。而对于右边的蓝色节点来说,如果您希望它被垃圾回收,那您需要把左右两边的路径都破坏才行。

值得警惕的是,如果您看到某个实例的 Depth 为 1 的话,这意味着它直接被 GC Root 引用,同时也意味着它永远不会被自动回收

下面是一个示例 Activity,它实现了 LocationListener 接口,高亮部分代码 requestLocationUpdates() 将会使用当前 Activity 实例来注册 locationManager。如果您忘记注销,这个 Activity 就会泄漏。它将永远都待在内存里,因为位置管理器是一个 GC Root,而且永远都存在:

在这里插入图片描述
您能在 Memory Profiler 中查看这一情况。点击一个实例,Memory Profiler 将会打开一个面板来显示谁正在引用这个实例:

在这里插入图片描述
我们可以看到位置管理器中的 mListener 正在引用这个 Activity。您可以更进一步,通过引用面板导航至堆的引用视图,它可以让您验证这条引用链是否是您所预期的,也能帮您理解代码中是否有泄露以及哪里有泄露。

Memory Profiler

Memory Profiler 是内置在 Android Studio 适用于查看实时内存情况 的内存分析工具。

Memory Profiler 界面说明

官方文档:使用 Memory Profiler 查看 Java 堆和内存分配

Memory Profiler 查找内存抖动

查找内存抖动还是比较简单的,运行的程序在 Memory Profiler 会呈现为在短时间内内存上下波动频繁触发 GC 回收

内存抖动比较常见的地方:

  • 自定义View的 onMeasure()、onLayout()、onDraw() 直接 new 创建对象

  • 列表比如 RecyclerView 的 onBindViewHolder() 直接 new 创建对象

  • 有循环的代码中创建对象

用一个简单的案例模拟内存抖动:

public class MainActivity extends AppCompatActivity {

    @SuppressWarnings("HandlerLeak")
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            // 模拟内存抖动
            for (int i = 0; i < 100; i++) {
                String[] args = new String[100000];
            }

            mHandler.sendEmptyMessageDelayed(0, 30);
        }
    };

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mHandler.sendEmptyMessage(0);
            }
        });
    }
}

案例非常简单,就是点击按钮时频繁的创建对象。在真机上运行上面的程序也许不会出现锯齿状的内存波动,但是会有非常频繁的 GC 回收,如下图:

在这里插入图片描述
那应该怎么具体的定位到是哪里发生的内存抖动呢?

在这里插入图片描述
按照上面的步骤操作:

  • 位置①:在程序处于运行时,点击 Record 按钮录制内存情况,然后点击 Stop 停止录制,会显示上图

  • 位置②:我们可以点击 Allocations 按降序从大到小或升序从小到大查看分配对象的数量,一般我们会选择降序从大到小看数量最多的对象。上图对象数量最多的是 String 对象

  • 位置③:在 Instance View 随便选择一个 String 对象,会显示下面的 Allocation Call Stack,它会显示这个对象的调用栈位置

  • 位置④: 从 Allocation Call Stack 可以看到,String 对象是在 MainActivity 的第 18 行 handleMessage() 创建的,从而也定位到内存抖动的位置

上面的操作还有一些小技巧:

  • 在位置①操作前,为了排除干扰,一般在录制前会先手动 GC 后再录制变化的内存;在 Android 8.0 以上的设备可以实时的拖动 Memory Profiler 选择查看的内存波动范围

  • 位置②上例是直接查看 Arrange by class,但实际项目中更多的会是选择 Arrange by package 查看自己项目包名下的类

Memory Profiler 查找内存泄露

上面讲到内存泄露的表现是会出现内存抖动,因为出现内存泄露时可用内存不断减少,系统需要内存时获取内存不足就会 GC,所以产生内存抖动。

出现内存泄露时 Memory Profiler 会呈现一个类似阶梯型的内存上升趋势,而且内存没有降下来:

在这里插入图片描述
上图的内存泄漏比较明显,实际项目开发中出现内存泄漏时可能不会特别明显,运行时间比较久才能发现内存是在缓慢上升的。这时候就需要 dump heap 帮助定位。

接下来会使用 Handler 内存泄露的案例简单讲解怎么使用 Memory Profiler 分析内存泄露。

public class HandlerLeakActivity extends AppCompatActivity {
    private static final String TAG = HandlerLeakActivity.class.getSimpleName();

    private Handler handler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            if (msg.what == 0) {
                Log.i(TAG, "handler receive msg");
            }
        }
    };

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        handler.sendEmptyMessageDelayed(0, 10 * 1000);
    }
}

上面代码非常简单,就是启动 app 后,每次进入 HandlerLeakActivity 就使用 Handler 延迟 10s 发送消息,在 10s 内退出界面,不断重复操作。

1、重复多次可能内存泄露的操作,Memory Profiler 堆转储出 hprof 文件(建议在操作前先 GC 排除干扰):
在这里插入图片描述
2、在 Memory Profiler 查看查看堆转储文件 hprof:
在这里插入图片描述
可以发现经过手动 GC 后,Allocations 显示有 5 个 HandlerLeakActivity,堆转储 Instance View 下也仍显示有多个 Activity 实例,说明已经内存泄露,具体的内存泄露定位可以在 Instance View 泄露的实例类对象中点击查看,Instance View 下面的 Reference 会显示具体的引用链。

在新版本的 Memory Profiler 提供了 Activity/Fragment Leaks 复选框,选中它可以直接找到可能内存泄露的位置:

在这里插入图片描述

MAT

MAT 的全称是 Memory Analysis Tool,是对内存进行详细分析的工具,它是 Eclipse 插件,MAT 能帮助我们深入的进行分析并确定内存泄露和内存占用,导入 hprof 堆转储文件具体分析。

MAT 适用于静态内存数据分析,即通常会用两个 hprof 在处理前后对比文件内存信息分析。MAT 经常使用的是 Dominator Tree 或 Histogram。

Android Studio 提供的 Memory Profiler 同样是可以导出 hprof 堆转储文件:
在这里插入图片描述
但是 AS 的堆转储文件并不是标准的,需要将它转换为 MAT 可以识别的标准 hprof 文件,否则 MAT 无法识别打开文件会报错。可以使用 SDK 自带的 hprof-conv 转换,它的路径在 sdk/platform-tools,命令执行:

// 第一个hprof路径是Android Studio堆转储生成的文件路径
// 第二个hprof路径是转换为标准MAT可识别的堆转储文件路径
>>hprof-conv D:\as.hprof D:\mat.hprof

在使用 MAT 打开标准的 hprof 堆转储文件前,建议另外创建一个文件夹存储即将打开的 hprof,因为MAT会在打开的堆转储文件当前目录生成很多临时文件。

使用MAT打开转换后的 hprof 文件:
在这里插入图片描述
饼状图主要用来显示内存的消耗,饼状图的区域代表如下:

  • 彩色区域:代表被分配的内存,点击每个彩色区域可以看到这块区域的内存分配情况

  • 灰色区域:代表空闲内存

其中我们使用 MAT 分析内存主要是 Histogram、Dominator Tree 这两个分析工具,除了圈出来的位置可点击使用这些工具外,还有头部 Action 栏可以打开这些工具:
在这里插入图片描述

Leak Suspects

MAT中对 Leak Suspects 的描述:

includes leak suspects and a system overview

打开 Leak Suspects ,会显示 MAT 根据导入的 hprof 的堆转储文件分析到的可能存在内存泄露的地方:
在这里插入图片描述
可以通过点击 Details 分析更深入的内存泄露原因,但是如果内存泄露不是特别明显,通过 Leak Suspects 是很难发现内存泄露的位置的。一般也很少使用这个工具。所以还需要借助 Dominator Tree 或 Histogram 具体分析。

Dominator Tree

MAT中对 Dominator Tree 的描述:

List the biggest objects and what they keep alive

意思是对象存活原因的展示列表,简单说就是 分析对象的引用关系
在这里插入图片描述
Shallow Heap 和 Retained Heap 在上面已经讲解过,这里再简单说明一下:

  • Shallow Heap:对象自身占用的内存大小

  • Retained Heap:对象自身占用的内存+对象引用的对象所占用的内存

  • Percentage:实例对象占用比例。可以通过它很直观的看出哪些对象占用比例比较大需要关注优化内存,一般 bitmap 占用的比例会比较大

Dominator Tree 是在对象实例的角度上进行分析,更注重引用关系分析

使用 Dominator Tree 可以很清晰的得到一个对象的直接支配对象,如果直接支配对象中出现了不该有的对象,就说明发生了内存泄露。

在 Dominator Tree 的顶部 Regex 可以输入过滤条件,如果是查找 Activity 内存泄露,可以在 Regex 输入 Activity 名称,比如输入 HandlerLeakActivity:
在这里插入图片描述
可以发现列出了多个 HandlerLeakActivity 实例,基本可以断定发生了内存泄露。具体内存泄露的原因可以查看 GC 引用链,在其中一个 HandlerLeakActivity 右键 -> Merge Shortest Paths to GC Root:
在这里插入图片描述
Merge Shortest Paths to GC Root 选项是用来显示距离 GC Root 最短的路径,根据引用类型会有多种选项,比如 with all references 就是显示包含所有的引用,一般我们要分析内存泄露需要排除软引用、弱引用和虚引用,因为这些引用是可以被回收的。选择后 MAT 会给出 HandlerLeakActivity 的GC引用链:
在这里插入图片描述
this$0 就是内部类,如果有分析过内部类字节码应该知道,内部类创建时是会在构造函数中传入一个引用,这个引用就是外部类,这里也就是 HandlerLeakActivity,这也就导致了 HandlerLeakActivity 无法被 GC。

Histogram

MAT中对 Histogram 的描述:

Lists number of instances per class

简单说 Histogram 会罗列出内存中的对象、对象个数和占用内存大小

相比 Dominator Tree 是在对象实例的角度上进行分析,注重引用关系分析,Histogram 则是类的角度上进行分析,注重量的分析
在这里插入图片描述
Shallow Heap 和 Retained Heap 和在 Dominator Tree 的分析一样,这里不再赘述,Class Name 代表类名,Objects 代表对象实例个数。

Histogram 同样支持 Regex 查找,输入 HandlerLeakActivity:
在这里插入图片描述
可以发现 HandlerLeakActivity 有5个实例,可以基本断定发生了内存泄露。具体内存泄露原因同样可以查看 GC 引用链。这里有两个概念需要弄清楚:

  • with outgoing references:它引用了哪些对象

  • with incoming references:哪些对象引用了它

因为我们要知道哪些对象还持有着 HandlerLeakActivity 导致它内存泄露,所以会选择 with incoming references。在 HandlerLeakActivity 右键-> List Objects -> with incoming references:
在这里插入图片描述
然后选择的其中一个 HandlerLeakActivity 右键 -> Path To GC Roots -> exclude all phantom/weak/soft etc.references:
在这里插入图片描述
在这里插入图片描述
图上有一个细节需要注意,可以看到 <java Local> java.lang.Thread 的文件标了一个黄色的小圆圈,它就是引起内存泄露的节点。

上面的内存泄露原因是消息 Message 有一个变量 target 持有了 Activity 的引用,Message 被 MessageQueue 持有,而 MessageQueue 又被主线程的 Looper 持有,其中 Looper 是静态的,主线程会跟随应用退出才退出,所以就出现了内存泄露 Activity 的引用被 Looper 持有无法被回收。

在 Dominator Tree 操作分析内存泄露的方式同样适用于 Histogram ,同样的 Histogram 这里的操作分析方式也适用于 Dominator Tree。

OQL

OQL 全称为 Object Query Language,类似于 SQL 能够查询内存中满足指定条件的所有对象。查询格式如下:

SELECT * FROM [ INSTANCEOF ]	<class_name> [ WHERE <filter-expression>]

比如我们想查找 hprof 文件中 Activity 对象:
在这里插入图片描述
输入完后按下 F5 执行或者 Action 工具栏的红色叹号,就会显示所有 Activity 对象。如果想查找具体的 Activity 或其他对象也可以使用 OQL 语句查询或者 Regex 筛选,然后查找 GC 引用链分析:
在这里插入图片描述
OQL 用法查看 OQL 官方文档

对比 hprof 文件

MAT 提供了对比两个 hprof 文件的强大功能,这也是我们使用 MAT 最常用的一种分析方式。在场景较为复杂的情况就需要通过对比 hprof 文件来分析,也很方便对比内存优化前后效果或对比内存数据。

我们先将存在内存泄露的代码解决,然后从 Memory Profiler 操作后堆转储导出 hprof 文件:

public class HandlerLeakActivity extends AppCompatActivity {
    private static final String TAG = "LeakActivity";

    private MyHandler handler;

    private static class MyHandler extends Handler {
        private WeakReference<HandlerLeakActivity> context;

        MyHandler(HandlerLeakActivity activity) {
            context = new WeakReference<>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            if (msg.what == 0) {
                Log.i(TAG, "handler receive msg");
            }
        }
    }

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        handler = new MyHandler(this);
        handler.sendEmptyMessageDelayed(0, 10 * 1000);
    }

    @Override
    protected void onDestroy() {
        handler.removeCallbacksAndMessages(null);
        super.onDestroy();
    }
}

我们先打开未解决内存泄露前的 hprof 文件,然后先打开 Navigation History:
在这里插入图片描述
打开 Histogram,将操作添加到 Compare Basket:
在这里插入图片描述
打开解决了内存泄露之后的 hprof 文件,按照上面的操作也添加到 Compare Basket,然后对比结果:
在这里插入图片描述
在这里插入图片描述
其中 Objects #0 代表的是未解决内存泄露的 hprof 文件对象实例个数,Objects #1 是解决了内存泄露的 hprof 文件对象实例个数。展示对比结果是按 hprof 添加进 Compare Basket 显示的,如果打开顺序相反就是下面的展示:
在这里插入图片描述
可以发现 HandlerLeakActivity 内存泄露问题已经解决,Activity 已经正常被 GC。

Dominator Tree 和 Histogram 使用小技巧

上面我们讲解使用 Dominator Tree 和 Histogram 分析内存泄露时,都是在已知条件直接搜索 HandlerLeakActivity,但是实际项目中很多时候我们在 Memory Profiler 看到了内存可能存在异常,却并不知道可能哪里会出现内存泄露或哪些对象内存占用过大问题,所以我们在目的不明确情况下为了分析应用,可以使用 Regex 筛选应用包名来查找出可疑的对象:
在这里插入图片描述
或者在 Action 栏根据包名分组查找,MAT 默认是使用 Group by class:
在这里插入图片描述

ListObjects 和 Show Object by class

在使用 MAT 时经常会需要针对某个可疑对象具体分析,上面在使用 Dominator Tree 和 Histogram 时有些常用概念可能还说得不够详细,这里再补充一下。

在这里插入图片描述

  • List Objects

    • List Objects -> with incoming references:哪些对象引用了它

    • List Objects -> with outgoing references:它引用了哪些对象

  • Show objects by class(以class方式查找)

    • Show objects by class -> by incoming references:哪些对象引用了它

    • Show objects by class -> by outgoing references:它引用了哪些对象

  • Path To GC Roots -> exclude xxx references:查看这个对象的GC引用链,排除软引用、弱引用、虚引用,剩下的就是强引用。除了强引用外,其他引用JVM是可以GC的,如果一个对象始终无法被GC,那么说明它是强引用导致GC无法回收

  • Merge Shortest Paths to GC Roots:找到从GC Root到一个对象或一组对象的最短路径

  • Show Retained Set:查找该对象被回收时将被GC回收的对象集合

其他内存分析工具

除了上面讲解的 LeakCanary、Memory Profiler 和 MAT 三种最常见的内存分析工具外,如果需要快速只想查看下内存信息,adb 也为我们内置了查看内存的命令;还有和 MAT 相似的分析工具 JHat。

adb shell cat /proc/meminfo

Android 设备的内存信息比如 RAM大小是在 /proc/meminfo,可以通过 adb 命令查看:

adb shell cat /proc/meminfo

具体参数参考链接:Android 获取内存(RAM)大小信息

adb shell dumpsys meminfo

通过命令 adb shell dumpsys meminfo 就可以查看对应进程的内存信息:

// 启动命令时需要应用处于运行状态
adb shell dumpsys meminfo [pid] // [pid]为进程id
或
adb shell dumpsys meminfo --package [packageName] // [packageName]指定查看的应用包名进程,可能有多个进程

展示的内存信息着重关注四个要点。

Heap Alloc

在这里插入图片描述
主要看 Native Heap 和 Dalvik Heap 的 Heap Alloc:

  • Heap Alloc[Native Heap]:表示native的内存占用,如果持续上升则有可能内存泄露

  • Heap Alloc[Dalvik Heap]:表示java层的内存占用

Views、Activities、AppContexts 的数量

在这里插入图片描述
一般可以在操作 app 时多次的进入退出界面,如果 Views、Activities、AppContexts 数量持续上升没有减少,则说明有内存泄露的风险。

SQL 的 MEMORY_USED 和 PAGECACHE_OVERFLOW

在这里插入图片描述

  • MEMORY_USED:表示数据库使用的内存

  • PAGECACHE_OVERFLOW:表示溢出也使用的缓存,这个数值越小越好

DATABASE 信息

在这里插入图片描述

  • pgsz:表示数据库分页大小,这里全是4KB

  • Lookaside(b):表示使用了多少个 Lookaside 的slots,可理解为内存占用的大小

  • cache:以第一个cache为例子,27/20/6 分别表示 分页缓存命中次数/未命中次数/分页缓存个数,未命中次数不应该大于命中次数

JHat

JHat 是 Oracle 推出的一款 hprof 分析软件,它和 MAT 并称为内存静态分析利器。不同于 MAT 的单人界面式分析,JHat 使用多人界面式分析(简单理解也就是可以通过 web 前端连接 server 查看)。它内置在 jdk 中,可以输入命令 jhat 查看是否存在:
在这里插入图片描述
上面的输出说明 JHat 已经内置。

我们要使用 JHat 分析 hprof,只需要输入命令 jhat xxx.hprof:

在这里插入图片描述
如果输出上面的描述说明 Server 已经启动成功,可以在浏览器访问地址 127.0.0.1:7000 查看 hprof 文件:

在这里插入图片描述
访问地址后会展示根据包名分组类对象,如果我们只关注自己的应用,在浏览器 F4 搜索应用包名或类名:
在这里插入图片描述
找到我们需要关注的类对象,点进链接就能查看相关的内存信息。

JHat 和 MAT 一样,同样也具有 Histogram 和 OQL。

JHat Histogram

打开 Histogram 访问地址 127.0.0.1:7000/histo/:

在这里插入图片描述
可以看到它是按 TotalSize 和 Instance Count 降序排序 Class,关注自己的应用同样也是搜索包名或类名:
在这里插入图片描述
Instance Count 实例数有5个说明已经内存泄露了,具体的可以点进链接排除弱引用查看引用链。

JHat OQL

打开 Histogram 访问地址 127.0.0.1:7000/oql/:

在这里插入图片描述
在使用上和 MAT 的 OQL 差不多,同样是使用类似 SQL 语句查询,如果要具体了解怎么使用,可以点即 OQL Help 查看使用文档。

合理使用及释放内存

内存优化其实覆盖非常广泛,大到图片合理使用、内存泄露处理、内存溢出处理等,小到代码中使用的数据结构、是否使用 for-each、基本数据类型拆装箱等都会影响到内存。那怎么合理的使用内存?

合理使用数据结构

合理使用数据结构分配对象是作为开发人员在工作开发过程中需要时刻留意的细节,根据不同的场景合理的使用不同的数据结构,比如使用 ArrayMap、SpareArray 而不是 HashMap 等,自定义 View 时不要在 onMeasure()、onLayout()、onDraw() 直接创建对象等,具体可以参考文章:Android 性能优化系列:如何合理使用内存

onLowMemory 和 onTrimMemory

onLowMemory() 会在当系统内存不足时,所有后台程序(优先级为 background 的进程,不是指后台运行的进程)都被杀死时会回调它。它会回调给实现了 ComponentCallbacks 的类,比如 Application、Activity、Service、ContentProvider 都实现了该接口,我们也可以自己实现这个接口监听回调:

ComponentCallbacks componentCallbacks = new CustomComponentCallbacks();
Context.registerComponentCallbacks(componentCallbacks);
Context.unregisterComponentCallbacks(componentCallbacks);

public class CustomComponentCallbacks implementation ComponentCallbacks {
	@Override
	public void onConfigurationChanged(Configuration newConfig) {
	}

	@Override
	public void onLowMemory() {
		// release cacche or other unnecessary resources
	}
}

可以在 onLowMemory() 释放我们的缓存和一些不需要的资源,因为当该方法被回调后系统将会 GC 处理回收。

一般情况下我们还应该同时实现 onTrimMemory(int level),该方法会传递系统不同的状态 level,我们可以根据相应状态处理内存和资源的释放。也可以自己实现 ComponentCallbacks2 接口,ComponentCallback2 实现了 ComponentCallback,所以 Application、Activity、Service、ContentProvider 也可以重写到该方法:

ComponentCallbacks componentCallbacks = new CustomComponentCallbacks();
Context.registerComponentCallbacks(componentCallbacks);
Context.unregisterComponentCallbacks(componentCallbacks);

public class CustomComponentCallbacks implementation ComponentCallbacks2 {
	@Override
	public void onTrimMemory(int level) {
	}
}

onTrimMemory(int level) 有多个级别状态:

  • TRIM_MEMORY_UI_HIDDEN:应用程序的所有UI界面被隐藏了,比如用户点击了 Home 或 Back,可以在该级别将占用比较大的资源和对象数量释放让内存能更好管理,该级别比较常用

  • TRIM_MEMORY_RUNNING_CRITICAL:应用程序运行正常,但系统已经根据 LRU 缓存规则杀掉了大部分缓存的进程了,当前进程应该释放任何不必要的资源,否则系统可能会继续杀掉所有缓存中的进程,并且开始杀掉一些本来应当保持运行的进程,比如后台运行的服务

  • TRIM_MEMORY_RUNNING_MODERATE:应用程序运行正常且必会被杀掉,但设备内存已经有点低,系统会根据 LRU 缓存规则去杀死进程

  • TRIM_MEMORY_RUNNING_LOW:应用程序运行正常并且不会被杀掉,但设备内存已经非常低,应该去释放一些不必要的资源提升系统性能,同时这页会影响到我们应用程序性能

应用程序是缓存的,则会收到以下几种级别:

  • TRIM_MEMORY_BACKGROUND:目前内存已经很低,系统准备开始根据 LRU 缓存来清理进程。当前程序处于 LRU 缓存列表的最近位置是不太可能被清理掉的,但如果能释放一些比较容易恢复的资源能让设备内存变得更充足,从而让我们的程序更长时间地保留在缓存中,用户返回我们的程序就会比较快而不是重新经历一次启动

  • TRIM_MEMORY_MODERATE:应用程序已经处于 LRU 列表的中间位置,如果当前程序能释放资源,程序有被系统杀掉的风险

  • TRIM_MEMORY_COMPLETE:应用程序已经处于 LRU 列表的末尾,如果没能找到更多的内存系统将会杀死该进程

我们在应用监听接收到 onTrimMemory() 或 onLowMemory() 的回调时,可以及时的清理释放一些内存,避免 Low Memory Killer 将我们的进程杀死。比如在图片库 Glide 就提供了对应的方法:

@Override
public vodi onTrimMemory(int level) {
	super.onTrimMemory(level);
	// Activity未销毁时收到回调清理内存
	if (!isDestroyed()) {
		GlideApp.get(this).onTrimMemory(level);
	}
}

使用 Glide 合理设置内存参数

在日常项目开发中经常会用到图片加载框架,比如 Glide、Picasso、Fresco 等,如果使用 kotlin 可能还会使用 coil。接下来会简单的使用 Glide 讲解在项目中如何合理处理内存。

在 4.x 版本的 Glide 会将内存相关的配置抽离到 AppGlideModule,在 Glide 启动时会应用我们所设置的参数配置。

@GlideModule
public class MyGlideModule extends AppGlideModule {
	@Override
	public boolean isManifestParsingEnabled() {
		return false;
	}

	@Override
	public void applyOptions(@NonNull Context context, @NonNull GlideBuilde builder) {
		// ... 设置项目所需的内存配置
	}
}

applyOptions() 在参数中提供了 GlideBuilder 类,可以用它配置图片加载相关的配置,例如图片颜色策略、图片磁盘缓存的编码质量、图片在磁盘的缓存大小、加载图片的线程数量、bitmap 池的大小等等。具体的配置还需要根据项目场景合理配置,举例如下:

@GlideModule
public class MyGlideModule extends AppGlideModule {
	private static final int DEFAULT_DISK_CACHE_SIZE = 20 * 1024 * 1024;
	private static final int LOW_DISK_CACHE_SIZE = 5 * 1024 * 1024;

	@Override
	public boolean isManifestParsingEnabled() {
		return false;
	}

	@Override
	public void applyOptions(@NonNull Context context, @NonNull GlideBuilde builder) {
		// 默认使用 ARGB_8888,改为RGB_565
		builder.setDefaultRequestOptions(new RequestOptions().format(DecodeFormat.PREFER_RGB_565)
                .set(Downsampler.ALLOW_HARDWARE_CONFIG, true)
                .encodeQuality(70)
                .timeout(25000));
        // 设置磁盘缓存大小
        builder.setDiskCache(new InternalCacheDiskCacheFactory(context, DiskCache.Factory.DEFAULT_DISK_CACHE_DIR, DEFAULT_DISK_CACHE_SIZE));
        // 设置图片异步加载的线程数量为2个
        builder.setSourceExecutors(GlideExecutor.newSourceExecutor(
        		2, 
        		"source", 
        		GlideExecutor.UncaughtThrowableStrategy.DEFAULT));
		// 设置缓存大小,设置为原始缓存的一半
		MemorySizeCalculator calculator = new MemorySizeCalculator.Builder(context).build();
            builder.setMemoryCache(new LruResourceCache(calculator.getMemoryCacheSize() / 2));
            builder.setBitmapPool(new LruBitmapPool(calculator.getBitmapPoolSize() / 2));
            builder.setArrayPool(new LruArrayPool(calculator.getBitmapPoolSize() / 2));
	}
}

Glide 除了可以很方便的配置参数外,还提供了释放内存的方法,我们应该在合适的时机调用它们释放内存。例如在 onLowMemory()、onTrimMemory() 和 RecyclerView 的 onViewRecycled():

@Override
public void onTrimMemory(int level) {
	super.onTrimMemory(level);
	if (!isDestroyed() && level != TRIM_MEMORY_UI_HIDDEN)) {
		GlideApp.get(this).onTrimMemory(level);
	}
}

@Override
public void onLowMemory() {
	super.onLowMemory();
	GlideApp.get(this).onLowMemory();
}

// RecyclerView 
@Override
public void onViewRecycled(@NonNull BaseViewHolder holder) {
	ImageView imageView = holder.getView(R.id.image);
	// 需要判断 context 是否有效,避免抛出异常
	if (imageView != null && isContextValid(mContext)) {
		GlideApp.with(imageView).clear(imageView);
	}
}

private static boolean isContextValid(Context context) {
	if (context == null) return false;

	Activity activity = findActivity(context);
	if (activity == null) return false;

	return !activity.isDestroyed() && !activity.isFinishing();
}

private static Activity findActivity(Context context) {
	if (context instanceof Activity) {
		return (Activity) context;
	} else if (context instanceof ContextWrapper) {
		return findActivity(((ContextWrapper) context).getBaseContext());
	} else {
		return null;
	}
}

总结

内存优化说到底主要就是解决两个问题:内存抖动和内存泄露

  • 内存抖动:主要是要知道原理,是由于频繁创建回收对象造成的,找到这个位置并修复

  • 内存泄露:排查常见的泄露场景打断引用链解决

  • 8
    点赞
  • 63
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
Android系统性能优化对于解决卡顿、提升稳定性和优化续航方面起着重要的作用。 首先,在解决卡顿问题上,开发人员需要关注应用程序的UI线程。为了确保应用程序的流畅运行,可以采用以下优化措施:优化布局文件,减少层级嵌套;使用异步加载图片,避免在主线程中进行网络请求等耗时操作;合理利用缓存机制,避免重复加载数据。此外,还可以针对卡顿问题进行性能分析,通过工具查找耗时操作,并进行相应的优化。 其次,在提高系统稳定性方面,开发人员需要考虑异常崩溃的处理和内存管理。异常崩溃处理可通过捕获并记录崩溃异常来及时解决问题和改进代码。内存管理方面,应避免内存泄漏和过度分配内存,使用系统提供的工具来进行内存管理和优化。 最后,在续航优化上,需要考虑电源管理和资源使用的合理分配。通过使用省电模式、灵活控制后台任务和限制应用程序在后台运行等方式,最大程度地延长设备的电池寿命。另外,合理管理资源,避免过度使用CPU、网络和图形渲染等资源,有助于降低能耗并优化系统续航。 总之,Android系统性能优化是一个综合性的工作,需要开发人员关注卡顿问题、提升稳定性和优化续航方面的问题。通过合理使用工具和采取相应的优化措施,可以实现系统性能的有效提升。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值