今天调试app的时候,LeakCanary
提示开机视频页面SplashVideoActivity
出现内存泄漏。然后用Android Profiler
查看了一下,果然已经执行了finish
的SplashVideoActivity
还存在于内存中。其中罪魁祸首就是这个AudioManager
,它持有了SplashVideoActivity
的引用。
然后去网上查了一下VideoView
导致内存泄漏的问题,果然,原来是VideoView
自身的bug。有人在Google的 Issue Tracker
上提出了这个问题 Memory leak: VideoView prevents its activity from being GC’ed
导致这个问题的原因是
AudioManager
可能会长时间持有Context
,当使用者(这里即VideoView
)请求了音频的焦点却没有及时释放的时候。
大家提出了各自的规避方法,主要有两种方法,但原理都是一样,即让AudioManager
持有ApplicationContext
,而不是持有Activity
:
VideoView 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);
如果把VideoView
写在Activity
的布局文件中,初始化的时候自然是用Activity
的Context
,这里直接用代码初始化VideoView
,强行传ApplicationContext
,就避免了Activity
被持有可能导致的内存泄漏。
考虑到AudioManager
在VideoView
里面的初始化方法如下:
public VideoView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
...
mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
...
}
即它是通过调用Context
的getSystemService(Context.AUDIO_SERVICE)
方法初始化的,那么不妨在Activity
中重写这个方法,使用ApplicationContext
来调用它,如下:
@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);
}
});
}
这样AudioManager
将持有ApplicationContext
而不是Activity
。
然后有人说,以上的方法都不完美,因为真正的问题在于VideoView
没有释放音频焦点而导致AudioManager
没有及时释放Context
,而不在于传了Activity
。而且,如果按第一种方法用ApplicationContext
去初始化VideoView
,会存在一个隐患。因为如果VideoView
播放视频文件出现解码错误的时候,会弹出一个提示框AlertDialog
。而弹AlertDialog
需要Activity
的Context
,如果用ApplicationContext
去创建AlertDialog
,将直接导致crash
! 一言惊醒梦中人,对于大神,我只能大写加粗一个 服 字。对于第二种方法,他/她说因为AudioManager只是Context的一个成员变量,如果通过ApplicationContext去获取将会获取到错误的实例,这里我不太能理解。
最后官方修复了这个bug (时间点为2015年3月),主要修复了两个地方,一是在VideoView
中及时释放音频焦点,二是让AudioManager
持有ApplicationContext
而不是持有Context
(由此看来,上面第二种方法似乎可行),具体修复内容可以看 Fix context leak
而我的手机出现内存泄漏,可能是因为手机的版本比较低(Android 5.1)。根据修复的时间点,Android 6.0及之后的版本应该已经没有这个问题了,我对比了各版本的源码,自api 23之后就已经修复了此bug。如果要避免低版本手机出现此问题,我觉得可以用上面介绍的第二种方法。
经过测试: Android5.1版本的手机会出现此内存泄漏问题,Android7.0版本的手机没有。如果使用上述第二种方法,Android5.1版本的手机也不会出现此内存泄漏问题。
参考:关于Android VideoView导致的内存泄漏的问题