關於Android VideoView導致的記憶體洩漏的問題
今天用 leakcanary 時發現用VideoView的 activity 出現洩漏,捕獲到如下的資訊,簡單說就是 android M(6.0)以前AudioManager用的Context是 當前傳入的,當activity finish之後 AudioManager依然保持對它的引用,所以就leak了,6.0後改用ApplicationContext修復了此問題,google後發現下面這種解決方法也不錯
首先宣告,下面的文章是我轉載的,感謝原作者的分享,來源https://blog.yeatse.com/2015/11/01/android-video-view-memory-leak
最近在做的專案裡要實現在Android中播放視訊這麼一個需求。功能本身並不複雜,因為趕時間圖省事,沒有用Android底層的MediaPlayer API,直接用了谷歌封裝好的VideoView
12345678910111213141516171819202122232425262728 |
<?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 |
像這樣在XML裡宣告一下,然後呼叫setVideoPath
方法傳入視訊路徑,start
、pause
和stopPlayback
方法控制播放,基本功能就做好啦。可喜可賀,可喜可賀。
但是在實機執行時,我發現即使關閉了含有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本身。試了一下果然可行:
12345 | 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
方法:
123456789101112131415 | protected void attachBaseContext(Context newBase){ super.attachBaseContext(new ContextWrapper(newBase) { 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輸出,問題解決。