1. 程式人生 > >關於Android VideoView導致的記憶體洩漏的問題

關於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
="@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本身。試了一下果然可行:

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 @Overrideprotected 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輸出,問題解決。