Android 系统渲染那些事(二)SurfaceView&TextureView详解

1,075 阅读30分钟

前言

之前分享了普通View的系统渲染上屏流程,感兴趣的同学可以看这篇文章 Android 系统渲染那些事(一)

但在我们抖音开播的场景,最重要的 View 当属 SurfaceView和 TextureView,公司内外也有大量对于这两个View的分享,要么讲的太浅,要么就是直接分析源码,读完收获甚小,很多问题还是找不到答案,所以有必要单独拎出来探讨一下。

这两个 View 都具有在子线程渲染的能力,大量用在音视频、弹幕、相机预览和游戏场景。由于SurfaceView的性能整体上都优于TextureView,谷歌基本上每个大版本都会给它做一些优化,再加上Android7.0的时候补齐了SurfaceView跟随父View平移缩放的动画能力,除非遇到特殊场景,可以毫不犹豫的选择SurfaceView。

目前在抖音的开播侧主要使用 SurfaceView ,站外小窗场景使用TextureView(小窗场景就是这样的特殊case,通过非常规手段也能实现小窗圆角效果,但成本比较高,最终选择了使用TextureView)。

言归正传,本篇分享要讲哪些内容?

首先会从源码上分析SurfaceView跟TextureView,分析两者的实现原理和一些潜在的优化点,同时解释为什么SurfaceView的性能比较好,为什么SurfaceView经常会黑屏,为什么在高版本能展示动画圆角,以及在小窗场景为什么无法展示背景圆角。

其次会介绍一下容易踩坑的点,帮助大家更好的使用SurfaceView。用过SurfaceView的同学应该都知道,这玩意设计的太难用了,一不小心就掉坑里了。

期间会结合抖音开播侧的实践,分享一些常见的 SurfaceView 的优化实践与理论上可能优化的地方,由于个人实力有限,可能有些地方写的不对或者不清楚,欢迎一起讨论。

SurfaceView和TextureView的对比

我们先从整个渲染流程上看一下两个View的位置,然后再去讨论细节。

image.png SurfaceView具有独立的Surface,可以直接将自身的数据渲染到共享缓存中,可能很多人对独立Surface没什么概念,这里简单解释下,一个Surface 对应一个窗口(Window),相当于SurfaceView跟其他View已经不在一个窗口了,甚至基于这个独立的Surface,我们可以重新设计一套View的渲染体系。

而 TextureView 不具有独立的Surface,它先是绘制到自己的缓存中,然后再调用view的invalidate方法触发重绘,在硬件渲染过程中通过纹理ID取出缓存重新绘制到view树上,最终通过 Activity 的 Surface 渲染到共享缓存,从这里可以看出,TextureView在时间跟空间上都逊色于SurfaceView。由于TextureView在渲染完之后需要再次调用 view.invalidate 才能真正渲染,这么操作会导致一个问题,了解Android黄油工程的话就会知道(如果不了解,可以参考这篇文章Android黄油工程详解与实践),invalidate之后至少得等到 下下一帧才会显示

结合以上分析以及业界认知,对比两者如下:

SurfaceViewTextureView备注
内存
绘制速度
延迟无延迟至少延迟1帧很多文章说延迟1-3帧,延迟1帧是肯定有的,但3不知道怎么来的。
耗电量SurfaceView渲染一遍,TextureView渲染了两遍。
动画7.0之前不支持父容器动画,之后版本支持平移缩放动画。支持
硬件可软可硬必须硬件加速的时候使用

接下来从源码层面分析下这两个View。

SurfaceView

对于自定义View 有一个分析技巧就是根据 ViewRootImp 的 traversal 顺序来看,从onMeasure,onLayout、draw 这三个方法顺序看起。虽然SurfaceView得真实绘制不在这个流程里,但它也是View树种的一个节点,也受这三个方法的控制。所以我们会先探索下这三个方法,然后再去探索SurfaceView最核心的独立线程独立Surface渲染。

测量大小(measure)

SurfaceView的测量分为两部分,第一部分是View大小的测量,第二部分是透明区域的测量。之所以要进行透明区域的测量是因为SurfaceView对应的Layer跟View树虽在的Layer不在同一层,两者是有上下级跟遮挡关系的存在,具体的关系通过SurfaceView的mSubLayer控制。mSubLayer通过正常途径(通过反射调用也可以设置为其他值,可以但没必要)只能设置三个值。

APPLICATION_MEDIA_SUBLAYER跟APPLICATION_MEDIA_OVERLAY_SUBLAYER都是在View树的下方,此时如果不额外处理,根本看不到surfaceView,针对这种情况,系统会计算出一个透明区域用来显示SurfaceView,俗称挖洞。

APPLICATION_PANEL_SUBLAYER 这个值代表SurfaceView在View树的上方,此时它会遮盖住View树,理论上这个设置的效率会很高,无需挖洞等操作,但是一旦使用了该值,就无法在上面展示交互层,对于无需交互层的场景,那么可以考虑使用这个值。

APPLICATION_MEDIA_SUBLAYER = -2 
APPLICATION_MEDIA_OVERLAY_SUBLAYER = -1
APPLICATION_PANEL_SUBLAYER = 1

View大小的测量

先看下 SurfaceView的onMeasure方法,测量的方法没有特别的地方,跟一般view相比主要是多了mRequestedWidth和mRequestedHeight,这两个值是通过 SurfaceHolder 的 setFixedSize设置。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int width = mRequestedWidth >= 0
            ? resolveSizeAndState(mRequestedWidth, widthMeasureSpec, 0)
            : getDefaultSize(0, widthMeasureSpec);
    int height = mRequestedHeight >= 0
            ? resolveSizeAndState(mRequestedHeight, heightMeasureSpec, 0)
            : getDefaultSize(0, heightMeasureSpec);
    setMeasuredDimension(width, height);
}
private final SurfaceHolder mSurfaceHolder = new SurfaceHolder() {
    ...
    
    @Override
    public void setFixedSize(int width, int height) {
        if (mRequestedWidth != width || mRequestedHeight != height) {
            ...
            mRequestedWidth = width;
            mRequestedHeight = height;
            
            requestLayout();
            ...
        }
    }
    ...
}

通过官方注释可以看到,该方法会给SurfaceView设置一个固定大小,并且必须在主线程中调用,这是因为内部方法调用了requestLayout(),看过ViewRootImp源码的同学应该都知道,这个方法内部有主线程的检查。

这个方法使用不恰当,会踩坑。

第一个问题是这个方法跟直接设置View的宽高效果一样,但是一旦使用setFixedSize,再通过LayoutParams去设置就不会再改变宽高,这个问题还好,仔细点很快就能发现。

