泄露信息如下:
====================================
HEAP ANALYSIS RESULT
====================================
1 APPLICATION LEAKS
References underlined with "~~~" are likely causes.
Learn more at https://squ.re/leaks.
Signature: 4f5c3ad956b290a0c61f4567bfd87dd73f3578
┬───
│ GC Root: Global variable in native code
│
├─ android.media.PlayerBase$1 instance
│ Leaking: UNKNOWN
│ Anonymous subclass of com.android.internal.app.IAppOpsCallback$Stub
│ ↓ PlayerBase$1.this$0
│ ~~~~~~
├─ android.media.MediaPlayer instance
│ Leaking: UNKNOWN
│ ↓ MediaPlayer.mSubtitleController
│ ~~~~~~~~~~~~~~~~~~~
├─ android.media.SubtitleController instance
│ Leaking: UNKNOWN
│ ↓ SubtitleController.mAnchor
│ ~~~~~~~
├─ android.widget.VideoView instance
│ Leaking: YES (View.mContext references a destroyed activity)
│ View is part of a window view hierarchy
│ View.mAttachInfo is null (view detached)
│ View.mID = R.id.video_view
│ View.mWindowAttachCount = 1
│ mContext instance of LoginActivity with mDestroyed = true
│ ↓ View.mContext
╰→ LoginActivity instance
Leaking: YES (ObjectWatcher was watching this because LoginActivity received
Activity#onDestroy() callback and Activity#mDestroyed is true)
key = 17268c98-e72a-4afb-9e79-ea4a86bba904
watchDurationMillis = 5170
retainedDurationMillis = 166
mApplication instance of ChatApplication
mBase instance of androidx.appcompat.view.ContextThemeWrapper
====================================
原因分析:
1、引用链结构:MediaPlayer 的SubtitleController 引用了VideoView 中的context对象,而VideoView 对象中的mContext引用到了LoginActivity 中context,导致LoginActivity 无法销毁。
查看源码得知在VideoView 中有如下代码:
final Context context = getContext();
final SubtitleController controller =new SubtitleController(context, mMediaPlayer.getMediaTimeProvider(), mMediaPlayer);
mMediaPlayer.setSubtitleAnchor(controller, this);
SubtitleController 持有了VideoView 中的context对象,进而MediaPlayer 持有了控件对象,当LoginActivity 无法销毁时却没有释放引用。
泄漏点的root引用是PlayerBase$1.this$0(PlayeBase的子类是MediaPlayer),这是IAppsCallback$Stub的匿名类,在7.0系统可以看到
http://androidxref.com/7.0.0_r1/xref/frameworks/base/media/java/android/media/PlayerBase.java
这个匿名内部类是Binder类,长生命周期持有短生命周期VideoPlayerActivity的引用,导致VideoPlayerActivity的泄漏
再进一步看,发现8.0以上的手机不会出现这个内存泄漏,原来是系统源码已经解决了这个内存泄漏
http://androidxref.com/8.0.0_r4/xref/frameworks/base/media/java/android/media/PlayerBase.java
处理方式是初始化mAppOpsCallback时,new 一个静态内部类,并且这个静态类传入的参数是弱引用
2、解决方法:因为在源码层面无法修改源码,在引用端切断引用链。
import android.content.Context;
import android.os.Build;
import android.util.AttributeSet;
import android.view.ViewGroup;
import android.widget.VideoView;
/**
* 关于Android VideoView导致的内存泄漏的问题
* https://www.jianshu.com/writer#/notebooks/38625762/notes/93298920
*/
public class NoMemoryLeakVideoViewextends VideoView {
public NoMemoryLeakVideoView(Context context) {
super(context.getApplicationContext());
}
public NoMemoryLeakVideoView(Context context, AttributeSet attrs) {
super(context.getApplicationContext(), attrs);
}
public NoMemoryLeakVideoView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context.getApplicationContext(), attrs, defStyleAttr);
}
public void clearMemoryLeak(ViewGroup container) {
suspend();
clearFocus();
// VideoView在发生播放错误的时候,会有弹窗错误提示的Dialog,Dialog依赖传入mContext,如果是Application,那么会报崩溃。
// 解决方法: 通过查看源码发现,在弹这个dialog的时候,会有条件判断,拦截这个条件不弹错误提示的Dialog即不会崩溃,然后这个Dialog在外部回调接口弹出。这个拦截条件是VideoView的setOnErrorListener的实现方法返回true。
setOnErrorListener((mp, what, extra) ->true);
setOnPreparedListener(null);
setOnCompletionListener(null);
setOnTouchListener(null);
setOnClickListener(null);
setOnDragListener(null);
setOnKeyListener(null);
setOnLongClickListener(null);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) {
setOnApplyWindowInsetsListener(null);
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
setOnScrollChangeListener(null);
}
setOnFocusChangeListener(null);
if (container !=null) {
container.removeView(this);
}
}
}
最后在页面销毁的地方调用clearMemoryLeak方法
网友评论