关于视频播放 VideoView 的一个 GC 问题

这段时间在一个项目中用到了视频和图片轮播的广告,一开始我使用了 ExoPlayer 视频播放库,结果出现了一个奇葩的问题 ANR on player release #4352,我的设备系统是 Android 5.1.1 于是我尝试工程更新到 AndroidX 将 ExoPlayer 更新到最新版本,结果还是会出问题,貌似在 Android 7.0 中这个问题得到了修复。迫于这个问题的困扰,于是我放弃了 ExoPlayer 而选择系统默认的视频播放库,结果遇到了另一个令人不爽的 bug bug 详细描述

这个问题在更高的 Android 系统中已经被修复 Fix context leak

最后变相解决 VideoView 在 Activity 中内存泄漏的方法:

创建 AudioServiceActivityLeak.java 类:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
 * Fixes a leak caused by AudioManager using an Activity context. 
 * Tracked at https://android-review.googlesource.com/#/c/140481/1 and
 * https://github.com/square/leakcanary/issues/205
 */
public class AudioServiceActivityLeak extends ContextWrapper {

  AudioServiceActivityLeak(Context base) {
    super(base);
  }

  public static ContextWrapper preventLeakOf(Context base) {
    return new AudioServiceActivityLeak(base);
  }

  @Override public Object getSystemService(String name) {
    if (Context.AUDIO_SERVICE.equals(name)) {
      return getApplicationContext().getSystemService(name);
    }
    return super.getSystemService(name);
  }
}

在使用 VideoView 的 Activity 中重写 attachBaseContext

1
2
3
4
5
6
public class ActivityUsingVideoView extends Activity {
  
  @Override protected void attachBaseContext(Context base) {
    super.attachBaseContext(AudioServiceActivityLeak.preventLeakOf(base));
  }
}

到这里问题已经可以解决了,我们来简单看一下解决的原理。

问题原因描述

而这个 attachBaseContext 方法是在 onCreate 之前调用,顺序如下:

Activity 启动调用顺序

Android Context本身是一个抽象类. ContextImpl, Activity, Service, Application这些都是Context的直接或间接子类, 下面通过看看这些类的关系,如下:

Context 子类关系图

Application / Activity / Service 通过 attach() 调用父类 ContextWrapper 的 attachBaseContext(), 从而设置父类成员变量 mBase 为 ContextImpl 对象; ContextWrapper 的核心工作都是交给 mBase (即 ContextImpl )来完成;

要理解 Context, 需要依次来看看四大组件的初始化过程,下面是 Activity 的初始化过程。

ActivityThread.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
    //...
    ActivityInfo aInfo = r.activityInfo;
    if (r.packageInfo == null) {
        //step 1: 创建LoadedApk对象
        r.packageInfo = getPackageInfo(aInfo.applicationInfo, r.compatInfo,
                Context.CONTEXT_INCLUDE_CODE);
    }
    //... //component初始化过程

    java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
    //step 2: 创建Activity对象
    Activity activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);
    //...

    //step 3: 创建Application对象
    Application app = r.packageInfo.makeApplication(false, mInstrumentation);

    if (activity != null) {
        //step 4: 创建ContextImpl对象
        Context appContext = createBaseContextForActivity(r, activity);
        CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());
        Configuration config = new Configuration(mCompatConfiguration);
        //step5: 将Application/ContextImpl都attach到Activity对象 [见小节4.1]
        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);

        //...
        int theme = r.activityInfo.getThemeResource();
        if (theme != 0) {
            activity.setTheme(theme);
        }

        activity.mCalled = false;
        if (r.isPersistable()) {
            //step 6: 执行回调onCreate
            mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
        } else {
            mInstrumentation.callActivityOnCreate(activity, r.state);
        }

        r.activity = activity;
        r.stopped = true;
        if (!r.activity.mFinished) {
            activity.performStart(); //执行回调onStart
            r.stopped = false;
        }
        if (!r.activity.mFinished) {
            //执行回调onRestoreInstanceState
            if (r.isPersistable()) {
                if (r.state != null || r.persistentState != null) {
                    mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state,
                            r.persistentState);
                }
            } else if (r.state != null) {
                mInstrumentation.callActivityOnRestoreInstanceState(activity, r.state);
            }
        }
        //...
        r.paused = true;
        mActivities.put(r.token, r);
    }

    return activity;
}

startActivity 的过程最终会在目标进程执行 performLaunchActivity() 方法, 该方法主要功能:

  1. 创建对象 LoadedApk;
  2. 创建对象 Activity;
  3. 创建对象 Application;
  4. 创建对象 ContextImpl;
  5. Application / ContextImpl 都 attach 到 Activity 对象;
  6. 执行 onCreate() 等回调;

再来看一看 ContextWrapper 类,ContextWrapper 是一个典型的装饰模式。其中 AUDIO_SERVICE 用于获得 AudioManager 对象上下文。也就是上面提到的解决方案,使用 AudioManager 的上下文 Context 替换 Activity 的上下文来解决此 bug.

本文参考文章:

http://gityuan.com/2017/04/09/android_context/

https://issuetracker.google.com/issues/37024105

https://github.com/google/ExoPlayer/issues/4352

https://gist.github.com/jankovd/891d96f476f7a9ce24e2