第二个问题是如果LayoutParams设置的是非固定值,这个方法设置的宽高超出了父容器能提供的宽高,就会导致SurfaceView的Surface绘制区域比较大,但是ViewRootImp的Surface给开的洞比较小,最终效果就是画面被裁剪。

这里的建议就是如果想要设置固定尺寸,通过LayoutParams设置就行。

透明区域的测量(挖洞)

在开始详解这里之前,这里先提一个问题,大家可以在后面自行寻找答案。

为什么不直接设置SurfaceView的大小为透明区域的大小,反而使用如此复杂的计算方法?

接下来介绍一下挖洞的过程,整个调用流程我简化如下:

SurfaceView.onAttachedToWindow

->ViewGroup.requestTransparentRegion

->ViewRootImp.requestTransparentRegion

->ViewRootImp.requestLayout

...等待Vsync信号

->ViewRootImp.performTraversals

->ViewGroup.gatherTransparentRegion

  ...遍历整个View树,调用所有View的gatherTransparentRegion

1 从SurfaceView的onAttachedToWindow开始调用父view的requestTransparentRegion,然后再是父view的父view,一直到ViewRootImp里的requestTransparentRegion,设置flag为PFLAG_REQUEST_TRANSPARENT_REGIONS, 最后ViewRootImp会调用requestLayout()

2 待Vsync下一帧的渲染信号到来之后,开始调用doTraversal,根据第一步设置的 Flag 开始调用根View的gatherTransparentRegion。此时会初始化一个透明区域,大小跟根view一致。

3 从根View开始,遍历所有子View,收集所有不透明的区域。最终将要显示的view位置会设置为不透明。

4 根据SurfaceView的大小精确计算region的区域

5 通过 mTransaction 将该区域通知给SufaceFlinger,在合成Layer的时候进行挖洞。

整个挖洞流程到这里算是结束了。但想要挖洞生效,还依赖与SurfaceView的draw方法,后面会详细讲到。

这里需要注意的是,requestTransparentRegion和gatherTransparentRegion不是一一对应的,很多研发会在这里有误解。

只要调用了一次requestTransparentRegion,会设置一个标记位置,后续每一次vsync信号的到来,只要执行了performTraversals,最终都会遍历执行gatherTransparentRegion。尽可能的减少gatherTransparentRegion是优化SurfaceView渲染的一个方向。

这里额外说一下 SurfaceView的gatherTransparentRegion方法,作为该方法的主角,它必须站出来说两句。

