客户在使用展锐平台在文件管理器中打开一个视频循环播放,长时间发现播放自动停止,无法继续播放
通过排查log发现出现如下问题
08-08 19:34:35.881 1941 1941 E JavaBinder: !!! FAILED BINDER TRANSACTION !!! (parcel size = 232)
08-08 19:34:35.920 1941 1941 W MoviePlayerVideoView: Unable to open the videocontent://com.android.providers.media.documents/document/video%3A13
08-08 19:34:35.920 1941 1941 W MoviePlayerVideoView: DeadSystemException: The system died; earlier logs will point to the root cause
通过定位代码发现未出现明显错误,网上对JavaBinder: !!! FAILED BINDER TRANSACTION !!! 错误的大部分原因都是intent传递的数据过大导致。
但是通过本地代码查看未出现intent相关的操作,但是仍然出现了FAILED BINDER TRANSACTION !,那么就要在binder失败入手,考虑到此问题是长时间运行后必现的问题,那么
就要往内存泄露的方向去考虑了,一次小的binder传输可能不会有啥问题,但是如果内存泄露积累多了,是不是就会导致 JavaBinder: !!! FAILED BINDER TRANSACTION !!! (parcel size = 232)
顺着这个思路我们去排查一下内存泄露,通过log来定位 W MoviePlayerVideoView: Unable to open the videocontent://com.android.providers.media.documents/document/video%3A13
二分法加log判断具体行数,最终定位到
MoviePlayerVideoView.java里的mMediaPlayer.setDataSource(mContext, mUri, mHeaders);
这一行,通过查看MediaPlayer的代码可以发现setDataSource的时候确实去跟服务端进行了通讯,
前面的
mMediaPlayer.setOnPreparedListener(mPreparedListener);
mMediaPlayer.setOnVideoSizeChangedListener(mSizeChangedListener);
mMediaPlayer.setOnSeekCompleteListener(mSeekCompleteListener); // SPRD: add
mMediaPlayer.setOnCompletionListener(mCompletionListener);
mMediaPlayer.setOnErrorListener(mErrorListener);
mMediaPlayer.setOnInfoListener(mInfoListener);// MERGETEMP
mMediaPlayer.setOnBufferingUpdateListener(mBufferingUpdateListener);
这些操作,包括
mMediaPlayer = new MediaPlayer();
的时候都没有去binder服务端。
我们把这段代码整个贴出来,
private void openVideo() {
if (mUri == null || mSurfaceHolder == null) {
/** SPRD:not ready for playback just yet, will try again later */
return;
}
Log.i(TAG, "openVideo()");
/* SPRD:Delete for bug609816 The wrong logic for audiofocus @{
AudioManager am = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
am.requestAudioFocus(null, AudioManager.STREAM_MUSIC,
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);*/
/* Bug609816 end @} */
/**
* SPRD:we shouldn't clear the target state, because somebody might have called start()
* previously
*/
release(false);
try {
mMediaPlayer = new MediaPlayer();
// TODO: create SubtitleController in MediaPlayer, but we need
/** SPRD: a context for the subtitle renderers */
final Context context = getContext();
StandardFrameworks.getInstances().setVideoViewRendererAndSubtitleAnchor(context, mMediaPlayer, this);
if (mAudioSession != 0) {
mMediaPlayer.setAudioSessionId(mAudioSession);
} else {
mAudioSession = mMediaPlayer.getAudioSessionId();
}
mMediaPlayer.setOnPreparedListener(mPreparedListener);
mMediaPlayer.setOnVideoSizeChangedListener(mSizeChangedListener);
mMediaPlayer.setOnSeekCompleteListener(mSeekCompleteListener); // SPRD: add
mMediaPlayer.setOnCompletionListener(mCompletionListener);
mMediaPlayer.setOnErrorListener(mErrorListener);
mMediaPlayer.setOnInfoListener(mInfoListener);// MERGETEMP
mMediaPlayer.setOnBufferingUpdateListener(mBufferingUpdateListener);
mCurrentBufferPercentage = 0;
mMediaPlayer.setDataSource(mContext, mUri, mHeaders);
mMediaPlayer.setDisplay(mSurfaceHolder);
mMediaPlayer.setAudioStreamType(AudioManageer.STREAM_MUSIC);
mMediaPlayer.setScreenOnWhilePlaying(true);
mMediaPlayer.prepareAsync();
/* SPRD:bug474646 Add Drm feature modify by old bug506989
* we can not consume when video trun to background. 355
@{ */
Log.d(TAG, "if need consume " + mNeedConsumeDrmRight);
// SPRD: Bug568552, temp modify for AndroidN porting @{
// mMediaPlayer.setNeedToConsume(mNeedConsumeDrmRight);
if (mNeedConsumeDrmRight) {
Log.i(TAG, "consumeDrmRights");
StandardFrameworks.getInstances().consumeDrmRights(mMediaPlayer);
}
// @}
/** @} */
for (Pair
addSubtitleSource(pending.first, pending.second);
}
/**
* SPRD:remove for 5.0 mMediaPlayer.setLastInterruptPosition(mInterruptPosition); //
* SPRD: add we don't set the target state here either, but preserve the target state
* that was there before.
*/
mCurrentState = STATE_PREPARING;
attachMediaController();
} catch (IOException ex) {
Log.w(TAG, "Unable to open content: " + mUri, ex);
mCurrentState = STATE_ERROR;
mTargetState = STATE_ERROR;
mErrorListener.onError(mMediaPlayer, MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
return;
} catch (IllegalArgumentException ex) {
Log.w(TAG, "Unable to open content: " + mUri, ex);
mCurrentState = STATE_ERROR;
mTargetState = STATE_ERROR;
mErrorListener.onError(mMediaPlayer, MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
return;
} catch (IllegalStateException ex) {
Log.w(TAG, "Unable to open" + mUri, ex);
mCurrentState = STATE_ERROR;
mTargetState = STATE_ERROR;
mErrorListener.onError(mMediaPlayer, MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
//add for bug529336 java.lang.NullPointerException
} catch (Exception ex) {
Log.w(TAG, "Unable to open the video" + mUri, ex);
mCurrentState = STATE_ERROR;
mTargetState = STATE_ERROR;
mErrorListener.onError(mMediaPlayer, MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
} finally {
mPendingSubtitleTracks.clear();
}
}
通过上面我们的猜想与内存泄露有关加上与binder有关,并且通过查看MediaPlayer的代码可以发现setDataSource的时候确实去跟服务端进行了通讯,
那么现在我们最应该怀疑的就是MediaPlayer对象了。上面有一段代码 StandardFrameworks.getInstances().setVideoViewRendererAndSubtitleAnchor(context, mMediaPlayer, this);
这里通过
getInstances语义可知这里是一个单例,在编程的时候单例如果处理不好很容易造成内存泄露。为了验证我们的猜想我们需要利用AndroidStudio自带工具Profiler去定位问题,
再刚开始播放的时候通过箭头的地方dump出来java heap
此时我们看到如下:
这时候MediaPlayer的实例只有58个。
等继续循环播放一段时间我们再次dump heap如下:
可以看到已经增加到234了,这个增加速度还是很惊人的,此时我们并不能确定是谁在引用这个对象,我们可以点进去查看如下:
发现就是我们的MoviePlayerVideoView在引用这个类,继续找到MoviePlayerVideoView的引用情况如下:
发现我们引用了太多SubtitleController,那么现在就更加印证了我们的猜测。
我们回到memory界面进行一次强制内存回收gc。如下:
回收之后我们再次dump heap如下:
虽然我们回收了但是这些MediaPlayer实例并未被回收而且SubtitleController一样的数量存在。这就更一步印证了它的
内存泄漏情况。
因为我们的单例就是用来控制setVideoViewRendererAndSubtitleAnchor,就是这里调用了SubtitleController
这个功能是展锐自己添加的悬浮窗播放的一些功能,此处我们不需要这个功能建议客户直接去掉测试。
修改完以后我们再次通过同样的方法去操作,而且强制回收以后mediaplayer对象可以得到正确释放,因此问题不再复现。
总结java层容易导致内存泄露的几种情况如下:
1、静态成员引起的内存泄露
2、单例模式引起的内存泄露
3、静态变量导致的内存泄露
4、Handler 引起的内存泄露
MessageQueue是在一个Looper线程中不断轮询处理消息,而有时候message还是个老不死,能够重复利用。如果当Activity退出时候,还有消息未处理或正在处理,由于message引用handler,handler又引用Activity,此时将引发内存泄露。
5、内部类引起的内存泄露
需要被释放的对象被一些常驻内存的对象持有那么就会出现泄露的情况存在,进而导致我们程序的异常,内存的浪费