先看一个Bug
Flutter外接纹理大家应该都比较熟悉,引擎可以通过纹理id直接“map”对应的纹理图像,渲染到屏幕上,用起来可以说是非常方便,解决了FlutterPlatformView使用复杂、性能差、兼容性差的痛点;对于视频渲染场景,通过Flutter外接纹理复用原生层能力无疑是最佳解决方案;但是任何方案都不是完美的,今天我们就针对Flutter外接纹理渲染的一个Bug来探析外接纹理渲染的原理,并解决Bug,先看一下Bug:
在实际使用的过程中,我们发现,当我们在应用使用了Texture控件,并同时又在上层引入了PlatformView组件时(PlatformView使用Hybird Composition方式,也就是SurfaceAndroidView),会发现后面的Texture不再自动刷新了,画面像卡住了一样,移除PlatformView后刷新回归正常;此类问题包括视频+Webview(SurfaceAndroidView渲染方式)组合时,视频刷新卡顿、停滞;两个case的原理是一致的,都是Texture与PlatformView进行图层混合时渲染时出现的问题。这个问题出现在安卓平台,而iOS平台无此问题,这个主要是因为iOS与安卓对于原生组件的渲染方式不同。从现象上来看,是引入了PlatformView组件后产生的视频刷新停滞,那会不是PlatformView的渲染引发的此问题?为了搞清是不是这个原因我们先来分析一下PlatformView的渲染。
PlatformView方案演进
VirtualDisplay
我们在创建PlatformView时,Dart侧有两种实现方案供我们选择,分别是AndroidView、AndroidViewSurface,当我们使用AndroidViewSurface时,Dart层会附加“hybrid=true”的参数,Java层会根据此参数判断是否启用HybirdComposition。最初的Flutter还不支持HybirdComposition,只有AndroidView——原生层对应的实现方案是VirtualDisplay。此时AndroidView是通过把原生组件渲染到VertualDisplay(VertualDisplay由系统api创建)的Surface上(Surface由Flutter创建传入),再根据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,二是开始执行下一帧画面的刷新ScheduleFrame,ScheduleFrame方法大家可能已经从其它地方看到过,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流程,而是将last_layer_tree_也就是上一次setState流程后产生的渲染指令重绘;
那么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数据?外接纹理在引擎层对应的渲染类为TextureLayer,layer_tree中的TextureLayer在Paint时,会通过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中视频画面卡住时,实际上TextureLayer、AndroidExternalTextureGL内部在正常绘制;通过native断点可以更清晰的看到绘制流程,AndroidExternalTextureGL::Paint方法调用堆栈:
上面的流程没有定位到明显的疑点,且通过测试发现Flutter3.0不存在这个问题,所以我们怀疑的目标落在Hybrid图层合成上。当我们使用了Hybird Composition方式的PlatformView时,Flutter在渲染时,会根据PlatformView与FlutterWidget的实际交叉、覆盖关系,使用不同的绘制策略。
比如,当PlatformView只在顶层时,Flutter只是在FlutterView(FrameLayout)上层添加了一个原生控件;当PlatformView上层还有FlutterWidget时,也就是原生布局被夹在中间,关键来了,我们知道FlutterUI绘制在SurfaceView(默认)或者TextureView上,SurfaceView或者TextureView做为一个原生组件,与PlatformView添加的原生组件属于同一级别,那么上层的Widget如何显示在PlatformView之上?Hybrid Composition中,是将上层的Widget图层,绘制在了FlutterImageView原生组件上,相当于把Flutter控件抽出来放到了NativeView的上层,这样便实现了PlatformView与FlutterWidget无缝混合;
分析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