public boolean gatherTransparentRegion(Region region) {
    // 这里的判断很简单,如果Surfaceview的layer在顶层或者surface还没初始化完成,就不往下执行
    if (isAboveParent() || !mDrawFinished) {
        return super.gatherTransparentRegion(region);
    }
    
    boolean opaque = true;
    if ((mPrivateFlags & PFLAG_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);
            // otherwise, punch a hole in the whole hierarchy
            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;
}
挖洞总结

根据上面的分析可以看出,挖洞的流程还是比较繁琐耗时的,这里就有了一定的优化空间。

由于开播侧的性能瓶颈不在这里,所以这里我没有自己去实践,不过有些优化是别的团队已经实践过的,有些是理论上可以完成的。

1 不计算Region,默认把SurfaceView的大小当做透明区域。这样做的影响是脏区域扩大,渲染效率提升,功耗少量增加。追求功耗的时候,就放开计算region,追求流畅度就不要计算Region。

2 尽量少计算Region,精细化计算Region,保留上一次更新的结果,后续如果有新的view更新,再增量计算,这样做渲染效率跟功耗会做到平衡,性能达到极致,弊端就是实操起来比较复杂,但理论上是可行的。

不挖洞可以吗?

答案是当然可以。

在看到挖洞逻辑如此复杂之后,我就在思考,能不能不挖洞。挖洞的本质就是把SurfaceView下方的背景设置透明,那么我直接拦截requestTransparentRegion,然后将Activity设置为透明(也可以不用设置Activity,只要将window设置成允许透明即可)不就行了,当然设置为透明会引来其他问题,不在本篇文章的讨论范围。经过线下验证,这种写法完全没有问题,不过没有在线上跑过,感兴趣的同学可以线上尝试一下,看能不能拿到收益。

在SurfaceView的父View中直接拦截requestTransparentRegion,这样就不会挖洞了。

public void requestTransparentRegion(View child) {
    // do nothing
    Log.i("weike","requestTransparentRegion");
}

<style name="Theme.ActivityTransparant" parent="Theme.AppCompat.Light">
    <item name="android:windowBackground">#00000000</item>
</style>

计算位置(layout)

SurfaceView的Layout没有额外处理的逻辑,ViewRootImp 只能控制给Surfaceview挖的洞在哪,但是具体SurfaceView的所在位置是由自己决定的,具体计算位置的代码在SurafaceView的updateSurface方法里面,相关逻辑比较枯燥繁琐,干货较少,这里主要就是记录一下,帮助后续需要的时候回顾。

步骤如下(这里只讲硬件加速的流程):

  1. 在updateSurface的时候通过 getLocationInWindow 获取Surfaceview在屏幕中的位置。调用updateSurface的时机比较多,但归根结底,最终都是SurfaceView的大小位置发生了改变才会调用。

  2. 将位置等参数构造成 Transaction,并调用ViewRootImp的applyTransactionOnDraw,触发 scheduleTraversals()等待Vsync的信号。

  3. 下一帧的Vsync信号来了之后,ViewRootImp会执行 perfromDraw函数,会去注册监听 RenderThread 的draw回调方法。

  4. RenderThread在执行DrawFrameTask(不清楚DrawFrameTask的可以去翻阅我上一篇分享)的时候,会回调上一步设置的监听方法。

    1. ViewRootImp 中执行 mBlastBufferQueue.applyPendingTransactions(frame);
// android/view/SurfaceView.java
protected void updateSurface() {
    ...
    getLocationInWindow(mLocation);
    ...
    mWindowSpaceLeft = mLocation[0];
    mWindowSpaceTop = mLocation[1];
    mSurfaceWidth = myWidth;
    mSurfaceHeight = myHeight;
    ...
    mScreenRect.left = mWindowSpaceLeft;
    mScreenRect.top = mWindowSpaceTop;
    mScreenRect.right = mWindowSpaceLeft + getWidth();
    mScreenRect.bottom = mWindowSpaceTop + getHeight();
    ...
    final boolean realSizeChanged = performSurfaceTransaction(viewRoot, translator,
        creating, sizeChanged, hintChanged, relativeZChanged,
        surfaceUpdateTransaction);
    ...
}
private boolean performSurfaceTransaction(ViewRootImpl viewRoot, Translator translator,
        boolean creating, boolean sizeChanged, boolean hintChanged, boolean relativeZChanged,
        Transaction surfaceUpdateTransaction) {
    ...
    applyTransactionOnVriDraw(surfaceUpdateTransaction);
    ...
 }
 
 private void applyTransactionOnVriDraw(Transaction t) {
    final ViewRootImpl viewRoot = getViewRootImpl();
    if (viewRoot != null) {
        // If we are using BLAST, merge the transaction with the viewroot buffer transaction.
        viewRoot.applyTransactionOnDraw(t);
    } else {
        t.apply();
    }
}

@Override
public boolean applyTransactionOnDraw(@NonNull SurfaceControl.Transaction t) {
    if (mRemoved || !isHardwareEnabled()) {
        t.apply();
    } else {
        // Copy and clear the passed in transaction for thread safety. The new transaction is
        // accessed on the render thread.
        mPendingTransaction.merge(t);
        mHasPendingTransactions = true;
        // Schedule the traversal to ensure there's an attempt to draw a frame and apply the
        // pending transactions. This is also where the registerFrameCallback will be scheduled.
        scheduleTraversals();
    }
    return true;
}

private boolean performDraw() {
    ...
    registerCallbackForPendingTransactions();
    ...
}
private void registerCallbackForPendingTransactions() {
    Transaction t = new Transaction();
    t.merge(mPendingTransaction);

    registerRtFrameCallback(new FrameDrawingCallback() {
        @Override
        public HardwareRenderer.FrameCommitCallback onFrameDraw(int syncResult, long frame) {
            mergeWithNextTransaction(t, frame);
            if ((syncResult
                    & (SYNC_LOST_SURFACE_REWARD_IF_FOUND | SYNC_CONTEXT_IS_STOPPED)) != 0) {
                mBlastBufferQueue.applyPendingTransactions(frame);
                return null;
            }

            return didProduceBuffer -> {
                if (!didProduceBuffer) {
                    mBlastBufferQueue.applyPendingTransactions(frame);
                }
            };

        }

        @Override
        public void onFrameDraw(long frame) {
        }
    });
}
 

具体回调的方法在 RenderThread中,相关代码在 DrawFrameTask.cpp中,这里的详细流程可以参考我上一篇文章。

// frameworks/base/libs/hwui/renderthread/DrawFrameTask.cpp
void DrawFrameTask::run() {
   ...
    std::function<void(int64_t)> callback = std::move(mFrameCallback);
    mFrameCallback = nullptr;
    ...
    if (CC_UNLIKELY(callback)) {
        context->enqueueFrameWork(
                [callback, frameNr = context->getFrameNumber()]() { callback(frameNr); });
    }
}

View树的渲染(draw or dispatchDraw)

众所周知,SurfaceView的渲染在独立线程,但是它本身还是View树的一员,所以通过draw方法还是能绘制的,这里的绘制起到了点睛的作用。

接下来我们详细介绍下draw方法和独立线程的绘制方法。

SurfaceView跟ViewGroup一样有个小优化,在SurfaceView的构造函数里设置了setWillNotDraw(true),如果没有设置background或者手动设置setWillNotDraw(false),父view不会调用SurfaceView的draw方法,仅仅会调用dispatchDraw,从源码里可以看出,draw方法的实现跟dispatchDraw要做的事情是一样的。

之前见到有些写法会给SurfaceView设置background,这种行为会有两个问题,一个是会导致这个优化失效,另一个是会导致SurfaceView的透明区域设置失败。

我们继续分析dispatchDraw的源码,dispatchDraw中的核心方法是clearSurfaceViewPort,这个方法是点睛之笔,我们看看它具体做了哪些事,这里我使用的是Android12的源码,更老一些的版本调用的方法是 canvas.drawColor(0, PorterDuff.Mode.CLEAR),相当于把在SurfaceView区域之前的绘制(可以理解为在SurfaceView下方的View)全部用透明色擦掉,这样就能看到下层的SurfaceView了。这里就有一个潜藏的优化点就是就是SurfaceView下面一定不要放其他View避免无效渲染,无效计算。

Android12针对圆角做了一些处理,同时使用了新的方法canvas.punchHole提高处理圆角的速度,老版本是没有这个api的,这个方法的最终实现在SkiaRecordingCanvas.cpp 类中,对细节感兴趣可以去查看。

public SurfaceView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr,
        int defStyleRes, boolean disableBackgroundLayer) {
    super(context, attrs, defStyleAttr, defStyleRes);
    setWillNotDraw(true);
    mDisableBackgroundLayer = disableBackgroundLayer;
}

@Override
public void draw(Canvas canvas) {
    if (mDrawFinished && !isAboveParent()) {
        // draw() is not called when SKIP_DRAW is set
        if ((mPrivateFlags & PFLAG_SKIP_DRAW) == 0) {
            // punch a whole in the view-hierarchy below us
            clearSurfaceViewPort(canvas);
        }
    }
    super.draw(canvas);
}

@Override
  protected void dispatchDraw(Canvas canvas) {
        if (mDrawFinished && !isAboveParent()) {
            // draw() is not called when SKIP_DRAW is set
            if ((mPrivateFlags & PFLAG_SKIP_DRAW) == PFLAG_SKIP_DRAW) {
                // punch a whole in the view-hierarchy below us
                clearSurfaceViewPort(canvas);
            }
        }
        super.dispatchDraw(canvas);
    }

private void clearSurfaceViewPort(Canvas canvas) {
        if (mCornerRadius > 0f) {
            canvas.getClipBounds(mTmpRect);
            if (mClipSurfaceToBounds && mClipBounds != null) {
                mTmpRect.intersect(mClipBounds);
            }
            canvas.punchHole(
                    mTmpRect.left,
                    mTmpRect.top,
                    mTmpRect.right,
                    mTmpRect.bottom,
                    mCornerRadius,
                    mCornerRadius
            );
        } else {
            canvas.punchHole(0f, 0f, getWidth(), getHeight(), 0f, 0f);
        }
    }
    
  //frameworks/base/libs/hwui/pipeline/skia/SkiaRecordingCanvas.cpp
 void SkiaRecordingCanvas::punchHole(const SkRRect& rect) {
    // Add the marker annotation to allow HWUI to determine where the current
    // clip/transformation should be applied
    SkVector vector = rect.getSimpleRadii();
    float data[2];
    data[0] = vector.x();
    data[1] = vector.y();
    mRecorder.drawAnnotation(rect.rect(), HOLE_PUNCH_ANNOTATION.c_str(),
                             SkData::MakeWithCopy(data, 2 * sizeof(float)));

    // Clear the current rect within the layer itself
    SkPaint paint = SkPaint();
    paint.setColor(0);
    paint.setBlendMode(SkBlendMode::kClear);
    mRecorder.drawRRect(rect, paint);

    mDisplayList->setHasHolePunches(true);
}

