项目经验,如需转载,请注明作者: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:内存优化建议