前言
之前分享了普通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的位置,然后再去讨论细节。
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之后至少得等到 下下一帧才会显示。
结合以上分析以及业界认知,对比两者如下:
SurfaceView | TextureView | 备注 | |
---|---|---|---|
内存 | 少 | 多 | |
绘制速度 | 快 | 慢 | |
延迟 | 无延迟 | 至少延迟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方法里面,相关逻辑比较枯燥繁琐,干货较少,这里主要就是记录一下,帮助后续需要的时候回顾。
步骤如下(这里只讲硬件加速的流程):
-
在updateSurface的时候通过 getLocationInWindow 获取Surfaceview在屏幕中的位置。调用updateSurface的时机比较多,但归根结底,最终都是SurfaceView的大小位置发生了改变才会调用。
-
将位置等参数构造成 Transaction,并调用ViewRootImp的applyTransactionOnDraw,触发 scheduleTraversals()等待Vsync的信号。
-
下一帧的Vsync信号来了之后,ViewRootImp会执行 perfromDraw函数,会去注册监听 RenderThread 的draw回调方法。
-
RenderThread在执行DrawFrameTask(不清楚DrawFrameTask的可以去翻阅我上一篇分享)的时候,会回调上一步设置的监听方法。
- 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就代表我们不能往上面绘制内容。
这里直接引用一下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个步骤,大家了解一个大概即可。
假设我们要绘制一个三角形的图片,各个步骤处理如下:
-
向GPU传入数据。
通俗简单的讲,就是传入三角形三个顶点的位置,如果顶点有颜色,这里也要把颜色传入,同理如果有纹理贴图,也要传入。
-
顶点着色(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; };
-
图元装配(Primitive Assembly)
根据上面的三个定点以及绘画方式,决定最终呈现的形状,决定要用屏幕中的哪些点,我们这个例子就是框出来一个三角形。这个阶段是显卡本身自己处理了,我们不用关心细节。大概知道它是干什么的就行。
- 光栅化(Rasterization)
根据上一步获取到的所有点,自动插值计算它的颜色。比如我们上面的栗子,我们给点的只有三个定点的颜色,但是最终画出来的是彩色的,这个就是光栅化自动处理了中间的点,一般都是用重心插值算法,感兴趣的可以自己去搜,这里不做过多的算术推导。
当然,这个阶段也是显卡自身处理了。我们也是知道它大概是干什么的就行
-
片段着色(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
,否则永远获取的是第一帧。
总结
SurfaceView跟TextureView到这算是讲完了,我们在日常的开发中有以下几点的启示:
- 如果要追求流畅度,牺牲一些功耗,透明区域的测量可以从根View上拦截掉,默认使用SurfaceView的大小即可,如果既要流畅度,又要低功耗,那么就尽量少计算Region,对于一些固定展示区域可以只计算一遍,全程复用。
- SurfaceView的黑屏大多数都是因为它的mBackgroundControl控制的layer导致的,如果直接加View盖在它上面会有性能上的损耗,最好是直接改mBackgroundControl的颜色(只支持RGB通道),SurfaceView里有开放设置的方法,这样性能会好一些。
- SurfaceView未引入Vsync机制,存在一个Vsync周期生产多帧的问题,也是性能上的冗余。引入Vsync后,也可以引入三缓冲,进一步提升流畅度。
- SurfaceView使用canvas绘图注意软硬件的api不一样。