Android 内存泄露详解

项目经验,如需转载,请注明作者:Yuloran (t.cn/EGU6c76)

前言

内存泄露说简单也简单,说复杂也复杂。简单是因为我们有很多工具,比如 Android Studio Profiler、MAT 等,对内存泄露进行定位。复杂是因为我们需要了解很多其它知识,比如 Android 虚拟机(Dalvik 或者 ART)的自动垃圾回收机制、Android 平台应用内存占用分析、Android Context 原理等,以看懂这些工具提供的数据。否则,无法进行下一步的内存泄露分析,或者只知其然,而不知其所以然。

所以,强烈推荐先阅读笔者的前三篇文章后,再阅读本文:

  • 《Java 虚拟机内存模型及 GC 算法详解》
  • 《Android 虚拟机 Vs Java 虚拟机》
  • 《分析并优化 Android 应用内存占用》

如果你已经对上述内容非常熟悉或有所了解,也可以直接阅读本文。

至于 Android Context 原理,笔者会就 Android 9.0(Pie,API28)源码进行简要分析。

如有错误,欢迎指正。

内存泄露概念

我们知道在 C 语言中,内存是需要开发人员自己管理的,使用 malloc() 分配内存,free() 释放内存。如果没有调用 free() 进行释放,将导致内存泄露。而 Java 虚拟机实现了自动内存管理机制,将开发人员从手动管理中解放出来。那么为什么也会存在“内存泄露”呢?其实,这里的“内存泄露”指的是变量的存活时间超过了其本身的生命周期,说的糙一点,就是“该死的时候没死”。在物理内存有限的移动设备上,每个 Java 进程都有一个内存使用阈值,超过这个阈值时,虚拟机将会抛出 OutOfMemoryError。而持续的内存泄露,很可能会导致 OutOfMemoryError,这也是我们为什么要修复内存泄露的原因。

单个应用堆内存上限

查看 Android 设备上单个应用的 Java 堆内存使用上限:

java 代码:

ActivityManager am = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
Logger.debug("JavaHeap", "dalvik.vm.heapgrowthlimit: %dM", am.getMemoryClass());
Logger.debug("JavaHeap", "dalvik.vm.heapsize: %dM", am.getLargeMemoryClass());
复制代码

Logger 是笔者封装的一个日志打印工具类:Logger.java

shell 命令:

adb shell getprop dalvik.vm.heapgrowthlimit
adb shell getprop dalvik.vm.heapsize
复制代码

分别对应:

  • 普通应用 Java 堆使用上限:对应 /system/build.prop 中 "dalvik.vm.heapgrowthlimit"
  • 大应用 Java 堆使用上限:需要在 Manifest application 标签中设置 android:largeHeap="true",对应 /system/build.prop 中 "dalvik.vm.heapsize"

示例(小米6,Android 8.0,MIUI 10 8.12.13):

2018-12-31 17:13:39.335 4142-4142/? D/WanAndroid: JavaHeap: dalvik.vm.heapgrowthlimit: 256M
2018-12-31 17:13:39.335 4142-4142/? D/WanAndroid: JavaHeap: dalvik.vm.heapsize: 512M
复制代码

查看内存信息

上一篇文章说过,我们可以使用:

adb shell dumpsys meminfo [-s|-d|-a] 
复制代码

[] 表示命令选项,<> 表示命令参数。

选项 作用
-s 输出概要内存信息
-d 输出详细内存信息
-a 输出全部内存信息

示例(小米6,Android 8.0,MIUI 10 8.12.13)

D:\Android\projects\wanandroid_java>adb shell dumpsys meminfo -s com.yuloran.wanandroid_java
Applications Memory Usage (in Kilobytes):
Uptime: 563701889 Realtime: 1391868153

** MEMINFO in pid 5897 [com.yuloran.wanandroid_java] **

 App Summary
                       Pss(KB)
                        ------
           Java Heap:     9384
         Native Heap:    15636
                Code:    28680
               Stack:      120
            Graphics:     5688
       Private Other:     8020
              System:     3788

               TOTAL:    71316       TOTAL SWAP PSS:       44

 Objects
               Views:        0         ViewRootImpl:        0
         AppContexts:        2           Activities:        0
              Assets:       18        AssetManagers:        2
       Local Binders:       24        Proxy Binders:       21
       Parcel memory:        8         Parcel count:       34
    Death Recipients:        3      OpenSSL Sockets:        0
            WebViews:        0
