记一次匿名内部类引起的内存泄露,contentResolver引起的泄露

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/anhenzhufeng/article/details/80322631

公司开发的自动化测试工具发现有内存泄露。


导出当时的.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对象呢,他们之间似乎并没有什么不同的地方。。。?

猜你喜欢

转载自blog.csdn.net/anhenzhufeng/article/details/80322631
今日推荐