独立Surface渲染

这里是SurfaceView最核心的部分,首先SurfaceView的独立渲染依赖于独立的Surface,所以会先讲一下Surface的创建流程,其次会讲SurfaceView的两种画图方式,最后再讲一下Surface的销毁(SurfaceView独特的销毁机制也是很多踩坑的原因)

一种是通过canvas.drawxxx的方法,另外一种是结合openGL接口画图,这一章末尾我会分享一个使用案例。

第一种的优势是软硬渲染皆可以,主要用在纯绘画场景,比如弹幕场景,依托于canvas清晰好用的api,画图会非常的方便。

第二种属于纯硬件渲染,一般用于相机视频流的渲染或者二次加工一般都是使用第二种,鉴于openGL难用的api,上手还是有一定的学习成本,如果要渲染文字,openGL会非常的麻烦。

Surface的创建

上一篇分享讲过ViewRootImp的Surface创建过程,这里再简单回顾一下,ViewRootImp 的 Surface的创建依赖于 SurfaceControl,Java层先是创建了Surface跟SurfaceControl的空壳子,然后在ViewRootImpl.relayoutWindow中创建一个真正的SurfaceControl,然后再创建真正的Surface。

不同于ViewRootImp的是,SurfaceView的Surface创建依赖于BLASTBufferQueue,BLASTBufferQueue上一篇也讲过,它是一个生产者消费者模型,它可以实现内存的跨进程分配、共享和同步,SurfaceView的双缓冲就是它提供的,上面是倒叙了它创建的过程,我们正着全流程走一遍SurfaceView的Surface的创建过程。

跟计算位置的方式一样,Surface的创建也是在 updateSurface 方法中。

1 创建 SurfaceControl

创建SurfaceControl的逻辑也比较复杂,这里也多花费一些笔墨介绍一下。

SurfaceView总共有三个SurfaceControl,分别为mSurfaceControl,mBackgroundControl,mBlastSurfaceControl,这几个跟ViewRootImp的SurfaceControl也是有一定的关系,SurfaceControl存在父子关系,具体关系如下图所示,这里留个开放式问题,为什么SurfaceControl要有父子关系?

我们再回顾一下SurfaceControl的作用,它用来控制Surface的创建、更新、销毁,一个Surface对应一个SurfaceControl,一个SurfaceControl对应一个layer(SurfaceFlinger进程里的Surface),这里需要注意的是,SurfaceControl不一定有应用层的Surface,但是一定会有一个SurfaceFlinger进程中的Surface,是不是有点绕。

这里还是用SurfaceView来举例子,SurfaceView中只有mBlastSurfaceControl具有应用层的Surface,mSurfaceControl跟mBackgroundControl都没有应用层的Surface,没有应用层的Surface就代表我们不能往上面绘制内容。

image.png

这里直接引用一下Google的注释解释各个SurfaceControl的作用,不过这里的描述是有问题的,主要原因是因为注释写完之后,Android13又更新了代码,但是没有更新注释。这里我就不做更正了,读者可以根据上面的父子关系图重新对应即可。

//android/view/SurfaceView.java
protected void updateSurface() {
...
    if (creating) {
        createBlastSurfaceControls(viewRoot, name, surfaceUpdateTransaction);
    }
...
}
private void createBlastSurfaceControls(ViewRootImpl viewRoot, String name,
        Transaction surfaceUpdateTransaction) {
    if (mSurfaceControl == null) {
        mSurfaceControl = new SurfaceControl.Builder(mSurfaceSession)
                .setName(name)
                .setLocalOwnerView(this)
                .setParent(viewRoot.getBoundsLayer())
                .setCallsite("SurfaceView.updateSurface")
                .setContainerLayer()
                .build();
    }

    if (mBlastSurfaceControl == null) {
        mBlastSurfaceControl = new SurfaceControl.Builder(mSurfaceSession)
                .setName(name + "(BLAST)")
                .setLocalOwnerView(this)
                .setParent(mSurfaceControl)
                .setFlags(mSurfaceFlags)
                .setHidden(false)
                .setBLASTLayer()
                .setCallsite("SurfaceView.updateSurface")
                .build();
    } else {
        // update blast layer
        surfaceUpdateTransaction
                .setOpaque(mBlastSurfaceControl, (mSurfaceFlags & SurfaceControl.OPAQUE) != 0)
                .setSecure(mBlastSurfaceControl, (mSurfaceFlags & SurfaceControl.SECURE) != 0)
                .show(mBlastSurfaceControl);
    }

    if (mBackgroundControl == null) {
        mBackgroundControl = new SurfaceControl.Builder(mSurfaceSession)
                .setName("Background for " + name)
                .setLocalOwnerView(this)
                .setOpaque(true)
                .setColorLayer()
                .setParent(mSurfaceControl)
                .setCallsite("SurfaceView.updateSurface")
                .build();
    }

    // Always recreate the IGBP for compatibility. This can be optimized in the future but
    // the behavior change will need to be gated by SDK version.
    if (mBlastBufferQueue != null) {
        mBlastBufferQueue.destroy();
    }
    mTransformHint = viewRoot.getBufferTransformHint();
    mBlastSurfaceControl.setTransformHint(mTransformHint);

    mBlastBufferQueue = new BLASTBufferQueue(name, false /* updateDestinationFrame */);
    mBlastBufferQueue.update(mBlastSurfaceControl, mSurfaceWidth, mSurfaceHeight, mFormat);
    mBlastBufferQueue.setTransactionHangCallback(ViewRootImpl.sTransactionHangCallback);
}

不同类型SurfaceControl的处理源码(没有看源码打算的同学建议跳过,这里主要是指明下源码位置):