复制代码

以上显示的是 PSS(不了解 PSS的,请阅读笔者的上篇文章)数据,单位是 KB。应用来自笔者的 JetPack 实践项目 wanandroid_java。

此处我们主要关注 Activities,这是定位应用是否存在内存泄露的切入点。因为 Activity 作为应用与用户的交互入口,绝大多数的内存泄露,最终都会反应到 Activity 上。正常情况下,打开一个 Activity,Activities 计数+1,退出一个 Activity,Activities 计数-1,退出应用,Activities 计数=0。 如果不是,说明你的应用存在内存泄露。显然,上图所示的数据,Activities 为 0,不存在内存泄露。

至于 AppContexts,表示的是进程中仍然存活的 Context 对象,相较于 Activities 重要性不高。我们知道 Application、Activity、Service 都是 Context 对象,所以这些对象的数量,都会纳入 AppContexts 计数。而 ContentProvider、BroadcastReceiver 的 Context 是外部传进去的,所以不会对 AppContexts 的计数结果产生影响。一般情况下,Application 的数量为 1,除非应用开了多个进程(一个 Java 进程对应一个虚拟机,所以自然也对应一个新 Application),笔者的应用并没有使用多进程,那么为什么 AppContexts 数量为 2 呢?使用 Android Studio Profiler 调试一下:

Instance View 窗口可以查看该对象的内部引用,Reference 窗口可以查看该对象被哪些外部对象引用。我们分别查看下这两个 Context 对象正被哪些对象引用:

如上图所示,该对象被 MyApplication 的成员变量 mBase 引用,没有问题。

如上图所示,该对象被 ActivityThread 的成员变量 mSystemContext 引用,也没有问题。

Context 源码简析

既然上面提到了 Context,此处简单分析下其子类 Application、Activity、Service 的创建源码。由于 Application 一般在创建 Activity 时顺带创建,所以直接从 Activity 的创建源码切入,源码基于 SDK Android 9.0(Pie,API28)。

Activity 创建源码

-> ActivityThread::performLaunchActivity()

    private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
        // 创建一个适用于 Activity 的 ContextImpl 对象,此处变量名改为 activityContext 更合适
        // ContextImpl extends Context,所以也是 Context 的子类
        ContextImpl appContext = createBaseContextForActivity(r);
        Activity activity = null;
        try {
            // 反射创建 Activity 对象
            java.lang.ClassLoader cl = appContext.getClassLoader();
            activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);
        } catch (Exception e) {
        }

        try {
            // 创建 Application 对象
            Application app = r.packageInfo.makeApplication(false, mInstrumentation);
            if (activity != null) {
                // 将 activity 赋给 ContextImpl::mOuterContext 对象,可以看出 mOuterContext 代表的是实际组件对象,
                // 比如具体的 Application、Activity、Service 对象,此处为 Activity 对象
                appContext.setOuterContext(activity);
                // 将 ContextImpl 类型的 appContext 对象赋给 ContextWrapper::mBase 对象,而大多数时候,实际干活的
                // 也正是这个对象
                activity.attach(appContext, this, getInstrumentation(), r.token,
                        r.ident, app, r.intent, r.activityInfo, title, r.parent,
                        r.embeddedID, r.lastNonConfigurationInstances, config,
                        r.referrer, r.voiceInteractor, window, r.configCallback);
                // 设置主题
                int theme = r.activityInfo.getThemeResource();
                if (theme != 0) {
                    activity.setTheme(theme);
                }
                // 通过 Instrumentation 类型的 mInstrumentation 对象,间接调用 onCreate()
                // 注意 mInstrumentation 对象,这是代理模式的一个应用,用于托管直接创建系统组件或调用
                // 系统组件功能,用来监测和协助运行 Android 应用。
                if (r.isPersistable()) {
                    mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
                } else {
                    mInstrumentation.callActivityOnCreate(activity, r.state);
                }
            }
        } catch (SuperNotCalledException e) {
        } catch (Exception e) {
        }
        return activity;
    }
