今天調試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
:
方法一:在Java代碼中初始化VideoView
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
被持有可能導致的內存泄漏。
方法二:重寫getSystemService方法
考慮到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版本的手機也不會出現此內存泄漏問題。