Flutter与原生组件的深度融合:PlatformView的核心优势解析

发表时间: 2024-06-20 10:34

我们在使用Flutter开发产品时,可能会遇到一些情况不得不依赖原生组件的能力,尤其是在视频渲染、网页浏览方面,Native端有完善的封装,我们的研发周期基本不会允许在Flutter层再实现一份同样的“轮子”,此时就要用到Flutter的超能力:PlatformViewor 外接纹理。我们今天的主题就由一个Native组件渲染的Bug,深入Flutter engine展开Native组件渲染原理的分析,小提示:

  • 本文主要由Flutter Android源码展开分析
  • 有一定Flutter使用经验阅读体验更佳


先看一个Bug

Flutter外接纹理大家应该都比较熟悉,引擎可以通过纹理id直接“map”对应的纹理图像,渲染到屏幕上,用起来可以说是非常方便,解决了FlutterPlatformView使用复杂、性能差、兼容性差的痛点;对于视频渲染场景,通过Flutter外接纹理复用原生层能力无疑是最佳解决方案;但是任何方案都不是完美的,今天我们就针对Flutter外接纹理渲染的一个Bug来探析外接纹理渲染的原理,并解决Bug,先看一下Bug:

在实际使用的过程中,我们发现,当我们在应用使用了Texture控件,并同时又在上层引入了PlatformView组件时(PlatformView使用Hybird Composition方式,也就是SurfaceAndroidView),会发现后面的Texture不再自动刷新了,画面像卡住了一样,移除PlatformView后刷新回归正常;此类问题包括视频+WebviewSurfaceAndroidView渲染方式)组合时,视频刷新卡顿、停滞;两个case的原理是一致的,都是TexturePlatformView进行图层混合时渲染时出现的问题。这个问题出现在安卓平台,而iOS平台无此问题,这个主要是因为iOS与安卓对于原生组件的渲染方式不同。从现象上来看,是引入了PlatformView组件后产生的视频刷新停滞,那会不是PlatformView的渲染引发的此问题?为了搞清是不是这个原因我们先来分析一下PlatformView的渲染。

PlatformView方案演进

VirtualDisplay

我们在创建PlatformView时,Dart侧有两种实现方案供我们选择,分别是AndroidView、AndroidViewSurface,当我们使用AndroidViewSurface时,Dart层会附加“hybrid=true”的参数,Java层会根据此参数判断是否启用HybirdComposition。最初的Flutter还不支持HybirdComposition,只有AndroidView——原生层对应的实现方案是VirtualDisplay。此时AndroidView是通过把原生组件渲染到VertualDisplayVertualDisplay由系统api创建)的Surface上(SurfaceFlutter创建传入),再根据textureid关联到flutter引擎渲染,其渲染流程如下;其中VertualDisplay的渲染只是图像,并不能接收手势,这需要Dart层再转发一次手势,造成了很多手势的玄学问题,比如在app运行中可能出现点击无响应(此时应用未卡死,画面能正常渲染)的情况,目前这种方案使用较少;

HybirdComposition

从Flutter 1.20开始,Flutter引入了 Hybird Composition渲染方式,也就是原生组件与Flutter画布共同渲染,对应Dart Api中的SurfaceAndroidView。原生组件通过Addview添加到了FlutterView中,通过FlutterMutorView(FrameLayout)包裹来提供显示隐藏切换及手势转发等,同时通过FlutterImageView来实现FlutterWidget与原生组件的图层混合;这是目前使用较多的原生桥接方案,相对于VirtualDisplay更加稳定,但图层的合成比较复杂,这也是我们今天探究的问题根源;

Flutter3.0新一代渲染方案

