【Android】广播未取消注册为什么会内存泄漏

1,337 阅读6分钟

一.背景

Android开发过程中,如果在OnCreate里通过registerReceiver注册了广播,就必须在OnDestroy里面调用unregisterReceiver取消注册,否则就会出现Activity泄漏。最近项目刚好遇到因广播未取消注册出现的内存泄露问题,下面分析广播导致内存泄漏的原因,以及如何定位到具体的代码。

以Activity内调用registerReceiver和unregisterReceiver进行说明,Service也类似。

二.为什么会内存泄漏

1.Activity的创建过程

在启动Activity时,会执行到ActivityThread#performLaunchActivity方法内,代码如下。重点方法已添加注释

// frameworks/base/core/java/android/app/ActivityThread.java
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
     ...
     // r.packageInfo类型是LoadedApk,通过applicationinfo获取当前进程的LoadedApk实例
     // 该实例在handleBindApplication内创建
     r.packageInfo = getPackageInfo(aInfo.applicationInfo, mCompatibilityInfo,
                    Context.CONTEXT_INCLUDE_CODE);
     
     // 1.该方法会调用到ContextImpl的构造函数
     ContextImpl activityBaseContext = = createBaseContextForActivity(r);
     
     // 2.创建Activity
     Activity activity = mInstrumentation.newActivity(
                    cl, component.getClassName(), r.intent);
            StrictMode.incrementExpectedActivityCount(activity.getClass());;
            
    // 3.activityBaseContext的mOuterContext属性赋值为当前activity
    activityBaseContext.setOuterContext(activity);
    
    // 4.调用Activity的attach方法,activityBaseContext作为参数传入
    activity.attach(activityBaseContext, 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.activityConfigCallback,
                        r.assistToken, r.shareableActivityToken);
    
    // 5.调用Activity的OnCreate方法
    mInstrumentation.callActivityOnCreate(activity, r.state);
    
    ...
}

第1步创建ContextImpl时会进入ContextImpl的构造函数,其mPackageInfo属性保存每个进程对应的LoadedApk实例

// frameworks/base/core/java/android/app/ContextImpl.java
private ContextImpl(@Nullable ContextImpl container, @NonNull ActivityThread mainThread,
        @NonNull LoadedApk packageInfo, @NonNull ContextParams params,
        @Nullable String attributionTag, @Nullable AttributionSource nextAttributionSource,
        @Nullable String splitName, @Nullable IBinder token, @Nullable UserHandle user,
        int flags, @Nullable ClassLoader classLoader, @Nullable String overrideOpPackageName) {
    ...
    mOuterContext = this;
    mMainThread = mainThread;
    mToken = token;
    mFlags = flags;
    // 保存LoadedApk实例,后面会用到
    mPackageInfo = packageInfo ;
    ...
}

第2步和第5步是创建Activity实例和调用Activity的OnCreate方法,不详细介绍。

第3步是将当前Activity设置到activityBaseContext中。

第4步调用Activity的attach方法,该方法内创建PhoneWindow等,目前只需要关注Context相关处理即可,代码如下

// frameworks/base/core/java/android/app/Activity.java
final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor,
            Window window, ActivityConfigCallback activityConfigCallback, IBinder assistToken,
            IBinder shareableActivityToken) {
        ...
        attachBaseContext(context);
        
        //创建PhoneWindow,不详细介绍
        mWindow = new PhoneWindow(this, window, activityConfigCallback);
        ...
        //给Window设置WindowManagerImpl,不详细介绍
        mWindow.setWindowManager(
                (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
                mToken, mComponent.flattenToString(),
        ...        
}       
               

attachBaseContext层层调用父类方法,最终ContextWrapper类中给mBase属性赋值

// frameworks/base/core/java/android/content/ContextWrapper.java
protected void attachBaseContext(Context base) {
    if (mBase != null) {
        throw new IllegalStateException("Base context already set");
    }
    mBase = base;
}

2.Activity注册/取消注册关播

注册和取消注册的代码都在Activity的父类ContextWrapper中实现,下面代码可以看到都是转交给mBase(ContextImpl实例)来处理的,该mBase是在performLaunchActivity里创建的,mBase里面的mOuterContext就是在performLaunchActivity创建的Activity。

// frameworks/base/core/java/android/app/ContextImpl.java
@Override
public Intent registerReceiver(@Nullable BroadcastReceiver receiver, IntentFilter filter) {
    return mBase.registerReceiver(receiver, filter);
}

@Override
public void unregisterReceiver(BroadcastReceiver receiver) {
    mBase.unregisterReceiver(receiver);
}

3.ContextImpl注册关播的过程

注册广播会调用到registerReceiverInternal方法,参数的getOuterContext()就是我们的Activity

// frameworks/base/core/java/android/app/ContextImpl.java
@Override
public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
    return registerReceiver(receiver, filter, null, null);
}

@Override
public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter,
        String broadcastPermission, Handler scheduler) {
    //getOuterContext()就是我们在performLaunchActivity创建的Activity
    return registerReceiverInternal(receiver, getUserId(),
            filter, broadcastPermission, scheduler, getOuterContext () , 0);
}
// frameworks/base/core/java/android/app/ContextImpl.java
private Intent registerReceiverInternal(BroadcastReceiver receiver, int userId,
        IntentFilter filter, String broadcastPermission,
        Handler scheduler, Context context, int flags) {
    IIntentReceiver rd = null;
    ...
    if (scheduler == null) {
        scheduler = mMainThread.getHandler();
    }
    // 重点是这个
    rd = mPackageInfo.getReceiverDispatcher(
        receiver, context, scheduler,
        mMainThread.getInstrumentation(), true);
    ...
    // AMS里面注册广播,本文不关注
    final Intent intent = ActivityManager.getService().registerReceiverWithFeature(
            mMainThread.getApplicationThread(), mBasePackageName, getAttributionTag(),
            AppOpsManager.toReceiverId(receiver), rd, filter, broadcastPermission, userId,
            flags);
    ...
    return intent;
}

