[Flutter]从Flutter来看TextureView和SurfaceView

1,693 阅读18分钟

从Flutter来看TextureView和SurfaceView

在这之前你可能已经听说过TextureView和SurfaceView这两个专用于外部绘制的视图View,并且在一些文章中也读到过它们的一些特点,概括下来大致上是:

  1. SurfaceView会启用一块独立的Surface进行绘制,因此它会独立于ViewHierachy,导致它无法和其他的View一致进行统一的View旋转动画操作,但是具有更高的性能。特别地,其余的普通的ViewHierachy上的视图会归属于一块统一的Surface上,这块Surface将会由主线程进行绘制,Render线程进行渲染。
  2. TextureView解决了SurfaceView的一些问题,TextureView会将独立线程绘制的纹理数据和ViewHierachy对应的主线程Surface进行整合,并且统一绘制在最终的画面当中,因此它不具有独立的Surface,它会兼容普通的View视图动画操作,但是由于多了这么个步骤,导致性能有一定的损失,对于ViewHierachy来说,它具有更高的兼容性。

whiteboard_exported_image-2.png

我们可以看到,TextureView会将生产者产生的图像绘制指令统一交给主线程对应的渲染线程进行渲染,这里会接住View#draw方法,通过布置一个drawTextureLayer任务来在主线程完成渲染。 这样就可以像一个普通View那样去融入ViewHierachy了,通俗一点说,TextureView就是一个图像生产者(解码器、相机等等)在ViewHierachy中的消费者,它消费图像数据的方式就是将数据重新「画」到Canvas上。

它比SurfaceView多了一个步骤,在图像数据的生产者产生图像数据后,而将图像数据交给主线程统一去渲染,而不是由自己的渲染线程去处理。

但是,SurfaceView和TextureView的图像数据生产者都可以是独立的,所以在一些场景下这二者其实是通用的,对于一些复杂视图的业务场景来说,TextureView显然泛用性更好;而SurfaceView的泛用性显然就差了一些,SurfaceView和其他View不在同一个渲染线程上渲染,导致彼此之间无法正常地去联动。

基于这个特点,我们在使用Flutter时,官方其实也提供了两种可选的渲染模式,可以在FlutterActivity及其子类或者其他和FlutterActivityAndFragmentDelegate.Host的实现类中,通过重写getRenderMode来指定你需要指定的RenderMode:

override fun getRenderMode(): RenderMode {
    return super.getRenderMode()
}

基于以上的特点,我们一般会在全屏都是Flutter视图的页面使用SurfaceView来渲染Flutter视图,因为它的兼容性更好;而在复杂层级、动画场景下的FlutterFragment、FlutterView这一类嵌入原生页面的场景下,去使用TextureView模式完成渲染。

一、Flutter与TextureView