到了Flutter3.0,VertualDisplay混入实现发生“重大”变革,新的实现方案同样是通过Addview添把原生组件添加到FlutterView中,通过PlatformViewWrapper包裹来实现显示隐藏、手势转发、以及视图矩阵变化,不同的是,PlatformViewWrapper组件的draw方法会根据需要将视图绘制到FlutterSurfaceTexture,并且通过surface.lockHardwareCanvas()来减少cpu拷贝,相对于VertualDisplay方式,3.0版本PlatformViewWrapper本身就是一个FrameLayout,对于安卓手势处理也相对简单了很多(与 Hybird Composition处理手势的方式相同),提升了性能、兼容性的同时,相对于Hybird Composition还简化了不同图层的混合的复杂度。

显然Flutter团队对于新的PlatformView实现方案非常有信心,从源码注释得知,此前主流的Hybird Composition渲染方式也将很快退出舞台,未及时适配的小伙伴有必要适配一波了;

外接纹理源码分析

Texture刷新流程

分析完大致原理我们来进一步分析外接纹理的刷新流程。我们知道,在使用外接纹理时,我们只需要在Dart层传入纹理id,Texture控件便可以在原生层纹理渲染完成后自动刷新,那么它的自动刷新机制是什么?flutter是怎么知道原生层视频帧渲染完成、绘制到屏幕?是不是自动刷新逻辑有Bug呢?带着这个问题我们再来看一下外接纹理渲染刷新机制。首先原生层创建SurfaceTexture(由安卓FrameWork提供),创建自增id并注册到FlutterEngine