mPackageInfoLoadedApk实例,是在进程创建后的handleBindApplication方法时创建,每个进程只有一个。

LoadedApk的mReceivers是个Map,Key是Context,Value也是个Map(用于保存一个Activity注册的多个广播)

// frameworks/base/core/java/android/app/LoadedApk.java
@UnsupportedAppUsage
private final ArrayMap<Context, ArrayMap<BroadcastReceiver, ReceiverDispatcher>> mReceivers
    = new ArrayMap<>();
    
public IIntentReceiver getReceiverDispatcher(BroadcastReceiver r,
        Context context, Handler handler,
        Instrumentation instrumentation, boolean registered) {
    synchronized (mReceivers) {
        LoadedApk.ReceiverDispatcher rd = null;
        ArrayMap<BroadcastReceiver, LoadedApk.ReceiverDispatcher> map = null;
        // registered参数是true
        if (registered) {
            // 先看下当前Context是否注册过广播,如果存在就获取注册的Map
            map = mReceivers.get(context);
            if (map != null) {
                rd = map.get(r);
            }
        }
        if (rd == null) {
            // 当前Context未注册过,创建一个ReceiverDispatcher
            rd = new ReceiverDispatcher(mActivityThread.getApplicationThread(), r, context,
                    handler, instrumentation, registered);
            // 该参数是true
            if (registered) {
                if (map == null) {
                    // 创建一个map,用来保存该Context所有注册的广播
                    map = new ArrayMap<BroadcastReceiver, LoadedApk.ReceiverDispatcher>();
                    // 将context和map存入mReceivers中
                    mReceivers.put(context, map);
                }
                // 将当前的广播保存在map中
                map.put(r, rd);
            }
        } else {
            rd.validate(context, handler);
        }
        rd.mForgotten = false;
        return rd.getIIntentReceiver();
    }
}

4.ContextImpl取消注册广播

如果Context对应的广播都被取消后,则从mReceivers移除context。mReceivers移除对Context的强引用后,下次GC的时候该Context就可能被回收了。

// frameworks/base/core/java/android/app/ContextImpl.java
@Override
public void unregisterReceiver(BroadcastReceiver receiver) {
    if (mPackageInfo != null) {
        // 移除当前context中的receiver
        IIntentReceiver rd = mPackageInfo.forgetReceiverDispatcher(
                getOuterContext(), receiver);
        ...
        // AMS里面取消注册广播,本文不关注
        ActivityManager.getService().unregisterReceiver(rd);
    } else {
        throw new RuntimeException("Not supported in system context");
    }
}
// frameworks/base/core/java/android/app/LoadedApk.java
public IIntentReceiver forgetReceiverDispatcher(Context context,
            BroadcastReceiver r) {
    synchronized (mReceivers) {
        // 获取当前context所有注册过的广播map
        ArrayMap<BroadcastReceiver, LoadedApk.ReceiverDispatcher> map = mReceivers.get(context);
        LoadedApk.ReceiverDispatcher rd = null;
        // map != null说明该context注册过广播
        if (map != null) {
            // 从map里面获取该广播对应的ReceiverDispatcher
            rd = map.get(r);
            if (rd != null) {
                // 移除该广播
                map.remove(r);
                // 如果该广播是最后一个注册的广播了,则从mReceivers里面移除context
                if (map.size() == 0) {
                    mReceivers.remove(context);
                }
                ...
                rd.mForgotten = true;
                return rd.getIIntentReceiver();
            }
        }
        ...
    }
}

三.如何定位广播导致的内存泄漏的代码

出现内存泄露时先抓取hprof文件,然后通过MAT分析GC root。关于如果抓hprof可以参考官方文档,MAT的使用教程可以自行搜索下。项目中遇到的是Activity泄露,其GC root如下:

结合之前的分析可以看到是LoadedApk里面的mReceivers引用导致。定位到具体的代码方式也很简单,我们在mReceivers添加和移除的地方添加断点或者添加Log.e(TAG,"xx",new Exception())日志获取调用堆栈,通过分析调用堆栈查看哪边注册的广播未取消注册。