// frameworks/native/services/surfaceflinger/SurfaceFlinger.cpp
status_t SurfaceFlinger::createLayer(const String8& name, const sp<Client>& client, uint32_t w,
                                     uint32_t h, PixelFormat format, uint32_t flags,
                                     LayerMetadata metadata, sp<IBinder>* handle,
                                     sp<IGraphicBufferProducer>* gbp,
                                     const sp<IBinder>& parentHandle, int32_t* outLayerId,
                                     const sp<Layer>& parentLayer, uint32_t* outTransformHint) {
    ...
    switch (flags & ISurfaceComposerClient::eFXSurfaceMask) {
        case ISurfaceComposerClient::eFXSurfaceBufferQueue:
        case ISurfaceComposerClient::eFXSurfaceBufferState: {
            result = createBufferStateLayer(client, std::move(uniqueName), w, h, flags,
                                            std::move(metadata), handle, &layer);
            std::atomic<int32_t>* pendingBufferCounter = layer->getPendingBufferCounter();
            if (pendingBufferCounter) {
                std::string counterName = layer->getPendingBufferCounterName();
                mBufferCountTracker.add((*handle)->localBinder(), counterName,
                                        pendingBufferCounter);
            }
        } break;
        case ISurfaceComposerClient::eFXSurfaceEffect:
            // check if buffer size is set for color layer.
            if (w > 0 || h > 0) {
                ALOGE("createLayer() failed, w or h cannot be set for color layer (w=%d, h=%d)",
                      int(w), int(h));
                return BAD_VALUE;
            }

            result = createEffectLayer(client, std::move(uniqueName), w, h, flags,
                                       std::move(metadata), handle, &layer);
            break;
        case ISurfaceComposerClient::eFXSurfaceContainer:
            // check if buffer size is set for container layer.
            if (w > 0 || h > 0) {
                ALOGE("createLayer() failed, w or h cannot be set for container layer (w=%d, h=%d)",
                      int(w), int(h));
                return BAD_VALUE;
            }
            result = createContainerLayer(client, std::move(uniqueName), w, h, flags,
                                          std::move(metadata), handle, &layer);
            break;
        default:
            result = BAD_VALUE;
            break;
    }
    ...
    return result;
}
2 创建 BLASTBufferQueue

上一篇文章讲过,在Android12之前是没有BLASTBufferQueue的,Android12虽然加了BLASTBufferQueue,但是在java层还是存在两套逻辑,一套是通过SurfaceControl创建Surface,另外一套是通过BLASTBufferQueue创建surface,两套的Native层都是创建的BBQSurface。在Android13之后,就直接删除了SurfaceControl创建Surface的方式,统一由BLASTBufferQueue去创建Surface,这样做的好处一个是跟Native层更统一,另外一个好处是java层mBlastSurfaceControl在Surface更新的时候不用重建,只需要重建BLASTBufferQueue即可。

3 创建 Surface

接着由 mBlastBufferQueue 创建 Surface,详细步骤可以看下面的源码,核心代码用绿色背景标注。由于后续版本都没有mSurfaceControl什么事,通过mSurfaceControl创建的逻辑本次就不过了。

//android/view/SurfaceView.java
protected void updateSurface() {
...
copySurface(creating /* surfaceControlCreated */, sizeChanged);
...
}
//android/view/SurfaceView.java  Android12
private void copySurface(boolean surfaceControlCreated, boolean bufferSizeChanged) {
    ...
    if (mUseBlastAdapter) {
          mSurface.copyFrom(mBlastBufferQueue);
    } else {
          mSurface.copyFrom(mSurfaceControl);
    }
    ...
}
//android/view/SurfaceView.java  Android13-15
private void copySurface(boolean surfaceControlCreated, boolean bufferSizeChanged) {
    ...
    mSurface.copyFrom(mBlastBufferQueue);
    ...
}

//frameworks/base/core/jni/android_view_Surface.cpp
static jlong nativeGetFromBlastBufferQueue(JNIEnv* env, jclass clazz, jlong nativeObject,
                                           jlong blastBufferQueueNativeObj) {
    Surface* self(reinterpret_cast<Surface*>(nativeObject));
    sp<BLASTBufferQueue> queue = reinterpret_cast<BLASTBufferQueue*>(blastBufferQueueNativeObj);
    const sp<IGraphicBufferProducer>& bufferProducer = queue->getIGraphicBufferProducer();
    // If the underlying IGBP's are the same, we don't need to do anything.
    if (self != nullptr &&
        IInterface::asBinder(self->getIGraphicBufferProducer()) ==
                IInterface::asBinder(bufferProducer)) {
        return nativeObject;
    }

    sp<Surface> surface = queue->getSurface(true /* includeSurfaceControlHandle */);
    if (surface != NULL) {
        surface->incStrong(&sRefBaseOwner);
    }

    return reinterpret_cast<jlong>(surface.get());
}
//frameworks/native/libs/gui/BLASTBufferQueue.cpp
sp<Surface> BLASTBufferQueue::getSurface(bool includeSurfaceControlHandle) {
    std::unique_lock _lock{mMutex};
    sp<IBinder> scHandle = nullptr;
    if (includeSurfaceControlHandle && mSurfaceControl) {
        scHandle = mSurfaceControl->getHandle();
    }
    return new BBQSurface(mProducer, true, scHandle, this);
}
4 回调SurfaceHolder

应用层不建议直接操作Surface的,需要通过SurfaceHolder去完成,SurfaceHolder对象SurfaceView已经帮我们创建好了,我们直接通过surfaceView.getSurfaceHolder()去获取即可。通过SurfaceHolder我们可以设置callback给SurfaceView,用来监听SurfaceView的生命周期。只有在接受到surfaceCreated的时候我们才可以去绘制。

public interface Callback {

void surfaceCreated(@NonNull SurfaceHolder holder);
    
void surfaceChanged(@NonNull SurfaceHolder holder, @PixelFormat.Format int format,
            @IntRange(from = 0) int width, @IntRange(from = 0) int height);

void surfaceDestroyed(@NonNull SurfaceHolder holder);
}
Canvas 画图

Surface创建好之后,我们就可以在上面画图了,画图分为三步:

第一步lockcanva,从surface中获取绘制缓冲区

第二步canvas.drawxxx,在缓冲区上绘制。

第三步unlockCanvasAndPost,将绘制的缓冲区提交给SurfaceFlinger合成。

接下来重点介绍下lockcanva跟unlockCanvasAndPost。

lockCanva

上一篇文章讲到过,软硬件渲染的区别就在于使用不同的canvas,SurfaceVIew也不例外,如果要使用软件渲染,就调用lockCanvas,硬件渲染那就调用lockHardwareCanvas,所谓的lockxxxCanvas,最终都是为了锁定一块缓存,SurfaceView一般是有两块缓存,这就是所谓的双缓冲,一块用于应用层的渲染,一块用于SurfaceFlinger合成渲染。

这里以软件渲染的源码为例进行分析,lockCanvas最终会调用到ndroid_view_Surface.cpp的 nativeLockCanvas 方法,通过surface->lock方法去获取回执缓冲区(最终还是通过BLASTBufferQueue去获取),然后再设置给canvas。

