公司开发的自动化测试工具发现有内存泄露。
导出当时的.hprof文件(红框),导入到MAT中,点击Historian,然后搜索关键字“Activity",过滤出了如下一些数据:
大概扫了一眼,只有三个我们自己实现的类,其他的都是系统api的类,而数量为1的类也就只有com.android.browser.BrowserActivity 这个activity本身以及 com.android.browser.BrowserActivity$1,$1是其中的内部类,这个是被混淆后的。 那个数量为3的BrowserActivity$b暂时不去理会。
先看下BrowserActivity这个对象的引用栈,右键选择Merge Shortest paths to GC Roots,选择所有的引用
然后就出现了这个结果:
最终指向了一个变量e(混淆后的代号),它的类型就是BrowserActivity,那么具体这个e在代码中是谁呢?通过导出当时编译apk时保留的mappings文件,搜索BrowserActivity关键字
可以看到,在代码中就是变量mIns。查找这个变量被使用地方:
这个类中定义了一个静态方法用来获取BrowserActivity实例,在activity的onCreate中赋值,在onDestroy中置空。按理说,当activity销毁的时候,这个变量就指向了一个空地址了,getInstance这个静态方法应该不会再持有activity实例了吧? 这个暂时没搞明白,分析不下去了。
换个方向来分析下那个内部类BrowserActivity$1代表的是什么?同样的,找到引用链:
可以看到,这个内部类被一个FullScreenController引用了。
打开源码看看:
controller对象是个静态变量,生命周期很长,会持有一个FullScreenListener类型的变量。FullScreenListener这个类是定义在BrowserActivity中的匿名内部类。
我们知道匿名内部类会持有外部类的引用,也就是会持有activity,这个内部类创建的对象又被一个单例持有,这就可能存在泄露了。解决办法就是改成静态对象了。
最后,通过两处activity引用的分析,大概理解了一下上面mIns被置为空后,为什么activity还没被回收。
首先,mIns在onDestroy的时候被置成null,只是说明这个变量指向了一个空地址,并不是说当前的activity这个实例被置为空,BrowserActivity这个实例对象还存在于堆中,只有当对于这个对象的所有引用都为0的时候,gc才会去回收它。
我们在后面的分析中可以看到,匿名内部类也会持有这个BrowserActivity实例对象的引用,可以说这个引用和mIns是不同的变量,但是他们都指向了同一个实例。mINs虽然被置空了,但是匿名内部类这个还纯在着引用着activity实例,所以引用计数肯定是不为0的,也就是说还不能被回收。
------------------------------------------------------这里是分割线-----------------------------------------------------------------------------------------------
继续分析文章开头的第一次泄露的日志:
这里有个奇怪的问题就是,报告指出有view和activity的数量都不为0,但是ViewRoot的数量却为0,这个很费解啊,一个Activity不就只有一个ViewRoot吗,所有的view都是被ViewRoot实例操作的,而这里的ViewRoot却不见了。
先不理会这个奇怪的问题。我们继续把hprof文件导入到MAT中看看结果。
同样的方法定位到了BrowserActivity的引用链:
从上可以看出,BrowserActivity实例最终被一个线程持有了。从mappings中查到对应关系:
DownloadTouchIcon在源码中是一个继承自AsyncTask的类,其构造方法中需要传入一个ContentResolver对象,这个对象在doInbackgroud中有被使用到。
那么问题就很明显了,很可能是这个ContentResolver对象持有了activity,activity销毁的时候,contentResolver对象还在子线程中使用着,导致activity不能释放。
从上面的引用链也可以看出来,DownloadTouchIcon对象,引用了一个ApplicationContentResolver对象b(实际上就是ContentResolver的实现类),b又引用了一个ContextImpl对象mContext,mContext又引用了一个BrowserActivity类型的mOuterContext对象。
那为什么会这样呢?这就需要了解Context在整个Android应用系统中作用了。
我们知道一个应用程序中,有三种类型的Context,一个是Application,它继承自ContextWrapper(ContextWrapper又继承自Context),一个是activity,它继承自ContextThemeWrapper(ContextThemeWrapper又继承自ContextWrapper),一个是service,它继承自ContextWrapper。所以一个应用进程(单进程)的context的数量就是,activity + service + 1。
他们的关系如下:
最终,他们每个对象都有一个ContextImpl实例,而这个ContextImpl实例在被创建的时候,会调用setOuterContext(Context context),将当前这个对象(比如activity对象),传递到ContextImpl实例中,赋值给mOuterContext。
具体的创建过程可参考这个:Android应用程序窗口(Activity)的运行上下文环境(Context)的创建过程分析
这里,截取API25上,ActivityThread的performLaunchActivity代码的一段来分析下:
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
// System.out.println("##### [" + System.currentTimeMillis() + "] ActivityThread.performLaunchActivity(" + r + ")");
---------------
----------------
Activity activity = null;
try {
java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent); // 1、创建activity实例
StrictMode.incrementExpectedActivityCount(activity.getClass());
r.intent.setExtrasClassLoader(cl);
r.intent.prepareToEnterProcess();
if (r.state != null) {
r.state.setClassLoader(cl);
}
} catch (Exception e) {
if (!mInstrumentation.onException(activity, e)) {
throw new RuntimeException(
"Unable to instantiate activity " + component
+ ": " + e.toString(), e);
}
}
try {
Application app = r.packageInfo.makeApplication(false, mInstrumentation); // 2、创建application实例
if (localLOGV) Slog.v(TAG, "Performing launch of " + r);
if (localLOGV) Slog.v(
TAG, r + ": app=" + app
+ ", appName=" + app.getPackageName()
+ ", pkg=" + r.packageInfo.getPackageName()
+ ", comp=" + r.intent.getComponent().toShortString()
+ ", dir=" + r.packageInfo.getAppDir());
if (activity != null) {
Context appContext = createBaseContextForActivity(r, activity); // 3、创建ContextImpl实例
CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());
Configuration config = new Configuration(mCompatConfiguration);
if (DEBUG_CONFIGURATION) Slog.v(TAG, "Launching activity "
+ r.activityInfo.name + " with config " + config);
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);
主要是看三个注释点,先是创建activity实例,接着创建application实例,第三点,就是去创建ContextImpl实例了,我们跟进去看看:
private Context createBaseContextForActivity(ActivityClientRecord r, final Activity activity) {
int displayId = Display.DEFAULT_DISPLAY;
try {
displayId = ActivityManagerNative.getDefault().getActivityDisplayId(r.token);
} catch (RemoteException e) {
}
ContextImpl appContext = ContextImpl.createActivityContext(
this, r.packageInfo, displayId, r.overrideConfig); // 调用ContextImpl的静态方法创建
appContext.setOuterContext(activity); // 设置当前的activity(也就是上一步创建的activity)为mOuterContext
Context baseContext = appContext;
final DisplayManagerGlobal dm = DisplayManagerGlobal.getInstance();
// For debugging purposes, if the activity's package name contains the value of
// the "debug.use-second-display" system property as a substring, then show
// its content on a secondary display if there is one.
String pkgName = SystemProperties.get("debug.second-display.pkg");
if (pkgName != null && !pkgName.isEmpty()
&& r.packageInfo.mPackageName.contains(pkgName)) {
for (int id : dm.getDisplayIds()) {
if (id != Display.DEFAULT_DISPLAY) {
Display display =
dm.getCompatibleDisplay(id, appContext.getDisplayAdjustments(id));
baseContext = appContext.createDisplayContext(display);
break;
}
}
}
return baseContext;
}
还是看注释,调用ContextImpl的静态方法createActivityContext创建一个ContextImpl实例。注意这个方法在创建application的ContextImpl的时候也用到。
进入这个方法看看:
static ContextImpl createActivityContext(ActivityThread mainThread,
LoadedApk packageInfo, int displayId, Configuration overrideConfiguration) {
if (packageInfo == null) throw new IllegalArgumentException("packageInfo");
return new ContextImpl(null, mainThread, packageInfo, null, null, false,
null, overrideConfiguration, displayId);
}
这个方法直接new了一个对象。
我们看构造方法的最后一段:
private ContextImpl(ContextImpl container, ActivityThread mainThread,
LoadedApk packageInfo, IBinder activityToken, UserHandle user, boolean restricted,
Display display, Configuration overrideConfiguration, int createDisplayWithId) {
mOuterContext = this;
mMainThread = mainThread;
mActivityToken = activityToken;
mRestricted = restricted;
if (user == null) {
user = Process.myUserHandle();
}
mUser = user;
mPackageInfo = packageInfo;
mResourcesManager = ResourcesManager.getInstance();
final int displayId = (createDisplayWithId != Display.INVALID_DISPLAY)
? createDisplayWithId
: (display != null) ? display.getDisplayId() : Display.DEFAULT_DISPLAY;
CompatibilityInfo compatInfo = null;
if (container != null) {
compatInfo = container.getDisplayAdjustments(displayId).getCompatibilityInfo();
}
if (compatInfo == null) {
compatInfo = (displayId == Display.DEFAULT_DISPLAY)
? packageInfo.getCompatibilityInfo()
: CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO;
}
mDisplayAdjustments.setCompatibilityInfo(compatInfo);
mDisplayAdjustments.setConfiguration(overrideConfiguration);
mDisplay = (createDisplayWithId == Display.INVALID_DISPLAY) ? display
: ResourcesManager.getInstance().getAdjustedDisplay(displayId, mDisplayAdjustments);
Resources resources = packageInfo.getResources(mainThread);
if (resources != null) {
if (displayId != Display.DEFAULT_DISPLAY
|| overrideConfiguration != null
|| (compatInfo != null && compatInfo.applicationScale
!= resources.getCompatibilityInfo().applicationScale)) {
resources = mResourcesManager.getTopLevelResources(packageInfo.getResDir(),
packageInfo.getSplitResDirs(), packageInfo.getOverlayDirs(),
packageInfo.getApplicationInfo().sharedLibraryFiles, displayId,
overrideConfiguration, compatInfo);
}
}
mResources = resources;
if (container != null) {
mBasePackageName = container.mBasePackageName;
mOpPackageName = container.mOpPackageName;
} else {
mBasePackageName = packageInfo.mPackageName;
ApplicationInfo ainfo = packageInfo.getApplicationInfo();
if (ainfo.uid == Process.SYSTEM_UID && ainfo.uid != Process.myUid()) {
// Special case: system components allow themselves to be loaded in to other
// processes. For purposes of app ops, we must then consider the context as
// belonging to the package of this process, not the system itself, otherwise
// the package+uid verifications in app ops will fail.
mOpPackageName = ActivityThread.currentPackageName();
} else {
mOpPackageName = mBasePackageName;
}
}
mContentResolver = new ApplicationContentResolver(this, mainThread, user);
}
直接new了一个applicationContentResolver对象出来,并且把自身this传递进去。
也就是说,我们启动一个activity,系统会为我们创建这个activity实例,然后创建一个ContextImpl实例,然后创建一个ApplicationContentResolver实例,这三个是一一对应的。
我们平时在activity中使用contentResolver是怎么使用的呢?不就是getContentResolver来获取一个ContentResolver对象吗?
看看Activity的getContentResolver这个方法,额,竟然没在activity中找到,那我们去其父类ContextWrapper中看看,最终在ContextWrapper中看到这个方法:
@Override
public ContentResolver getContentResolver() {
return mBase.getContentResolver();
}
这里的mBase变量实际上就是ContextImpl了。
ContextImpl中直接返回了在构造方法中创建的ApplicationContentResolver对象:
@Override
public ContentResolver getContentResolver() {
return mContentResolver;
}
这样就理清了contentResolver是怎么来的了。
所以一旦我们在线程中使用了从activity获取到的contentResolver对象,那么必然就可能会导致内存泄露。
那怎么解决呢?很简单,我们直接从Application中获取contentResolver不就行了么,application存在于整个进程生命周期中,也就不会存在泄露了。
回头看下,在activityThread的performLaunchActivity中的第2处注释,这里执行创建application对象的过程(具体是LoadedApk类来实现):
public Application makeApplication(boolean forceDefaultAppClass,
Instrumentation instrumentation) {
--------
try {
java.lang.ClassLoader cl = getClassLoader();
if (!mPackageName.equals("android")) {
initializeJavaContextClassLoader();
}
ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this); // 使用同一个静态方法创建ContextImpl
app = mActivityThread.mInstrumentation.newApplication(
cl, appClass, appContext);
appContext.setOuterContext(app); // 关联本身
} catch (Exception e) {
if (!mActivityThread.mInstrumentation.onException(app, e)) {
throw new RuntimeException(
"Unable to instantiate application " + appClass
+ ": " + e.toString(), e);
}
}
-----------------
return app;
}
同样的创建ContextImpl的过程,application实例关联一个ContextImpl实例,关联一个ApplicationContentResolver实例。
所以在以后使用contentResolver的过程中可以尝试使用application得到。
这里有个疑问,既然application可以拿到一个contentResolver对象,那为什么还要给activity或是service也有一个contentResolver对象呢,他们之间似乎并没有什么不同的地方。。。?