public SurfaceTextureEntry createSurfaceTexture() {   final SurfaceTexture surfaceTexture = new SurfaceTexture(0);   surfaceTexture.detachFromGLContext();   final SurfaceTextureRegistryEntry entry = new SurfaceTextureRegistryEntry(nextTextureId.getAndIncrement(), surfaceTexture);   //向引擎层注册   registerTexture(entry.id(), entry.textureWrapper());   return entry; } private native void nativeRegisterTexture(   long nativeShellHolderId, long textureId, @NonNull SurfaceTextureWrapper textureWrapper);

通过源码得知,当我们的视频渲染完成时,通过Android SurfaceTexture自身回调OnFrameAvailableListener便可以监听到帧渲染完成,然后通过jni直接通知c层,此过程通过Devtools 查看Performance overlay是不会有画面渲染的回调的(因为没有走Layout、Paint流程);

private SurfaceTexture.OnFrameAvailableListener onFrameListener =    new SurfaceTexture.OnFrameAvailableListener() {      @Override      public void onFrameAvailable(SurfaceTexture texture) {        ...        //Frame准备好后通知native        mNativeView            .getFlutterJNI()            .markTextureFrameAvailable(SurfaceTextureRegistryEntry.this.id);      }    }; //c方法,通知到flutter engineprivate native void nativeMarkTextureFrameAvailable(long nativeShellHolderId, long textureId);

因为java层涉及的逻辑比较简单,且通过Debug得知上述Bug发生时,此回调是正常走的,所以我们来进一步分析c层逻辑;先来看一下nativeMarkTextureFrameAvailable的实现。这里做了两件事,一是把对应的纹理画布标记为new_frame_ready_ = true,二是开始执行下一帧画面的刷新ScheduleFrameScheduleFrame方法大家可能已经从其它地方看到过,UI构建任务由此添加到task_runners等待执行(硬件Vsync信号发生时,触发执行);ScheduleFrame在其它场景执行时(比如动画刷新)参数都为true,只有TextureFrameAvailable这种情况是false,这也是为了优化性能。事实上,如果单纯把此参数改为true,发现修复了这个Bug,这里只是为了验证猜想

// 方法注册、转发部分省略// |PlatformView::Delegate|void Shell::OnPlatformViewMarkTextureFrameAvailable(int64_t texture_id) {  ...   // Tell the rasterizer that one of its textures has a new frame available.  task_runners_.GetRasterTaskRunner()->PostTask(      [rasterizer = rasterizer_->GetWeakPtr(), texture_id]() {        auto* registry = rasterizer->GetTextureRegistry();        ...        texture->MarkNewFrameAvailable();      });   // Schedule a new frame without having to rebuild the layer tree.  task_runners_.GetUITaskRunner()->PostTask([engine = engine_->GetWeakPtr()]() {    if (engine) {      //开始刷新布局(无需重建),这也是Texture刷新性能好的一个原因      engine->ScheduleFrame(false);    }  });}
  • ScheduleFrame(false)流程

我们再来看一下ScheduleFrame(false)时的后续逻辑,引擎层不会触发scheduleFrame流程,而是将last_layer_tree_也就是上一次setState流程后产生的渲染指令重绘;

  • ScheduleFrame(true)流程

那么Flutter层Widget刷新流程是什么呢(也就是正常setState流程)?为了对比分析Bug产生的原因,继续分析setState源码。当我们调用setState()之后,最终也是会触发 engine→ScheduleFrame(),只不过此时参数为true;这也解释了一个现象,当Texture不刷新时,如果Widget执行setState操作,画面又能正常刷新一帧。我们用两张图总结一下正常setState触发的刷新流程:

通过分析,发现Texture触发的刷新只是在引擎层进行了图层的合成,并没有进行三棵树的刷新、Layout、Paint,所以接下来的重点放在Texture在Flutter层、平台层、引擎层的实现上;

外接纹理Dart层实现

Flutter引擎在接收到Vsync信号(在Android上由Choreographer类回调)后,开始ScheduleFrame流程,首先Dart FrameWork在UI线程完成Layers的局部绘制,然后交给Raster线程进行合成及上屏。这里的UI和Raster两个线程就是我们打开性能检测后的两个主要性能监测维度。其调用链大家如果感兴趣可以查阅相关文章,这里面我们主要关心两个点,1.Flutter层,Flutter层中的Paint流程是否正确把Texture绘制区域标记为"dirty",进而参与重绘2.engine层,是否把TexutreLayer的数据正确合成、送显

Texture控件在flutter层对应的渲染树为TextureBox,经过查阅dart层framework源码发现,Texture在flutter层的实现比较简单,与其它组件不同,TexutreBox自身没有绘制逻辑,paint方法里只是在SceneBuilder中添加一个TextureLayer,后续的绘制交给了引擎层实现;

class TextureBox extends RenderBox {...@overridebool get alwaysNeedsCompositing => true; //需要合成@overrideSize computeDryLayout(BoxConstraints constraints) {  return constraints.biggest;          } @overridevoid paint(PaintingContext context, Offset offset) {  context.addLayer(TextureLayer(    rect: Rect.fromLTWH(offset.dx, offset.dy, size.width, size.height),    textureId: _textureId,    freeze: freeze,    filterQuality: _filterQuality,  )); }}
  • 外接纹理引擎层实现

那引擎层如何绘制texture数据?外接纹理在引擎层对应的渲染类为TextureLayerlayer_tree中的TextureLayerPaint时,会通过texture_id映射到texture的平台实现类调用其绘制方法;

void TextureLayer::Paint(PaintContext& context) const {  TRACE_EVENT0("flutter", "TextureLayer::Paint");   //拿到平台Texture的最终实现  std::shared_ptr<Texture> texture = context.texture_registry.GetTexture(texture_id_);  //进行绘制  texture->Paint(*context.leaf_nodes_canvas, paint_bounds(), freeze_, context.gr_context, sampling_);}

texture在不同平台的实现不同,比如安卓为AndroidExternalTextureGL,我们来看一下Paint方法;

void AndroidExternalTextureGL::Paint(SkCanvas& canvas,                                     const SkRect& bounds,                                     bool freeze,                                     GrDirectContext* context,                                     const SkSamplingOptions& sampling) {  GrGLTextureInfo textureInfo = {GL_TEXTURE_EXTERNAL_OES, texture_name_,GL_RGBA8_OES};     //取到离屏渲染的纹理数据  GrBackendTexture backendTexture(1, 1, GrMipMapped::kNo, textureInfo);  //生成SkImage  sk_sp<SkImage> image = SkImage::MakeFromTexture(      context, backendTexture, kTopLeft_GrSurfaceOrigin, kRGBA_8888_SkColorType,      kPremul_SkAlphaType, nullptr);  //绘制Image  if (image) {    SkAutoCanvasRestore autoRestore(&canvas, true);    canvas.translate(bounds.x(), bounds.y());    canvas.scale(bounds.width(), bounds.height());    if (!transform.isIdentity()) {      SkMatrix transformAroundCenter(transform);       transformAroundCenter.preTranslate(-0.5, -0.5);      transformAroundCenter.postScale(1, -1);      transformAroundCenter.postTranslate(0.5, 0.5);      canvas.concat(transformAroundCenter);    }    canvas.drawImage(image, 0, 0, sampling, nullptr);  }}

通过引擎源代码调试,发现Texture在有、无PlatformView的情况下调用堆栈和基本参数都没有区别;排除了图层渲染的问题,也就是说,当上述Bug中视频画面卡住时,实际上TextureLayerAndroidExternalTextureGL内部在正常绘制;通过native断点可以更清晰的看到绘制流程,AndroidExternalTextureGL::Paint方法调用堆栈:

  • Hybrid Composition中的图层合成

上面的流程没有定位到明显的疑点,且通过测试发现Flutter3.0不存在这个问题,所以我们怀疑的目标落在Hybrid图层合成上。当我们使用了Hybird Composition方式的PlatformView时,Flutter在渲染时,会根据PlatformViewFlutterWidget的实际交叉、覆盖关系,使用不同的绘制策略。

比如,当PlatformView只在顶层时,Flutter只是在FlutterView(FrameLayout)上层添加了一个原生控件;当PlatformView上层还有FlutterWidget时,也就是原生布局被夹在中间,关键来了我们知道FlutterUI绘制在SurfaceView(默认)或者TextureView上,SurfaceView或者TextureView做为一个原生组件,与PlatformView添加的原生组件属于同一级别,那么上层的Widget如何显示在PlatformView之上?Hybrid Composition中,是将上层的Widget图层,绘制在了FlutterImageView原生组件上,相当于把Flutter控件抽出来放到了NativeView的上层,这样便实现了PlatformViewFlutterWidget无缝混合;

分析Bug原因所在

分析到这里,回想一下既然是引入PlatformView之后才出现的问题,而通过上面我们对外接纹理渲染流程的初步分析后又没发现问题,那问题的关键可能就落在了PlatformView上;我们知道只有PlatformView启用了Hybird混合图层模式后,才会引发此问题,所以我们来分析一下Hybird会对原有渲染流程造成哪些影响。

在分析PlatformView渲染代码的过程中,发现FlutterView的一段特殊逻辑,在PlatformViewsController中,onDisplayPlatformView即请求展示PlatformView时,会先执行initializeRootImageViewIfNeeded()操作,此方法中,会将FlutterView的渲染Surface切换为FlutterImageView。啥意思呢?就是上面一小节原生层级最底层的SurfaceView(原本用于展示FlutterUI),变为了FlutterImageView。这是什么操作?代码里的注释原话是”When adding platform views using Hybrid Composition, the engine converts the render surface to a FlutterImageView to help improve animation synchronization on Android.”,意思是在Hybrid Composition模式下,引擎会把UI渲染到FlutterImageView上来提高动画的同步(一致、及时)性;

那么既然用于展示FlutterUI的SurfaceView变为了FlutterImageView,我们就进一步跟踪FlutterImageView,首先找到FlutterImageView刷新画面的关键代码;acquireLatestImage()这个方法主要逻辑是从FlutterRender中取到最后一帧图像数据,然后刷新展示;

我们直接断点调试,发现此方法由FlutterJNI.onEndFrame触发,onEndFrame时,Flutter端绘制完成,PaltformView Add完成,此时FlutterImageView把最后一帧Flutter画面展示出来,看上去顺理成章。但是没想到问题就出现在这里!调试过程中发现,onEndFrame方法在大部分时间都不会调用与此同时onBeginFrame在每次渲染时都会正常调用!onEndFrame不调用,FlutterImageView获取不到来自引擎的最新图层画面,所以也就出现了卡顿,甚至完全不刷新;

解决Bug

上面通过对源码的层层分析,顺便梳理了PlatformView的方案演化、外接纹理的绘制流程及原理,最终定位到Texture不刷新的问题在于onEndFrame没有调用,这里猜测一下为什么在DrawLastLayerTree流程中会没有此回调,这是有意为之,也应该是一个疏忽,正常情况下,Layers渲染树没有变化时,也就意味着Flutter Widget布局没有变化,自然也就不需要刷新,而ScheduleFrame(true)流程会正常调用onEndFrame,所以布局没任何变化时,当然可以去掉onEndFrame减少绘制、优化性能,但是这里忽略了TextureLayer布局虽然是固定不变的、它的刷新不需要setState、不会引起Layers渲染树的变化,但是它对应的纹理的图像内容是会变化的。布局不变,所以去掉了onEndFrame中断了后面的绘制流程,所以产生了我们本次分析的Bug;

既然定位到了原因所在,解决就好办了,通过进一步分析,这里有三个思路解决此问题;

解决方案一

引擎层没有回调JNI的onEndFrame,是因为DrawLastLayerTree流程中,没有此回调,我们手动添加上去;在Rasterizer::DrawToSurface方法中,添加回调逻辑:

RasterStatus Rasterizer::DrawToSurface(FrameTimingsRecorder& frame_timings_recorder,flutter::LayerTree& layer_tree){  //部分代码有省略   if (external_view_embedder_) {    external_view_embedder_->BeginFrame(layer_tree.frame_size(), surface_->GetContext(),        layer_tree.device_pixel_ratio(), raster_thread_merger_);  }  if (compositor_frame) {   RasterStatus raster_status = compositor_frame->Raster(layer_tree, false);   external_view_embedder_->SubmitFrame(          surface_->GetContext(), std::move(frame),          delegate_.GetIsGpuDisabledSyncSwitch());    //gang: 在渲染完成之后,添加endFrame回调,解决texture纹理不刷新;    auto should_resubmit_frame = raster_status == RasterStatus::kResubmit ||                               raster_status == RasterStatus::kSkipAndRetry;    if (external_view_embedder_) {      external_view_embedder_->EndFrame(should_resubmit_frame, raster_thread_merger_);    }  }}
  • 解决方案二

在java层,PlatformViewsController.onDisplayPlatformView()方法尾部添加acquireLatestImage的调用,此种方案比较简单,不需要改C++代码,但是不符合原有的API流程设计思路,不推荐;

解决方案三(推荐)

从源码得知,上面的“convert to FlutterImageView”流程受到synchronizeToNativeViewHierarchy开关的控制,默认是开启的,这个方法提供的桥接可以让我们在Flutter层直接操作,对应的类为PlatformViewsService,所以我们添加dart代码 “PlatformViewsService.synchronizeToNativeViewHierarchy(false)”,问题解决;

/// Whether the render surface of the Android `FlutterView` should be converted to a `FlutterImageView`.////// When adding platform views using/// [Hybrid Composition](https://flutter.dev/docs/development/platform-integration/platform-views),/// the engine converts the render surface to a `FlutterImageView` to improve/// animation synchronization between Flutter widgets and the Android platform/// views. On Android versions < 10, this can have some performance issues./// This flag allows disabling this conversion.////// Defaults to true.static Future<void> synchronizeToNativeViewHierarchy(bool yes) {  assert(defaultTargetPlatform == TargetPlatform.android);  return SystemChannels.platform_views.invokeMethod<void>(    'synchronizeToNativeViewHierarchy',    yes,  );}

参考文献

Exploration of the Flutter Rendering Mechanism from Architecture to Source CodeFlutter 深入探索混合开发的技术演进Compiling the engine


作者:邢小钢

来源-微信公众号:映客技术

出处:https://mp.weixin.qq.com/s/OlgYHRsiTPydJjIxzdQE3w