关于Android VideoView导致的内存泄漏的问题

最近在做的项目里要实现在Android中播放视频这么一个需求。功能本身并不复杂,因为赶时间图省事,没有用Android底层的MediaPlayer API,直接用了谷歌封装好的VideoView组件:

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
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="@android:color/black">

<RelativeLayout
android:id="@+id/videoContainer"
android:layout_width="fill_parent"
android:layout_height="fill_parent">

<VideoView
android:id="@+id/videoView"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_centerInParent="true"/>

<View
android:id="@+id/placeHolder"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="@android:color/black"/>

</RelativeLayout>

<!-- ...... -->

</RelativeLayout>

像这样在XML里声明一下,然后调用setVideoPath方法传入视频路径,startpausestopPlayback方法控制播放,基本功能就做好啦。可喜可贺,可喜可贺。

但是在实机运行时,我发现即使关闭了含有VideoView的Activity,它申请到的内存也不会被释放。多次进入这个页面再关闭的话,程序占用的内存越来越多,用Android Studio自带的Java Heap Dump功能可以看到这些Activity都没有被回收掉,显然在某些地方出现了内存泄漏。经过一番仔细排查,问题定位到了VideoView.setVideoPath方法上,屏蔽掉这一行,Activity就可以正常被回收;把这一行加回来,问题又出现了。

这个方法是用来给VideoView传视频url的,当然不能删掉了事,于是开google,看到有人在天国的google code上讨论过这个问题。大体意思是说,VideoView内部的AudioManager会对Activity持有一个强引用,而AudioManager的生命周期比较长,导致这个Activity始终无法被回收,这个bug直到2015年5月才被谷歌修复。因为Activity是通过VideoView的构造函数传给AudioManager的,所以回复里有人提出了一个workaround:不要用XML声明VideoView,改用代码动态创建,创建的时候把全局的Context传给VideoView,而不是Activity本身。试了一下果然可行:

1
2
3
4
5
videoView = new VideoView(getApplicationContext());
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
params.addRule(RelativeLayout.CENTER_IN_PARENT);
videoView.setLayoutParams(params);
((RelativeLayout)findViewById(R.id.videoContainer)).addView(videoView, 0);

这样做到底还是感觉不太优雅,且不说动态创建和XML声明混到一起后期难维护,直接传全局Context总觉得程序会在哪个魔改rom上就崩掉了。考虑到内存泄漏的原因出在AudioManager上,所以只针对它作处理就足够了。于是把VideoView放回XML里,重写Activity.attachBaseContext方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
protected void attachBaseContext(Context newBase)
{
super.attachBaseContext(new ContextWrapper(newBase)
{
@Override
public Object getSystemService(String name)
{
if (Context.AUDIO_SERVICE.equals(name))
return getApplicationContext().getSystemService(name);

return super.getSystemService(name);
}
});
}

重新调试,为了方便跟踪回收事件我在finalize方法中加了一行log,关闭Activity之后调用System.gc(),果然有回收的log输出,问题解决。