时间: 2020-09-03 00:08:26 人气: 2288 评论: 0
本文对小窗视频播放进行了详细的研究,针对几种实现方案进行了深入的对比分析,进而给出实现小窗视频播放的最优解。其中通过对系统源码的分析,详细探究了如何完美地实现移动、缩放等效果,很有技术深度。文中几种方案的对比,以及
SurfaceView、GLSurfaceView和TextureView相关知识点的讲解,非常实用,值得收藏。 — 责任编辑 junyihan
由于文章篇幅较长,将分为上、下两篇。上篇主要介绍小窗播放视频的原理,下篇主要介绍小窗播放视频的实现。
目前很多视频类App都有小窗播放功能,比如Youtube(如图1)、Facebook(如图2)等,不过它们的实现方式却不同。Youtube 是将视频播放View内嵌到应用内,优点是交互好;Facebook则是通过WindowManager添加视频播放View,同时支持应用内部和外部播放。
小窗播放视频功能在小窗和大屏之间切换时,视频类App通常一边执行交互动作一边播放视频。交互动作包括移动、缩放或者动画;这些App在播放时期望给用户平滑的过渡体验,流畅加载视频,不能有明显的卡顿。
二、SurfaceView 和 GLSurfaceView
Android 中使用 MediaPlayer 播放视频时,一般采用SurfaceView、GLSurfaceView和TextureView。Youtube、Facebook用不同技术方案实现了小窗播放视频功能,它们共同点是都可以使用SurfaceView、GLSurfaceView和TextureView来播放视频。接下来分析三个视图用于小窗播放视频的原理。
SurfaceView继承自类View,但与其他View的子类不同的是它有独立的Surface,如下源码可以看出它包含一个Surface属性,即它不与宿主窗口共享同一个绘图表面。因此SurfaceView的UI可以在一个独立的线程中进行绘制。由于不会占用主线程资源,因此SurfaceView可以实现复杂而高效的UI。GLSurfaceView继承SurfaceView,作为SurfaceView的补充,加入了EGL的管理,并自带了渲染线程。它用于小窗播放时效果和SurfaceView类似。本文以下内容以SurfaceView为例分析独立的Surface对小窗播放会有什么影响。
public class SurfaceView extends View { final Surface mSurface = new Surface(); }
Handle onto a raw buffer that is being managed by the screen compositor.[1]
通过Surface的类注释可以知道,Surface处理一块由Screen compositor管理的Raw buffer。而Screen compositor其实就是SurfaceFlinger服务。Surface字面意思就是绘图表面,可以理解为是UI的画布。
Android应用程序窗口需要请求SurfaceFlinger服务创建绘图表面(也就是Surface对象),同时窗口还需要被WindowManagerService管理;所以实际上窗口的绘图表面是通过两个Surface对象来描述,一个是应用程序进程创建的,另一个是由WindowManagerService创建的,这两个Surface对象对应于SurfaceFlinger服务的同一个Layer对象。
在应用程序进程这一侧,每一个应用程序窗口,如Activity,都有一个Surface对象,就是在ViewRootImpl对象的mSurface属性,这个Surface用来绘制应用程序窗口的UI,如下ViewRootImpl源码所示。当界面需要刷新时,窗口调用draw方法,向Surface请求canvas,执行绘画操作后再次提交给Surface完成屏幕显示。
在WindowManagerService服务这一侧,每一个窗口,都有一个对应的WindowState对象。其有一个属性mSurface,它负责设置窗口的位置、大小属性。例如,一个窗口的Z轴坐标大小要考虑到它的窗口类型,以及它与系统中的其它窗口的关系[2]。
public final class ViewRootImpl { private final Surface mSurface = new Surface(); private void draw(boolean fullRedrawNeeded) { if (!drawSoftware(surface, mAttachInfo, xOffset, yOffset, scalingRequired, dirty)) { } } private boolean drawSoftware(Surface surface, AttachInfo attachInfo,……) { inal Canvas canvas; canvas = mSurface.lockCanvas(dirty); surface.unlockCanvasAndPost(canvas); } }
SurfaceView有独立的绘图表面,那么SurfaceView又是怎么绘制在宿主窗口的呢?
上面说到每一个窗口在SurfaceFlinger服务中都对应有一个Layer,用来描述它的绘图表面。同时每一个SurfaceView在SurfaceFlinger服务中还对应有一个独立的Layer或者LayerBuffer,用来单独描述它的绘图表面,以区别于它的宿主窗口的绘图表面[3]。
(图3 SurfaceView的Surface的创建过程)
接下来了解Surface创建过程,如图3的时序图所示,每当一个窗口需要刷新UI时,就会调用ViewRootImpl类的performTraversals方法。如果当前窗口的Surface还没有创建,或者已经失效,SurfaceView就会请求WindowManagerService服务创建一个新的Surface,它最终会调用updateWindow来完成Surface的创建。
public class SurfaceView extends View { final Surface mSurface = new Surface(); MyWindow mWindow; int mWindowType = WindowManager.LayoutParams.TYPE_APPLICATION_MEDIA; private void updateWindow(boolean force, boolean redrawNeeded) { if (mWindow == null) { mSession.addWithoutInputChannel(mWindow, mLayout,……); } …… mSurfaceLock.lock(); try { …… final int relayoutResult = mSession.relayout(mWindow, mLayout,……, mSurface); …… } finally { mSurfaceLock.unlock(); } }
如SurfaceView源码所示,它有一个mSurface属性,相比之下TextView或者Button等普通View会共用ViewRootImpl的Surface。
SurfaceView类的属性mWindowType描述的是SurfaceView的窗口类型,它的默认值等于TYPE_APPLICATION_MEDIA,用来显示多媒体的,如视频。SurfaceView还有另外一个类型TYPE_APPLICATION_MEDIA_OVERLAY,它是在视频上面显示Overlay的,它可以显示视字幕等信息。宿主窗口会遮挡这两个类型的SurfaceView,如果窗口嵌入这两类SurfaceView,那么它们的Z轴位置会低于该窗口的Z轴位置,显示在该窗口下面。如果mWindow等于null的话,那么就说明该SurfaceView还没有添加到WindowManagerService服务中去,然后调用addWithoutInputChannel添加到WindowManagerService服务中。
mSession.relayout()请求WindowManagerService服务对SurfaceView的UI进行布局。如果宿主窗口的绘制表面还未创建,或者需要重新创建,那么就会请求SurfaceFlinger服务为它创建一个新的Surface。由于这一步可能会修改SurfaceView的Surface,所以添加了mSurfaceLock锁,避免其它线程同时修改该Surface的内容。
执行完成上述步骤之后,SurfaceView的Surface的创建完成了。但是mWindowType为TYPE_APPLICATION_MEDIA或TYPE_APPLICATION_MEDIA_OVERLAY的SurfaceView会被宿主窗口挡住,如何解决这个问题,这就要了解SurfaceView“挖洞”原理。
当SurfaceView附加宿主窗口时,它的onAttachedToWindow会被调用。这个方法调用requestTransparentRegion请求在宿主窗口上设置透明区域,即请求在宿主窗口上挖洞,其实就是设置ViewRootImpl中Surface的透明度。而每当其宿主窗口刷新自己的UI的时候,就会调用ViewGroup的gatherTransparentRegion将所有嵌入在它里面的SurfaceView所设置的透明区域收集起来。然后再通知WindowManagerService为SurfaceView的gatherTransparentRegion方法设置一个总的透明区域。这就是SurfaceViewd的“挖洞”原理,如图4时序图所示。
(图4 SurfaceView“挖洞”原理)
接下来结合ViewRootImpl类的requestTransparentRegion源码,来分析请求在宿主窗口上设置透明区域的过程。
public final class ViewRootImpl implements ……{ public void requestTransparentRegion(View child) { if (mView == child) { mView.mPrivateFlags |= View.REQUEST_TRANSPARENT_REGIONS; mWindowAttributesChanged = true; requestLayout(); } } }
当mView等于子View时,将mPrivateFlags的View.REQUEST_TRANSPARENT_REGIONS位设置为1,表示该窗口被设置了一块透明区域。当一个窗口被请求设置了一块透明区域之后,它的窗口属性就发生了变化,这时候除了要将与它所关联的ViewRootImpl对象的mWindowAttributesChanged值设置为true之外,还要调用该ViewRootImpl对象的requestLayout方法对窗口的UI进行重新布局和绘制。requestLayout最终会调用到另外一个方法performTraversals来实际执行刷新窗口UI的操作。
public final class ViewRootImpl implements ……{ private void performTraversals() { final View host = mView; if (didLayout) { host.layout(0, 0, host.mMeasuredWidth, host.mMeasuredHeight); if (……); host.gatherTransparentRegion(mTransparentRegion); if (!mTransparentRegion.equals(mPreviousTransparentRegion)) { mPreviousTransparentRegion.set(mTransparentRegion); try { sWindowSession.setTransparentRegion(mWindow, mTransparentRegion); } catch (RemoteException e) { } } } } boolean cancelDraw = attachInfo.mTreeObserver.dispatchOnPreDraw(); if (!cancelDraw && !newSurface) { draw(fullRedrawNeeded); } } }
ViewRootImpl类的方法performTraversals是用来收集嵌入在它里面的SurfaceView所设置的透明区域的。它处于窗口的UI布局完成之后,窗口的UI绘制之前。这是因为窗口的UI布局完成之后,各个子视图的大小和位置才能确定下来,进而才能确定SurfaceView的透明区域的位置和大小。从顶层视图开始,从上到下收集每一个子视图所要设置的区域,最终收集到的总透明区域并保存在ViewRootImpl类的成员变量mTransparentRegion中。其中host是DecorView,它的gatherTransparentRegion方法重载了父类ViewGroup的gatherTransparentRegion方法。
public abstract class ViewGroup extends View …… { @Override public boolean gatherTransparentRegion(Region region) { //检测mPrivateFlags的REQUEST_TRANSPARENT_REGIONS位是否为1,因为如果当前视图不透明,子视图都不可能设置有透明区域。 final boolean meOpaque = (mPrivateFlags & View.REQUEST_TRANSPARENT_REGIONS) == 0; if (meOpaque && region == null) { return true; } super.gatherTransparentRegion(region); …… boolean noneOfTheChildrenAreTransparent = true; for (int i = 0; i < count; i++) { final View child = children[i]; if ((child.mViewFlags & VISIBILITY_MASK)==VISIBLE||child.getAnimation() != null){ if (!child.gatherTransparentRegion(region)) { noneOfTheChildrenAreTransparent = false; } } } return meOpaque || noneOfTheChildrenAreTransparent; } }
ViewGroup的gatherTransparentRegion方法中,检测到有透明区域时,调用父类View的方法gatherTransparentRegion来检查当前视图容器是否需要绘制。如果需要绘制,说明当前视图的前景需要绘制,就会将它所占据的区域从参数region所占据的区域移除,以便可以显示当前视图的前景。然后调用每一个子视图的成员函数gatherTransparentRegion来继续往下收集透明区域。
public class SurfaceView extends View { @Override public boolean gatherTransparentRegion(Region region) { if (mWindowType == WindowManager.LayoutParams.TYPE_APPLICATION_PANEL) { return super.gatherTransparentRegion(region); } boolean opaque = true; if ((mPrivateFlags & SKIP_DRAW) == 0) { // this view draws, remove it from the transparent region opaque = super.gatherTransparentRegion(region); } else if (region != null) { int w = getWidth(); int h = getHeight(); if (w>0 && h>0) { getLocationInWindow(mLocation); int l = mLocation[0]; int t = mLocation[1]; region.op(l, t, l+w, t+h, Region.Op.UNION); } } if (PixelFormat.formatHasAlpha(mRequestedFormat)) { opaque = false; } return opaque; } …… }
SurfaceView类的方法gatherTransparentRegion中,先检测是否用作窗口面板以及mPrivateFlags的SKIP_DRAW位是不是1。如果都是,将它所占据的区域从参数region所描述的区域移除,region中剩下的就是透明区域。最后判断Surface的像素格式是否设置有透明值。如果有,返回false给ViewRootImpl,然后ViewRootImpl调用sWindowSession.setTransparentRegion(mWindow, mTransparentRegion);设置窗口为透明。这样就能看到SurfaceView了。
SurfaceView虽然具有独立的Surface,不过它仍然是宿主窗口的视图结构中的一个结点,因此,它仍然是可以参与到宿主窗口的绘制流程中去的。
public class SurfaceView extends View { @Override public void draw(Canvas canvas) { if (mWindowType != WindowManager.LayoutParams.TYPE_APPLICATION_PANEL) { // draw() is not called when SKIP_DRAW is set if ((mPrivateFlags & SKIP_DRAW) == 0) { // punch a whole in the view-hierarchy below us canvas.drawColor(0, PorterDuff.Mode.CLEAR); } } super.draw(canvas); } @Override protected void dispatchDraw(Canvas canvas) { if (mWindowType != WindowManager.LayoutParams.TYPE_APPLICATION_PANEL) { // if SKIP_DRAW is cleared, draw() has already punched a hole if ((mPrivateFlags & SKIP_DRAW) == SKIP_DRAW) { // punch a whole in the view-hierarchy below us canvas.drawColor(0, PorterDuff.Mode.CLEAR); } } // reposition ourselves where the surface is mHaveFrame = true; updateWindow 技术沙龙 教程文章 热点综合