//frameworks/base/core/jni/android_view_Surface.cpp
static jlong nativeLockCanvas(JNIEnv* env, jclass clazz,
        jlong nativeObject, jobject canvasObj, jobject dirtyRectObj) {
    sp<Surface> surface(reinterpret_cast<Surface *>(nativeObject));
    ...
    ANativeWindow_Buffer buffer;
    status_t err = surface->lock(&buffer, dirtyRectPtr);
    ...
    graphics::Canvas canvas(env, canvasObj);
    canvas.setBuffer(&buffer, static_cast<int32_t>(surface->getBuffersDataSpace()));
    ...
    sp<Surface> lockedSurface(surface);
    lockedSurface->incStrong(&sRefBaseOwner);
    return (jlong) lockedSurface.get();
}

//frameworks/base/libs/hwui/apex/android_canvas.cpp
bool ACanvas_setBuffer(ACanvas* canvas, const ANativeWindow_Buffer* buffer,
                       int32_t /*android_dataspace_t*/ dataspace) {
    SkBitmap bitmap;
    bool isValidBuffer = (buffer == nullptr) ? false : convert(buffer, dataspace, &bitmap);
    TypeCast::toCanvas(canvas)->setBitmap(bitmap);
    return isValidBuffer;
}
unlockCanvasAndPost

unlockCanvasAndPost 会调用android_view_Surface.cpp的nativeUnlockCanvasAndPost方法,该方法会调用surface.unlockAndPost,看到queueBuffer,大家就应该很清楚unlockAndPost具体要干什么了(不清楚的话可以看上一篇文章)。

//frameworks/base/core/jni/android_view_Surface.cpp
static void nativeUnlockCanvasAndPost(JNIEnv* env, jclass clazz,
        jlong nativeObject, jobject canvasObj) {
    sp<Surface> surface(reinterpret_cast<Surface *>(nativeObject));
    if (!isSurfaceValid(surface)) {
        return;
    }

    // detach the canvas from the surface
    graphics::Canvas canvas(env, canvasObj);
    canvas.setBuffer(nullptr, ADATASPACE_UNKNOWN);

    // unlock surface
    status_t err = surface->unlockAndPost();
    if (err < 0) {
        jniThrowException(env, IllegalArgumentException, NULL);
    }
}

// frameworks/native/libs/gui/Surface.cpp
status_t Surface::unlockAndPost()
{
    if (mLockedBuffer == nullptr) {
        ALOGE("Surface::unlockAndPost failed, no locked buffer");
        return INVALID_OPERATION;
    }

    int fd = -1;
    status_t err = mLockedBuffer->unlockAsync(&fd);
    ALOGE_IF(err, "failed unlocking buffer (%p)", mLockedBuffer->handle);

    err = queueBuffer(mLockedBuffer.get(), fd);
    ALOGE_IF(err, "queueBuffer (handle=%p) failed (%s)",
            mLockedBuffer->handle, strerror(-err));

    mPostedBuffer = mLockedBuffer;
    mLockedBuffer = nullptr;
    return err;
}
OpenGL接口画图

openGL画图不依赖于Surface,只要gl环境创建好,随便画,我们最终只需要把GPU渲染的数据交给对应的消费者即可 ,所以SurfaceView跟TextureView都可以使用openGL去画图。虽然大家都是通过GPU渲染,但是SurfaceView相比TextureView具有很大的优势,那就是GPU渲染的数据可以直接交换到Surface的绘制缓冲区,这样就少了数据的拷贝跟二次传递,时间空间上都有巨大的节省。

下面是一段线上老代码(现在已经不用了),大家可以看看存在什么性能问题。

public void onFrame() {
    if (this.mCanvasThread != null) {
        ...
        GLES20.glBindFramebuffer(36160, this.mFB);
        GLES20.glFramebufferTexture2D(36160, 36064, buffer.getType().getGlTarget(), buffer.getTextureId(), 0);
        ByteBuffer mBuffer = ByteBuffer.allocateDirect(buffer.getWidth() * buffer.getHeight() * 4);
        GLES20.glReadPixels(0, 0, buffer.getWidth(), buffer.getHeight(), 6408, 5121, mBuffer);
        ...
        GLES20.glBindFramebuffer(36160, 0);
        final Bitmap bitmap = Bitmap.createBitmap(buffer.getWidth(), buffer.getHeight(), Config.ARGB_8888);
        bitmap.copyPixelsFromBuffer(mBuffer);
        post(new Runnable() {
            public void run() {
                onCanvasDrawFrame(bitmap);
            }
        });
    }

}

private void onCanvasDrawFrame(Bitmap bitmap) {
    if (this.mSurfaceHolder != null) {
        Canvas canvas = this.mSurfaceHolder.lockCanvas();
        ...
        canvas.drawBitmap(bitmap, this.mMatrix, (Paint)null);
        ...
        this.mSurfaceHolder.unlockCanvasAndPost(canvas);
    }
}
GLSurfaceView

GLSurfaceView 继承自SurfaceView,实际上就是一个通过EGL初始化了GL环境的SurfaceView。为了方便开发者的使用,内部初始化了一些GL相关的上下文以及二次封装了SurfaceView,更适合游戏类场景的使用。

这里简单的啰嗦一下openGL画图的过程,普及一下基础知识,初学者看起来可能会懵,建议跳过,后面有机会我会再出一篇分享详细讲openGL跟GPU相关的设计。

openGL画图整体上可以简化为以下5个步骤,大家了解一个大概即可。

假设我们要绘制一个三角形的图片,各个步骤处理如下:

  1. 向GPU传入数据。

    通俗简单的讲,就是传入三角形三个顶点的位置,如果顶点有颜色,这里也要把颜色传入,同理如果有纹理贴图,也要传入。

  2. 顶点着色(Vertex Shading)

    这一步是可以进行编程的。 二次加工传入的三个点,在这里可以改变三个点的位置,变换出更多的形状,有几个顶点就会调用几次。下面是官方给的架构图(这个是gles2.0的架构,3.0有一些语法上的调整)。

       attribute vec4 vPosition; 
       uniform mat4 vMatrix;
       varying  vec4 vColor;
       attribute vec4 aColor;
    
       void main() {
           gl_Position = vMatrix*vPosition;
           vColor=aColor;
       };
    
    
  3. 图元装配(Primitive Assembly)

根据上面的三个定点以及绘画方式,决定最终呈现的形状,决定要用屏幕中的哪些点,我们这个例子就是框出来一个三角形。这个阶段是显卡本身自己处理了,我们不用关心细节。大概知道它是干什么的就行。

  1. 光栅化(Rasterization)

根据上一步获取到的所有点,自动插值计算它的颜色。比如我们上面的栗子,我们给点的只有三个定点的颜色,但是最终画出来的是彩色的,这个就是光栅化自动处理了中间的点,一般都是用重心插值算法,感兴趣的可以自己去搜,这里不做过多的算术推导。