复制代码

-> LoadedApk::makeApplication()

    public Application makeApplication(boolean forceDefaultAppClass, Instrumentation instrumentation) {
        Application app = null;
        try {
            java.lang.ClassLoader cl = getClassLoader();
            // 创建一个适用于 Application 的 ContextImpl 对象
            ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
            // 创建 Application 对象
            app = mActivityThread.mInstrumentation.newApplication(cl, appClass, appContext);
            // 将 app 赋给 ContextImpl::mOuterContext 对象,可以看出 mOuterContext 代表的是实际组件对象,
            // 比如具体的 Application、Activity、Service 对象,此处为 Application 对象
            appContext.setOuterContext(app);
        } catch (Exception e) {
        }
        return app;
    }
复制代码

-> Instrumentation::newApplication()

    public Application newApplication(ClassLoader cl, String className, Context context)
            throws InstantiationException, IllegalAccessException, ClassNotFoundException {
        Application app = getFactory(context.getPackageName()).instantiateApplication(cl, className);
        // 将 ContextImpl 类型的 context 对象赋给 ContextWrapper::mBase 对象,而大多数时候,实际干活的
        // 也正是这个对象
        app.attach(context);
        return app;
    }
复制代码

Service 创建源码

-> ActivityThread::handleCreateService()

    private void handleCreateService(CreateServiceData data) {
        LoadedApk packageInfo = getPackageInfoNoCheck(
                data.info.applicationInfo, data.compatInfo);
        Service service = null;
        try {
            // 创建 Service 对象
            java.lang.ClassLoader cl = packageInfo.getClassLoader();
            service = packageInfo.getAppFactory().instantiateService(cl, data.info.name, data.intent);
        } catch (Exception e) {
        }

        try {
            // 创建一个适用于 Application 的 ContextImpl 对象
            ContextImpl context = ContextImpl.createAppContext(this, packageInfo);
            // 将 service 赋给 ContextImpl::mOuterContext 对象,可以看出 mOuterContext 代表的是实际组件对象,
            // 比如具体的 Application、Activity、Service 对象,此处为 Service 对象
            context.setOuterContext(service);
            // 检查创建 Application 对象,只会创建一次
            Application app = packageInfo.makeApplication(false, mInstrumentation);
            // 将 ContextImpl 类型的 context 对象赋给 ContextWrapper::mBase 对象,而大多数时候,实际干活的
            // 也正是这个对象
            service.attach(context, this, data.info.name, data.token, app, ActivityManager.getService());
            // 调用 onCreate()
            service.onCreate();
        } catch (Exception e) {
        }
    }
复制代码

以上代码已经过笔者精简,仅保留了与本文主题相关部分。由以上源码可知,ContextWrapper::mBase 对象的具体类型为 ContextImpl,实际干活的也正是它。至于 Context 怎么理解,读者可以理解为类似于环境变量或者机器猫的百变口袋,可以用来获取各种系统资源。Android 中大多数的内存泄露也正是 Context 泄露,比如 View 和 Drawable 对象会保持对其源 Activity 的引用,因此保持 View 或 Drawable 对象,就可能导致 Activity 泄露。

内存泄露案例

本来想找个大厂 App 测试下有没有内存泄露,因为笔者以前曾经集成过他们的 SDk,那个时候是存在内存泄露的。刚才试了一下,额,退出后连进程都没有了?...不禁想起笔者读大学时,那个时候 Android 应用上线,有的连混淆都不做,强如 QQ 都被改的面目全非,各种杀马特皮肤。笔者也反编译修改过 Miui 的设置中心,剔除了自启管理和病毒扫描功能。而今天,各种混淆加密加固,再也无法胡作非为了?。而内存泄露这种低级问题,大厂的 App 自然也很少再出现了。

扯远了,没办法,手写一个内存泄露吧:

    @Override
    protected void onStart()
    {
        super.onStart();

        Single.fromCallable(new Callable()
        {
            @Override
            public SectionResp call() throws Exception
            {
                Thread.sleep(TimeUnit.SECONDS.toMillis(20));
                return new SectionResp();
            }
        }).subscribeOn(Schedulers.newThread()).subscribe(new Consumer()
        {
            @Override
            public void accept(SectionResp sectionResp) throws Exception
            {
                Logger.debug("RxThread", "thread stopped.");
            }
        });
    }
