[Android]浮层视频效果,在另外一个Window使用SurfaceView无法正常显示的问题排查与解决
最近在忙碌视频的事情,而视频的绘制需要使用到SurfaceView。为了完成浮层效果,我们很自然的想到使用多Window的方式。但是问题就来了,当你将你的SurfaceView放置在另外一个window中的时候,一切都变得不正常,为了验证这个东西,我写了一个小的demo:
代码非常简单,按下中间那个按钮,弹出一个Window,这个Window里面存放一个简单的SurfaceView,而这个Window的顶层View是一个FrameLayout。Window参数为:
private WindowManager.LayoutParams getWindowLayoutParams() { mWindowLayoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL; mWindowLayoutParams.setTitle("This is a test"); mWindowLayoutParams.width = WindowManager.LayoutParams.MATCH_PARENT; mWindowLayoutParams.height = WindowManager.LayoutParams.MATCH_PARENT; mWindowLayoutParams.flags |= WindowManager.LayoutParams.FLAG_FULLSCREEN | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN; mWindowLayoutParams.token = MainActivity.this.getWindow().getDecorView().getWindowToken(); return mWindowLayoutParams; }
好了,我们跑一下,就会发现界面没有任何变化,但是界面上的按钮都不可点击。这说明了什么呢?说明了你的Window已经被系统窗口管理服务所接收了,但是,界面显示出问题。我们给SurfaceView 增加一个SurfaceView回调,并在surfaceCreated处打印Log。你会发现,这个回调根本没有走。
@Override public void surfaceCreated(SurfaceHolder holder) { // TODO Auto-generated method stub Log.v("surface", "surfaceCreated"); }
这里我插入一句这个回调的作用,这个回调的作用在于告诉你,SurfaceFlinger给你分配的Surface已经可用了,但是这个回调不走,意味着当前状态下你的Surface处于不可用的状态,也就是SurfaceFlinger给你分配的Surface是不可用的。或许你到这里已经一头雾水了,不过不要紧,@非子墨兄刚开始也有点好奇,不过你静下来再想想,一个进程向SurfaceFlinger申请Surfacce并不是直接申请SurfaceFlinger服务申请的,而是向WindowManager服务申请的,也有可能是因为它引起的。我们在整理一下我们遇到的问题。我们增加了一个Window到窗口管理,但是我们看到了一个透明且没有surfacceCreate回调的SurfaceView。实际上这是两个问题:一个问题是透明,一个是没有回调。
我们先来解决第一个透明的问题,我们在顶层FrameLayout设置了背景后,发现还是透明的,这是为什么呢?是因为SurfaceView这个对象申请显示区域的时候非常特殊,并不是跟你的UI线程一个缓冲上叠加绘制,我们可以简单理解为它在UI线程所绘制的缓冲上开了个口子,然后在自己的Buffer上面绘制。那么怎么解决透明的问题呢?其实非常非常的简单,只需要给SurfaceView设置一个背景,告诉绘制服务你的这个SurfaceView是非透明的就可以了。我给SurfaceView设置了一个蓝色的背景,跑一下果然看到了效果:
好了,这样我们解决了第一个问题:透明问题。再次我们来看下第二个问题,SurfaceView不回调的问题。我们刚才对Surface对象无效的问题都纯属于猜测,为了验证我们的问题我们将SurfaceView中的Surface对象参数打印一下:
SurfaceView.java: private void updateWindow(boolean force, boolean redrawNeeded) { ... relayoutResult = mSession.relayout( mWindow, mWindow.mSeq, mLayout, mWidth, mHeight, visible ? VISIBLE : GONE, WindowManagerGlobal.RELAYOUT_DEFER_SURFACE_DESTROY, mWinFrame, mOverscanInsets, mContentInsets, mVisibleInsets, mConfiguration, mNewSurface); /*查看mSurface是否可用*/ booelan result = mSurface.isValid(); ... }果然不出我们所料,result的值为false。我们来看一下mSurface.isValid()的实现:
public boolean isValid() { synchronized (mLock) { if (mNativeObject == 0) return false; return nativeIsValid(mNativeObject); } }
可见,mNativeObject对象句柄为null,也就是系统并没有分配给你绘制内存句柄。这个时候,不知道你会不会放弃,告诉自己这是系统的问题,实际上你离真相已经很近了,只要再坚持一会儿就行。我们来看一下WMS的log:
W/WindowManager( 1154): Attempted to add window with token that is a sub-window: android.os.BinderProxy@432d6290. Aborting. W/WindowManager( 1154): Failed looking up window W/WindowManager( 1154): java.lang.IllegalArgumentException: Requested window android.os.BinderProxy@43296170 does not exist W/WindowManager( 1154): at com.android.server.wm.WindowManagerService.windowForClientLocked(WindowManagerService.java:7981) W/WindowManager( 1154): at com.android.server.wm.WindowManagerService.windowForClientLocked(WindowManagerService.java:7972) W/WindowManager( 1154): at com.android.server.wm.WindowManagerService.relayoutWindow(WindowManagerService.java:2784) W/WindowManager( 1154): at com.android.server.wm.Session.relayout(Session.java:190) W/WindowManager( 1154): at android.view.IWindowSession$Stub.onTransact(IWindowSession.java:235) W/WindowManager( 1154): at com.android.server.wm.Session.onTransact(Session.java:125) W/WindowManager( 1154): at android.os.Binder.execTransact(Binder.java:404) W/WindowManager( 1154): at dalvik.system.NativeStart.run(Native Method) W/WindowManager( 1154): Failed looking up window W/WindowManager( 1154): java.lang.IllegalArgumentException: Requested window android.os.BinderProxy@43296170 does not exist W/WindowManager( 1154): at com.android.server.wm.WindowManagerService.windowForClientLocked(WindowManagerService.java:7981) W/WindowManager( 1154): at com.android.server.wm.WindowManagerService.windowForClientLocked(WindowManagerService.java:7972) W/WindowManager( 1154): at com.android.server.wm.WindowManagerService.finishDrawingWindow(WindowManagerService.java:3105) W/WindowManager( 1154): at com.android.server.wm.Session.finishDrawing(Session.java:224) W/WindowManager( 1154): at android.view.IWindowSession$Stub.onTransact(IWindowSession.java:372) W/WindowManager( 1154): at com.android.server.wm.Session.onTransact(Session.java:125) W/WindowManager( 1154): at android.os.Binder.execTransact(Binder.java:404) W/WindowManager( 1154): at dalvik.system.NativeStart.run(Native Method)
我们粗浅的认为第二个堆栈引起的原因是因为第一个堆栈,而第一个堆栈引起的原因有可能是因为这句话:
W/WindowManager( 1154): Attempted to add window with token that is a sub-window: android.os.BinderProxy@432d6290. Aborting.这个其实不算是一个异常,可以当成系统提示,也就是说它将我们的Window当成一个简单的sub-window。我们看一下WMS这段代码的实现:
public int addWindow(Session session, IWindow client, int seq, WindowManager.LayoutParams attrs, int viewVisibility, int displayId, Rect outContentInsets, InputChannel outInputChannel) { ... if (attachedWindow.mAttrs.type >= FIRST_SUB_WINDOW && attachedWindow.mAttrs.type <= LAST_SUB_WINDOW) { Slog.w(TAG, "Attempted to add window with token that is a sub-window: " + attrs.token + ". Aborting."); return WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN; } ... if (addToken) { mTokenMap.put(attrs.token, token); } ... }
我们发现,当你以一个子Window的方式加入一个Window的时候,系统服务直接返回,这样就不能往mTokenMap中存放你的Token记录,而这个token不存在,导致了上面两个线程的异常堆栈。这样,我们离成功就只有一步之遥,我们已经定位我们的问题出在这句话:
mWindowLayoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL;而我们只需要改成FIRST_SUB_WINDOW和LAST_SUB_WINDOW之外的值就可以解决问题了。这里我选用了TYPE_TOAST
private WindowManager.LayoutParams getWindowLayoutParams() { mWindowLayoutParams.type = WindowManager.LayoutParams.TYPE_TOAST; mWindowLayoutParams.setTitle("This is a test"); mWindowLayoutParams.width = WindowManager.LayoutParams.MATCH_PARENT; mWindowLayoutParams.height = WindowManager.LayoutParams.MATCH_PARENT; mWindowLayoutParams.flags |= WindowManager.LayoutParams.FLAG_FULLSCREEN | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN; mWindowLayoutParams.token = MainActivity.this.getWindow().getDecorView().getWindowToken(); return mWindowLayoutParams; }
这样,SurfaceView的回调就正常了,此刻一切问题都迎刃而解。希望这篇文章能帮助到正在做这项功能的筒子们。
thx
郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。