TextureView可以有独立的绘制线程(注意,虽然TextureView的内容产生可以在其他线程中,但是和我们平常讲的View#draw方法的调用应该分开,TextureView的draw方法依然发生在主线程) ,它会和主线程共用渲染线程,我们可以将FlutterActivity的渲染模式设置为Texture模式,然后查看宿主View的相关情况,我们可以看到它是使用FlutterTextureView进行渲染的:

我们可以直接在AndroidStudio中,双击shift检索到FlutterTexutreView.java的源代码,我们可以看到它是TextureView的子View,并且实现了RenderSurface接口。

根据我们前面的描述,TextureView具有两个特点:

  1. 接收来自视图数据生产者的输出;
  2. 通过draw方法绘制到View上;

其一就对应这RenderSurface接口,它也是FlutterEmbedder层的一个接口,这个接口的定义注释机翻大致如下:

拥有FlutterRenderer想要绘制的Surface。

RenderSurface负责在请求时向给定的FlutterRenderer提供一个Surface,然后在Surface更改或损坏时通知FlutterRenderor。

提供Surface的行为委托给此接口,因为Surface可用性的时间由Android决定。因此,访问器方法无法满足要求。因此,为RenderSurface提供了一个FlutterRenderer,当Surface变得可用、更改或被销毁时,RenderSurface会通知FlutterRenderer。

显然,这里的FlutterRenderer就是和图像的生产者有很大关系的一个结构,进入它的代码内容,很多都是通过flutterJNI来对纹理数据进行操作的代码。

FlutterRenderer以及其他相关的结构,会完成视图数据的绘制,得到的图像数据最终会存储到SurfaceTexture当中,而draw方法发生时,会从当前的SurfaceTexture中取出FlutterRenderer绘制的最新的一帧输出到屏幕上。

TextureView的源代码:

aospxref.com/android-10.…

可以看到TextureView对于draw方法的处理:

先拿到Canvas,然后通过Canvas.drawTextureLayer(layer)方法,这一步实际上就完成了图像数据从生产者到消费者两侧的数据传递,drawTextureLayer最终也是直接通过native方法完成的渲染。

TextureLayer:TextureLayer表示一个Surface纹理,当在硬件加速的画布中绘制时,RenderThread会将其合成到帧中。这是由本机端的DeferredLayerUpdater支持的。

RecordingCanvas:一种Canvas实现,用于记录视图系统绘图操作以进行延迟渲染。它与RenderNode结合使用,此类保留了它绘制的所有Paint和Bitmap对象的列表(省略n个字,具体内容直接去Android Studio中看源码)。

Native通过拿到TextureView提供的自身的SurfaceTexture,而其中包含了当前帧的数据,由此完成图像合成和渲染,这就是TextureView的绘制机制,我们知道draw方法最终是发生在主线程的,所以绘制最终的渲染和绘制也会发生在主线程,正是因为如此,TextureView完全依附于ViewHierachy,因此它具有更高的视图兼容性,它的Measure/Layout/Draw方法和普通View一致,遵循View的绘制机制,只不过Draw方法绘制其实是视图生产者在ViewHierachy中的代理而已,它绘制的内容,其实是图像生产者的数据内容。

二、Flutter与SurfaceView、ANativeWindow

作为一种FlutterActivity默认的实现,我们不对FlutterActivity做任何修改,并且背景色不为透明的情况下,Flutter默认采用的就是SurfaceView来完成渲染:

FlutterSurfaceView实现了RenderSurface接口,并且直接继承自SurfaceView,和FlutterTextureView的结构非常相似,我们可以在attachToRenderer方法的connectSurfaceToRenderer调用中,看到FlutterSurfaceView将一个surface传递到了native一侧:

接下来的步骤其实都在native侧完成了,因为在native侧拿到独立的渲染Surface之后,native可以直接拿到Surface的引用,完成对Surface的直接绘制,而不再需要经过JVM。而Flutter渲染的最终图像数据也直接在Native侧可以拿到,显然不会再经过JVM层了,因此我们需要再去flutter/engine仓库中找找nativeSurfaceCreated的源代码:

platform_view_android_jni_impl.cc line.227 # SurfaceCreated:

static void SurfaceCreated(JNIEnv* env,
                           jobject jcaller,
                           jlong shell_holder,
                         jobject jsurface) {
  fml::jni::ScopedJavaLocalFrame scoped_local_reference_frame(env);
  auto window = fml::MakeRefCounted<AndroidNativeWindow>(
      ANativeWindow_fromSurface (env, jsurface)); 
  ANDROID_SHELL_HOLDER->GetPlatformView()->NotifyCreated(std::move(window));
}

这里的核心其实是ANativeWindow这个对象,前面我们提到:

Native可以直接拿到Surface的引用,完成对Surface的直接绘制,而不再需要经过JVM。

ANativeWindow就是做这件事情的,我们只需要从JVM层拿到Surface的引用,即可向它绘制颜色,一个比较简单的使用如下:

void SurfacePlayer::drawInner(JNIEnv *env,jobject surface) {
    // 1
    ANativeWindow *aNativeWindow = ANativeWindow_fromSurface (env, surface); 
    if (aNativeWindow == NULL) {
        logE("ANativeWindow::is Null");
        return;
    }

    // 2
    ANativeWindow_Buffer newBuffer; 
    logE("ANativeWindow_fromSurface...");

    // 3
    if (0 != ANativeWindow_lock (aNativeWindow, &newBuffer, 0 ) ) {
        logE("ANativeWindow::Lock Error");
        return;
    }

    int randomNumber = rand() % 0xffffffff;

    // ::fill color
    if (newBuffer.format == WINDOW_FORMAT_RGBA_8888) {
        logE("nwBuffer->format == WINDOW_FORMAT_RGBA_8888");
        logE("nwBuffer->height:%d", newBuffer.height);
        logE("nwBuffer->width:%d", newBuffer.width);
        for (int i = 0; i < newBuffer.height * newBuffer.width; i++) {
            *((int *) newBuffer.bits + i) = randomNumber;
        }
    }

    // 4
    if (0 != ANativeWindow_unlockAndPost (aNativeWindow) ) {
        logE("ANativeWindow_unlockAndPost error");
        return;
    }

    // 5
//    ANativeWindow_release(instance); 

}

在这个例子中,直接操作ANativeWindow的代码有五处:

第一处通过ANativeWindow_fromSurface(env, surface); 从JVM层传递过来的Surface对象中获取到了aNativeWindow实例,这个实例一般来说如果你要保持绘制,那么就一直持有着,如果不需要,则需要在本次绘制完成之后调用第五处的ANativeWindow_release(instance); 将获得的实例释放掉。

第二、三处则声明了一个Buffer,这个Buffer的赋值、尺寸信息的确定将会在稍后的ANativeWindow_lock调用中实现。一般来说,它的尺寸就是SurfaceView的宽、高。而lock方法之后,锁住了缓冲区即可向内部写入颜色数据,这里采用randomNumber来获取随机的颜色数据, 然后用一个大循环,往缓冲区的每一个位置都写入生成的颜色。

第四处则通过调用ANativeWindow_unlockAndPost来解锁缓冲区,并且提交所需要绘制的数据。后续步骤中,此前填充颜色的缓冲区最终就将会绘制到Surface中,最终通过硬件合成,和其他Surface合并后展示到屏幕上。

显然,把填充颜色的步骤替换为FlutterUI渲染的步骤,那么最终显示到屏幕上的数据就会是我们的Flutter渲染之后得到的视图数据了。

三、SurfaceView在ViewHierachy中的兼容性问题

如果我们想要比较直观地去理解这件事情,那么写一个Demo想必是最简单有效的手段,但是要搭建一个能够渲染TextureView或者SurfaceView数据的生产者,我们不得不去调用CameraX Api、OpenGL绘制或者是MediaCodec这一类的编解码器来产生图像数据,这就加大了写Demo的门槛,而FlutterFragment,则可以非常方便地切换两种渲染模式:

override fun initView() {
    val ts = supportFragmentManager.beginTransaction()
    ts.add(
        viewBinding.flContainer.id, FlutterFragment.withNewEngine()
            .renderMode(RenderMode.surface).build()
    )
    ts.commit()
}

在外部设置一个24dp的Padding,最终渲染出来的页面应该是这样的:

  1. 黄色部分为Parent视图的背景色,Parent视图留有24dp的Padding;
  2. 中间部分为Flutter的计数器,Scaffold的背景色被设置成了一个灰色的蒙版(0x33000000),以帮助我们更好地去透视结构。

我们启动一个线程,然后按照X轴、Y轴在16ms内,每个刻度转动1个单位,然后看看效果,使用SurfaceView时,我可以注意到SurfaceView的容器(内部旋转的框线)在做正常的旋转动画,但是SurfaceView绘制的内容已经全乱了:

而如果替换成TextureView模式时:

显然,TextureView才是我们预期的效果,SurfaceView在这种场景下几乎完全不可用。

我们把整个视图缩小一些,或许就更能发现为什么SurfaceView的旋转动画如此混乱了:

FlutterSurfaceView实际渲染的内容并没有跟着容器去做旋转,但是这个旋转的过程中,矩形容器的四个顶点位置不断地在发生变化,导致FlutterSurfaceView渲染内容的区域的顶点也在不断地改变,这显然是不符合我们预期的,例如这个时刻,右侧的蓝色顶点明显已经和容器的红色顶点发生了偏移。

参考View的实现,FlutterSurfaceView的绘制区域对应的矩形区域大概也只有四个参数:top、bottom、left和right,导致容器在做旋转的时候,topLeft顶点和topRight顶点不在一个水平线上时将无法准确划定top的区域。

那其他动画有这个问题吗?

这个问题的核心其实是其他类型的动画会不会有类似的顶点漂移的问题。显然平移和缩放动画都不会带来这种问题,因为他们的左右顶点始终在同一水平线上,上下顶点始终在同一锤线上,所以只有旋转动画会带来这个问题

但一些版本的缩放会有其他兼容性问题

例如Android6.0缩放时,SurfaceView的渲染区域不能随着缩放后尺寸的改变而改变,放大时内容只会按原尺寸展示在左上角:

这里只试了6.0的系统的缩放动画,其他版本的动画也很难说是完美的。

SurfaceView的单独Surface,在合成时采用的策略类似「挖洞」,在屏幕中间抠出一块区域给这个单独的SurfaceView来进行展示,然后和ViewHierachy对应的主Surface再进行合成。在合成的时候一定是两块矩形内容进行合成,矩形的FlutterSurfaceView渲染内容和旋转过的宿主View区域必然无法做到四个顶点完全对齐。如果要避免这个问题,显然使用TextureView是更适合的方案,否则可能就需要根据旋转角度来对SurfaceView的图像内容做矩阵变换了。

Content in Commit:490c4bf48cb7109ef8678fd59fb5e8e83e8b33b3

四、两种模式下动画线程行为观察

在SurfaceView模式下渲染FlutterFragment,然后采用原生View的缩放动画来做视图的缩放:

我们会发现,只有主线程和RenderThread在不断在执行,flutter的三个线程并没有动静。

注意,缩放动画只会将SurfaceView的视图图像内容做放大,不会让SurfaceView的内容去重新绘制以适配新的尺寸。

如果用TextureView模式来渲染:

整体差不太多。但是由于FlutterUI没有发生改变,我们无法得到更多的信息,

这个1.raster线程就是FlutterSurfaceView用来做渲染的线程,地位上应该等价于主线程的对应的RenderThread线程。

我们在Flutter侧加上一个定时器,16ms触发一次,触发后对计数器+1,就像这样:

floatingActionButton: FloatingActionButton(
  onPressed: () {
    Timer.periodic(const Duration(milliseconds: 16), (timer) {
      setState(() {
        count++;
      });
    });
  },
  child: const Icon(Icons.add),
),

运行后,以Surface模式去渲染Flutter视图,运行计数器:

第一个红点对应的是计数器的启动,我们可以看到,在Surface模式下,只有flutter的UI线程和raster线程在不断地去工作,我们的主线程、RenderThread没有任何动作;

第二个红点对应的是FlutterFragment的缩放动画开始执行,这个过程中我们的主线程和RenderThread才开始正常工作。

这说明了Surface模式下,RenderThread和Raster线程的工作是相互独立的。

换成TextureView模式去渲染Flutter视图,结果如下:

两个红点代表的事件含义还是一样的,两张图的不同之处在于:

  1. Flutter并没有创建Raster线程;
  2. 主线程和RenderThread从计时器开始工作时,就不断地在工作,和Surface模式的相关场景完全不同;

在这个过程中,主线程和RenderThread分别需要:

  1. 主线程将FlutterTextureView(即FlutterUI)绘制的数据通过drawTextureLayer绘制出来;
  2. RenderThread将FlutterTextureView的Canvas一起渲染出来;

多了一层转译,RenderThread把ViewHierachy的图像数据、Flutter的纹理数据使用渲染出来,Raster线程的工作被RenderThread干了。自然也就不再需要单独的Raster线程了。

附:Flutter官方对于Raster Thread的描述:

flutter-ko.dev/perf/ui-per…

五、SurfaceView、TextureView与PlatformView

我们常规的需求是在Android ViewHierachy中嵌入Flutter视图,以Texture模式为例,这样一来其实渲染的主体内容其实还是ViewHierachy,而Flutter视图则是部分View的展示逻辑。我们只需要正常渲染ViewHierachy上的其他视图,然后对于嵌入的FlutterView去做纹理提取、绘制即可。

如果现在我们以FlutterUI作为主体,然后渲染了一个完整的Flutter视图作为FlutterActivity的主要内容,如果此时我们希望在FlutterUI中,嵌入一个Android原生的视图呢?这个时候就需要借助我们的另一个工具:PlatformView了。

PlatformView存在的作用就是将一个原生视图嵌入到FlutterUI当中。但在这里有一个问题,FlutterUI无论是Surface模式还是Texture模式去渲染,最终其实都会通过原生的SurfaceView、TextureView去渲染,因此我们在FluterUI中渲染NativeView的结果实际上还是在原生侧,将负责FlutterUI渲染的主TextureView(或者SurfaceView)和原生一侧渲染的Native视图数据进行合并

例如如下的页面,就在Scaffold的body中,以24的内边距嵌入了一个PlatformView,对应着Native层的一个TextView(黄色区域):

我们可以看到TextView严丝合缝地贴合进了Flutter视图:

TextView的尺寸 = PlatformViewWrapper的尺寸 = 在FlutterUI一侧PlatformViewLink的尺寸。

整体视图通过FlutterView这个容器进行堆叠,它本身是一个FrameLayout,对于FrameLayout的默认子View的行为其实就是堆叠(Stack),这样一来,似乎是Flutter主视图通过TextureView在底层渲染,而PlatformView堆叠在上层实现的,但如果仅仅依靠这种方式实现有逻辑硬伤,如果Flutter侧使用Stack视图,如果有其他的Widget需要在PlatformView上层展示就无能为力了(比如右下角的FloatingActionButton)。

既然TextView能够正常地绘制出来,那么就一定走了它的draw方法,我们通过断点、查找最终可以锁定到PlatformViewWrapper的draw函数上:

@Override
@SuppressLint("NewApi")
public void draw(Canvas canvas) {
  if (surface == null) {
    super .draw(canvas); 
    Log.e(TAG, "Platform view cannot be composed without a surface.");
    return;
  }
  /// 省略一部分对surface、SurfaceTexture做一些检查
  if (!shouldDrawToSurfaceNow()) {
    // If there are still frames that are not consumed, we will draw them next time.
    invalidate();
  } else {
    recreateSurfaceIfNeeded();
    
    final Canvas surfaceCanvas = surface.lockHardwareCanvas();
    try {
      surfaceCanvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
      super .draw(surfaceCanvas); 
      onFrameProduced();
    } finally {
      surface.unlockCanvasAndPost(surfaceCanvas);
    }
  }
}

我们可以看到,一开始对surface做了检查,如果当前的PlatformViewWrapper没有和任何Surface建立联系,那么就会调用默认的super.draw(canvas) 方法。在一系列检查过后,PlatformViewWrapper确保Surface可用之后,托管super.draw(surfaceCanvas) 向Surface的Canvas进行了绘制。这就意味着虽然PlatformViewWrapper仍然处在ViewHierachy当中,但是它的draw方法却「偷梁换柱」,替换成了一块其它的Surface,这就意味虽然PlatformViewWrapper和它的子View会走ViewHierachy的生命周期,但是又不会完全跟随着View系统被绘制出来。

那么假设有视图A、视图B,将视图A的draw方法做类似的操作,是不是可以将视图A的内容draw到视图B所在的区域呢?

接下来的问题,其实是PlatformViewWrapper的surface对象是哪来的,弄清楚这个问题,我们大致上对PlatformView绘制原理有了一个基本的理解了。

我们所有surface = 的代码进行断点,就可以得到它的创建上下文,在PlatformViewWrapper#setTexture代码中调用赋值,大致上的堆栈如下:

我们可以看到,DartMessenger/PlatformViewsChannel在收到Flutter一侧的消息之后通过走到相关的创建代码,创建了一个TextureLayer对象,然后通过构造函数创建了PlatformViewWrapper这个View对象,而在PlatformViewsController#configureForTextureLayerComposition方法中,设置了PlatformViewWrapper这个View对象的LayoutParams,具体的参数正是从Flutter一传递过来的PlatformViewsChannel.PlatformViewCreationRequest,大致上的属性内容如下:

至此,我们介绍了这么一件事,我们创建了一块Surface,然后PlatformViewWrapper通过重写draw方法,向Surface提供的Canvas上绘制了图像数据。

生产者有了,缓冲区也有了,缺的自然是图像数据的消费者了。

在PlatformViewWrapper初始化的时候,会通过PlatformViewController向native层的Engine注册一块纹理,获得一个textureID,后续Engine在绘制FlutterUI时,遇到PlatformView区域的时候,会通过这个textureID将之前通过draw(surfaceCanvas)绘制的纹理数据提取出来,并和FlutterUI的视图纹理进行合成,得到聚合了NativeView和FlutterView的视图。

但是以上的内容,仅限于在Flutter3中启用了Texture Layer Hybrid Composition模式进行渲染的PlatformView。对于早期的VirtualDisplay、HybirdComposition模式进行渲染的视图有其他的渲染方式。Texture Layer Hybrid Composition模式也有他自身的局限性,例如他需要做一个步骤:PlatformViewWrapper需要重写draw方法,并且将子View的图像绘制到此前注册的一个Surface区域中去,如果PlatformViewWrapper的子View,也就是我们需要嵌入的NativeView是SurfaceView,显然是无法正常兼容的。例如此时我们想把CameraX库的PreviewView使用PlatformView接入到Flutter,如果此时的PreviewView采用Surface实现,那么可能就会导致各种各样奇奇怪怪的问题,比如实际渲染出来的SurfaceView完全偏离了预定的位置(红色框线),并且盖住了一部分FlutterUI(Scaffold和原有的数字):

image.png

Flutter注册的纹理压根拿不到从SurfaceView中绘制的数据,因为SurfaceView压根不走draw那套逻辑,在最后draw阶段也就绘制不出任何内容。这里能展示出来的是PreviewView提供的Surface的直接数据。

但如果你使用COMPATIBLE(也就是TextureView)来进行渲染相机纹理,此时绘制的内容相对来说就是正常的:

mPreviewView. implementationMode = PreviewView.ImplementationMode. COMPATIBLE

实际渲染的TextureView比PlatformView、PreviewView大是因为取景器获得的画幅就是这么大,TextureView需要根据scaleType选择相关的缩放策略:

mPreviewView.scaleType  = PreviewView.ScaleType.FILL_CENTER CameraX组件库,为我们提供了对相机设备更为简单、方便易用的使用组件,使得开发的门槛进一步降低 在XML布局文件中,我们只需要使用标签即可加入PreviewView视图到我们的View Hierarchy中,而代码中,我们只要使用简单地声明一个CameraController,即可使用它,PreviewView本身聚合了包括双指缩放在内的一系列的功能,我们只需要声明一个NativeView,和NativeViewFactory在其中创建PreviewView即可。使用文档: developer.android.google.cn/media/camer…