复制代码

上述代码,使用 RxJava 模拟弱网请求响应过慢的场景,不是很严重的内存泄露,因为我们一般都会设置请求超时时间,届时线程自会停止,泄露也随之消失。不过这并不重要,因为无论什么形式的泄露,定位方式是相同的。

首先,退出应用,然后 dumpsys meminfo,快速查看是否存在 Activity 泄露:

D:\Android\projects\wanandroid_java>adb shell dumpsys meminfo -s com.yuloran.wanandroid_java
Applications Memory Usage (in Kilobytes):
Uptime: 578084002 Realtime: 1406250267

** MEMINFO in pid 30243 [com.yuloran.wanandroid_java] **

 App Summary
                       Pss(KB)
                        ------
           Java Heap:    10692
         Native Heap:    27740
                Code:    31264
               Stack:      120
            Graphics:     5816
       Private Other:     8792
              System:    12896

               TOTAL:    97320       TOTAL SWAP PSS:       28

 Objects
               Views:      227         ViewRootImpl:        1
         AppContexts:        3           Activities:        1
              Assets:       18        AssetManagers:        3
       Local Binders:       26        Proxy Binders:       27
       Parcel memory:       10         Parcel count:       42
    Death Recipients:        3      OpenSSL Sockets:        0
            WebViews:        0
复制代码

显然,存在 Activity 泄露,因为 Activity 内通常都会持有其它引用,进而导致大量资源无法及时释放。

接下来,需要分析具体的内存泄露原因,有两种方法:Android Studio Profiler 和 MAT。

使用 Android Studio Profiler 分析

显然,onStart() 方法中的匿名内部类 new Callable(){} 导致了 MainActivity 泄露。

使用 MAT 分析

说实话,Android Studio 的性能调优工具发展到现在,从最初的 DeviceMonitor 到现在的 Profiler,确实是越来越好用了,想想也就是几年的时间。在以前自带工具不好用的时候,都是先把内存 dump 为 xxx.hprof,再转为 MAT 可读格式,使用 MAT 进行分析。

下载与安装

下载地址

下载 MAT 独立版,无需安装,解压即用。

导出为 *.hprof

将应用 Java 堆内存导出为 *.hprof(heap profile):

也可以使用 adb shell am dumpheap 命令来导出:

adb shell am dumpheap com.yuloran.wanandroid_java /data/local/tmp/temp.hprof
adb pull /data/local/tmp/temp.hprof temp.hprof
复制代码

格式转换

使用 hprof-conv.exe 将上面导出的文件转为 MAT 可读的文件,后缀名一样:

hprof-conv.exe .\memory-20181231T225508.hprof .\memory-20181231T225508_mat.hprof
复制代码

hprof-conv.exe 位于 platform-tools 目录下:

为方便使用,笔者将 platform-tools 添加到了系统环境变量中,所以在上面的 Windows Bat 命令中,可以直接使用此程序。

MAT 分析

打开上面转换后的文件 memory-20181231T225508_mat.hprof:

点击直方图:

输入 .*Activity.* 进行过滤:

右击选择 with outgoing references:

右击选择“排除所有虚/弱/软引用”:

查看 MainActivity 泄露原因:

显然,onStart() 方法中的匿名内部类 new Callable(){} 导致了 MainActivity 泄露。

结语

在公开平台写作与个人私下使用是两种完全不同的感受,需要更为严谨的态度以及多方论证。所以笔者的文章大多来自官方文档或源码分析,然而毕竟水平有限,如有错误,还望不吝赐教?。

Google Android Developers 官网有关内存调优的文章:

  • Overview of memory management:包括垃圾回收、共享内存、内存分配与回收、内存限制、快速切换机制
  • Investigating Your RAM Usage:包括 GC 日志分析、内存分配追踪、hprof 分析、dumpsys meminfo 数据详解、如何触发内存泄露
  • Manage your app's memory:内存优化建议

你可能感兴趣的:(Android 内存泄露详解)