当然,这个阶段也是显卡自身处理了。我们也是知道它大概是干什么的就行

  1. 片段着色(Fragment Shading)

    这一步也是可以编程的,每一个光栅化后的点都会调用一次这个方法,并将光栅化后的颜色值传给片段着色器,在这里我们可以给它赋予更多的颜色。所以这个方法的性能非常的重要。

      precision highp float;
      varying vec4 vColor;
    
      void main(){
          gl_FragColor=vColor;
      }
      ```
    
Surface的销毁

SurfaceView有自己的生命周期,如果不知道它的销毁时机,就会掉到各种坑里,比如SurfaceView又黑屏了等问题。

SurfaceView的生命周期可以自己设置,有两种模式,默认是第一种,只要SurfaceView的不可见,就会销毁Surface,在一些业务场景下, 可能需要临时性的设置SurfaceView不可见,等设置View.Visiable的时候,就会出现闪一下黑屏或者是一直黑屏的问题。这时候可以考虑下重新设置生命周期为SURFACE_LIFECYCLE_FOLLOWS_ATTACHMENT。

 /**
* The Surface lifecycle is tied to SurfaceView visibility.
*
* The Surface is created when the SurfaceView becomes visible, and is destroyed when the
* SurfaceView is no longer visible.
*/
public static final int SURFACE_LIFECYCLE_FOLLOWS_VISIBILITY = 1;

/**
* The Surface lifecycle is tied to SurfaceView attachment.
* The Surface is created when the SurfaceView first becomes attached, but is not destroyed
* until this SurfaceView has been detached from the current window.
*/
public static final int SURFACE_LIFECYCLE_FOLLOWS_ATTACHMENT = 2;

高版本特性

Android5.0之前,如果给SurfaceView的父View设置圆角是不会生效的,Android7.0之前,SurfaceView无法跟随父View的动画,所以在很多场景下,不得不选用TextureView,我们来看看谷歌是怎么解决这些问题的。

渲染圆角

对于普通View,我们只需要在父View上调用path.addRoundRect,然后再调用canvas.clipPath即可实现对子View的圆角效果,但是SurfaceView不行,就算是往SurfaceView上画带圆角的图也不行,会有黑色的边。

SurfaceView的圆角只能改变透明区域才能实现,改变透明区域有两种方法,

一种是SurfaceView自身的setCornerRadius方法,该方法是Android10.0引入的(翻了好几个版本的源码确认无误)。

另外一种方法是通过ViewOutlineProvider,该方法实际上也是通过影响SurfaceView的draw方法,让其在挖洞的时候保留圆角。在Android5.0的时候,系统增加了ViewOutlineProvider这个类,可以很轻松的给SurfaceView设置圆角,当然,针对普通View也可以。这样就省了我们自己再去复写父类,这就是我们软件研发中经常提到的依赖导致原则,非常的实用。ViewOutlineProvider的使用方式如下:

view?.apply {
setOutlineProvider(object : ViewOutlineProvider() {
        override fun getOutline(view: View?, outline: Outline?) {
            val rect = Rect(
                0, 0,
                ResUtil.dp2Px(xxx),
                ResUtil.dp2Px(xxx)
            )

            outline?.setRoundRect(
                rect,
                ResUtil.dp2Px(BroadcastVideoFloatWindowManager.VIEW_ROUND_CORNERS)
                    .toFloat()
            )
        }
    })
    setClipToOutline(true)
} 

接下来我们详细探讨下ViewOutlineProvider这个类,在设置外形的时候,会逐步调用到mRenderNode.setOutline(outline); RenderNode是硬件渲染里重要的概念,也就是说这个方法也是强依赖于硬件渲染,在该View的RenderNode保存了圆角等信息,

public void setOutlineProvider(ViewOutlineProvider provider) {
    mOutlineProvider = provider;
    invalidateOutline();
}

public void invalidateOutline() {
    rebuildOutline();
    ...
}
private void rebuildOutline() {
    final Outline outline = mAttachInfo.mTmpOutline;
    ...
    mOutlineProvider.getOutline(this, outline);
    mRenderNode.setOutline(outline);    
}

public boolean setOutline(@Nullable Outline outline) {
    ...
    switch (outline.mMode) {
        case Outline.MODE_EMPTY:
            return nSetOutlineEmpty(mNativeRenderNode);
        case Outline.MODE_ROUND_RECT:
            return nSetOutlineRoundRect(mNativeRenderNode,
                    outline.mRect.left, outline.mRect.top,
                    outline.mRect.right, outline.mRect.bottom,
                    outline.mRadius, outline.mAlpha);
        case Outline.MODE_PATH:
            return nSetOutlinePath(mNativeRenderNode, outline.mPath.mNativePath,
                    outline.mAlpha);
    }
}

static jboolean android_view_RenderNode_setOutlineRoundRect(CRITICAL_JNI_PARAMS_COMMA jlong renderNodePtr,
        jint left, jint top, jint right, jint bottom, jfloat radius, jfloat alpha) {
    RenderNode* renderNode = reinterpret_cast<RenderNode*>(renderNodePtr);
    renderNode->mutateStagingProperties().mutableOutline().setRoundRect(left, top, right, bottom,
            radius, alpha);
    renderNode->setPropertyFieldsDirty(RenderNode::GENERIC);
    return true;
}

void setRoundRect(int left, int top, int right, int bottom, float radius, float alpha) {
        ...

        // update mPath to reflect new outline
        if (MathUtils::isPositive(radius)) {
            mPath.addRoundRect(SkRect::MakeLTRB(left, top, right, bottom), radius, radius);
        } else {
            mPath.addRect(left, top, right, bottom);
        }
    }
}  

接下来我们再看一下渲染Outline的地方,上一篇文章提到过,最终渲染在RenderNodeDrawable的forceDraw里,我们可以直奔这里的代码看看细节。如果ViewOutlineProvider设置在SurfaceView上,就会走drawContent方法,如果是SurfaceView的父View就会走drawBackwardsProjectedNodes,同时在执行这行代码之前,限定死了操作在圆角范围内,这样做完之后,就会保证SurfaceView的canvas.punchHole或者canvas.drawColor(0, PorterDuff.Mode.CLEAR)不会挖一个长方形的洞,而是挖一个带圆角的洞。

void RenderNodeDrawable::forceDraw(SkCanvas* canvas) const {
    ...
    SkiaDisplayList* displayList = renderNode->getDisplayList().asSkiaDl();
    SkAutoCanvasRestore acr(canvas, true);
    const RenderProperties& properties = this->getNodeProperties();
    // pass this outline to the children that may clip backward projected nodes
    displayList->mProjectedOutline =
            displayList->containsProjectionReceiver() ? &properties.getOutline() : nullptr;
    if (!properties.getProjectBackwards()) {
        drawContent(canvas);
        if (mProjectedDisplayList) {
            acr.restore();  // draw projected children using parent matrix
            LOG_ALWAYS_FATAL_IF(!mProjectedDisplayList->mProjectedOutline);
            const bool shouldClip = mProjectedDisplayList->mProjectedOutline->getPath();
            SkAutoCanvasRestore acr2(canvas, shouldClip);
            canvas->setMatrix(mProjectedDisplayList->mParentMatrix);
            if (shouldClip) {
                canvas->clipPath(*mProjectedDisplayList->mProjectedOutline->getPath());
            }
            drawBackwardsProjectedNodes(canvas, *mProjectedDisplayList);
        }
    }
    displayList->mProjectedOutline = nullptr;
}
为什么小窗场景不能显示圆角

在小窗场景,圆角的渲染流程跟上一节一模一样,那么为什么在小窗上圆角就不会生效?

从前几章的学习中我们可以知道surfaceView的展示是通过SurfaceView的diaptchDraw或者draw方法挖洞显示出来的。在小窗场景,圆角倒是也能展示出来,但是背景是我们应用的背景,SurfaceView跟小窗是在其他应用的上方,圆角的外部是需要显示其他应用的背景。

那么有同学可能要问了,把我们的背景设置透明不就可以了?如果把我们的背景设置透明,因为SurfaceView的Surface在它的下面,独立Surface就完美的展示出来,圆角就没了。

那么聪明的宝宝又要问了?我们把SurfaceView的独立Surface裁剪一个圆角出来,由于每一帧都要计算,性能暂且不谈,SurfaceView还有一个纯黑的backgroundLayer,必须用黑科技把这个backgroundLayer设置透明才可以。

所以开播侧最终还是选用了TextureView。

渲染动画

SurfaceView跟随父View做动画的实现是从Android7.0开始加入的,目前只支持平移跟缩放,旋转是不支持的。

这里我就不详细介绍了,大家直接看 SurfaceViewPositionUpdateListener 即可。

private class SurfaceViewPositionUpdateListener implements RenderNode.PositionUpdateListener {
    ...
    @Override
    public void positionChanged(long frameNumber, int left, int top, int right, int bottom) {
        try {
            if (DEBUG_POSITION) {
                Log.d(TAG, String.format(
                        "%d updateSurfacePosition RenderWorker, frameNr = %d, "
                                + "position = [%d, %d, %d, %d] surfaceSize = %dx%d",
                        System.identityHashCode(SurfaceView.this), frameNumber,
                        left, top, right, bottom, mRtSurfaceWidth, mRtSurfaceHeight));
            }
            synchronized (mSurfaceControlLock) {
                if (mSurfaceControl == null) return;

                mRTLastReportedPosition.set(left, top, right, bottom);
                onSetSurfacePositionAndScale(mPositionChangedTransaction, mSurfaceControl,
                        mRTLastReportedPosition.left /*positionLeft*/,
                        mRTLastReportedPosition.top /*positionTop*/,
                        mRTLastReportedPosition.width()
                                / (float) mRtSurfaceWidth /*postScaleX*/,
                        mRTLastReportedPosition.height()
                                / (float) mRtSurfaceHeight /*postScaleY*/);

                mPositionChangedTransaction.show(mSurfaceControl);
            }
            applyOrMergeTransaction(mPositionChangedTransaction, frameNumber);
        } catch (Exception ex) {
            Log.e(TAG, "Exception from repositionChild", ex);
        }
    }
    ...

}

TextureView

不同于SurfaceView,TextureView必须在硬件加速场景下使用,好奇宝宝可能就要问了,为什么只能在硬件加速场景下使用呢?

主要原因是TextureView 为了提升性能,大量使用了跟硬件加速相关的特性。

次要原因是谷歌压根就没有考虑软件渲染,这里其实适配下软件渲染成本也不会太高,但是性能会比较差,估计TextureView的研发心里想的是,都使用软件渲染了,你不塞车谁塞车,老老实实用SurfaceView去吧。

由于TextureView 使用的场景越来越少,本次主要讲一讲它使用到一些通用技术SurfaceTexture,在音视频相机等场景,SurfaceTexture的使用频次非常的高,在我们的开播场景,为了提升相机的采集传输效率,也使用了SurfaceTexture,开播的相机也积累了很多优化经验,后续有时间我会单独以相机为主题做一篇分享。

SurfaceTexture

SurfaceTexture是什么?这里我直接引用官方的解释:

SurfaceTexture 是 Surface 和 OpenGL ES (GLES) 纹理的组合。SurfaceTexture 实例用于提供输出到 GLES 纹理的接口。

SurfaceTexture 包含一个以应用为使用方的 BufferQueue 实例。当生产方将新的缓冲区排入队列时,onFrameAvailable() 回调会通知应用。然后,应用调用 updateTexImage()。此时会释放先前占用的缓冲区,从队列中获取新缓冲区并执行 EGL 调用,从而使 GLES 可将此缓冲区作为外部纹理使用。

SurfaceTexture不仅仅可以用于TextureView,它可以用于任何硬件渲染的场景,甚至是不需要任何View,进行离屏渲染。

下图展示了SurfaceTexture的工作流程,本质上是一个生产者消费者模型,一般会将SurfaceTexture包装成一个特殊的Surface,Surface大家应该很清楚了,核心在于渲染缓存,这个特殊的Surface的缓存由SurfaceTexture提供。

数据生产者(相机、视频)将图像数据写入SurfaceTexture的缓冲,并转换成GLES的外部纹理,然后通知消费者有新的数据, 消费者通过纹理ID获取缓存数据,然后通过GLES再渲染出来。注意这里一定要调用updateTexImage,否则永远获取的是第一帧。

image.png

总结

SurfaceView跟TextureView到这算是讲完了,我们在日常的开发中有以下几点的启示:

  • 如果要追求流畅度,牺牲一些功耗,透明区域的测量可以从根View上拦截掉,默认使用SurfaceView的大小即可,如果既要流畅度,又要低功耗,那么就尽量少计算Region,对于一些固定展示区域可以只计算一遍,全程复用。
  • SurfaceView的黑屏大多数都是因为它的mBackgroundControl控制的layer导致的,如果直接加View盖在它上面会有性能上的损耗,最好是直接改mBackgroundControl的颜色(只支持RGB通道),SurfaceView里有开放设置的方法,这样性能会好一些。
  • SurfaceView未引入Vsync机制,存在一个Vsync周期生产多帧的问题,也是性能上的冗余。引入Vsync后,也可以引入三缓冲,进一步提升流畅度。
  • SurfaceView使用canvas绘图注意软硬件的api不一样。