大家好,我是染陌,这是我在全球开源技术峰会GOTC上的一个topic《基于Flutter的Web渲染引擎北海Kraken》。我主要从技术角度来分享Kraken的一些实现原理以及关键的技术特性,现在整理成文字版分享给大家。 KrakenGithub:https:github。comopenkrakenkraken Kraken官网:https:openkraken。com北海的技术背景 说到北海的技术背景就不得不提及跨端技术的演进,很多同学应该都比较熟悉跨端技术的历程了,我还是简单讲一些。 我们知道,浏览器是最成熟的天然跨平台方案。早在PC时代,浏览器已经成为了互联网的入口,大家都会习惯性通过浏览器来进行网页的浏览以汲取各种信息,当时我们把这种上网的方式叫做冲浪。然而到了移动时代,浏览器在移动设备上并没有一个抢眼的表现,反之因为内存大、弱网环境白屏久、传感器能力缺失(标准跟进慢)等问题使各种质疑不绝。 为了弥补上述浏览器在移动端的一些不足,出现了Hybrid技术,在Web之上通过容器的能力实现一些非标准化的超集,同时也通过prefetch、离线包等各种技术来提升首屏的加载性能。 此后,出现了类RN的方案(典型代表ReactNative),它的原理是通过JSengine将Native控件与前端生态实现一个桥接,通过Web开发业务逻辑提升效率,而向下通过Native控件渲染来提升性能及体验。但是这类方案的缺点是无法完全抹平两端的差异,没有解决一致性的问题,而最终将复杂度暴露给了开发者。 Flutter作为跨端届的新宠,这两年也获得了越来越多的关注,下面介绍一下Flutter。 Flutter的优点是性能好、由于其通过自绘渲染使得跨端一致性高,但是它也有它自身的缺点,比如生态自成一派,既不是前端也不是AndroidiOS。 这就是引出了一系列的问题。首先,前端(JavaScript)或客户端(SwiftJAVA)转型都有一定成本,但是由于端侧的GUI体系大同小异。笔者站在一个前端开发者的视角去看语言上的学习成本并不会特别高,有React或者Vue等前端框架经验的同学可以通过简答的学习快速上手。对于一些小型的创业团队,确实可以小步快跑快速学习上手并开发,但当组织庞大到一定程度,这个转换的成本将会指数级上升。其次,生态圈等待重新建设,一些Flutter开发者朋友或许觉得目前Flutter开发已经有挺多的pub可以直接使用了。但实际上生态圈不止于Flutterpub,还有各种已有的基础链路,比如建设相关的CICD,再比如搭建等等。这一系列的生态都需要重新建设,成本是非常大的。再次,已有的非常多业务都是通过JavaScript前端框架开发的前端项目,我们如果想把它们迁移成DartWidget成本无疑是非常庞大的。 在面对如此多的问题以及切换的高成本的同时,我们也期望通过Flutter给我们的业务带来更多的技术的可能性,同时改善Web容器在端上的一些性能及体验问题。那么,引入一项新技术的第一步是解决引入这项新技术的成本问题,所以我们积极探索一种将前端生态与Flutter结合起来的方案。 于是产生了本次topic的主角北海Kraken。 Kraken是一款高性能Web标准的自绘渲染引擎,具有高性能、易扩展、基于Flutter以及遵守Web标准的特点。 下面我列举了一些北海在阿里的一些应用场景,在C端APP或者IoT设备上,北海都有相关的落地。 北海的技术原理 在介绍Kraken的技术原理之前,我先演示一下如何开发一个Kraken应用。因为Kraken是基于W3C标准来开发的Web渲染引擎,所以上层是框架无关的,无论开发者使用的是Vue或者React还是Rax都可以在Kraken上进行一个应用开发。 以Vue。js开发为例,下面是我用Vue官方提供的vuecli起的一个项目。具体的代码见官方示例。 可以看到的是,最左边是Vue的相关代码,右边分别是该应用在Chrome(左)上跑的结果以及在Kraken(右)上跑的结果,大家可以看到结果是完全一致的。 了解了如何开发一个Kraken应用,我们再来理解一下Kraken的技术原理。为了大家更好地理解,首先我们来比较一下Flutter于Webview的渲染流程。 WebView的渲染流程相信大家非常熟悉了,面试中非常经典的题目就是一个URL输入如何最终渲染到屏幕上了。总的来说就是解析HTML、JS以及CSS文件,执行相应JS调用DOMAPI,最终会生成DOMTree以及CSSOMTree,然后会计算最终得到RenderTree,经过Layout以及Paint流程生成一系列的Layer,最终通过合成以及光栅化渲染到屏幕上。 再看Flutter这边,Flutter经典的三棵树WidgetTree、ElementTree以及RenderObjectTree。WidgetTree对应到前端类似于前端框架这层,而Element与DOMTree,RenderObjectTree与RenderTree分别对应,最终也会通过Layout以及Paint一系列计算生成Layer,然后通过合成以及光栅化渲染到屏幕上。 那么,我们再将前端框架加入到我们整个流程中进行一个更加直观的对比,这里还是以Vue。js为例。 Vue。js会在运行时生成一系列Vdom产生VdomTree,再通过platfom的抽象调用具体平台的API。 那么我们就会发现,只需要把我用红框圈出来的部分的流程进行互换,就可以实现我们最终想要实现的效果(上层Web开发,下层基于Flutter进行渲染)。 基于以上设想,那么北海的渲染流程就出来了。 目前主流的前端框架都会将产物打成一个JSBundle,通过标准的DOMAPI去操作具体的视图,而HTML内一般只有一个根结点。在Web下,页面会先请求HTML文件,再解析Script标签去加载对应的JS文件。而Kraken的入口设计成了一个JS文件,这样做可以减少一次请求,加快首屏的渲染。 该JS文件会在JSEngine中执行,Kraken的runtime通过JSEngineBinding的方式提供了一系列Web标准的API接口,调用相应API会执行相关逻辑并创建一系列需要发送给Dart层处理的指令,指令通过一个struct进行存储。C通过FFI将相应的指令底层的address发送到Dart这边,Dart处理相关指令并生成DomTree。同样的,CSS也会通过Parser生成对应的CSSOMTree,最终会结合生成Flutter的RenderObjectTree,经过Layout以及Paint的一系列计算,生成对应的一系列Layter,然后通过合成光栅化最终上屏显示。 同样的,在最新的实现中,我们考虑到了SSR应用的场景,所以加入了HTML为入口的北海应用开发方式,通过HTMLParser即可解析对应的HTML文件,后续流程是一样的。SSR的支持也让首屏的秒开率更上一层楼。 那么了解了Kraken的整个渲染流程,那么我们如何基于Flutter去完成Web标准的渲染引擎的开发呢? 那么要基于Flutter去做这个事情,就必须先了解Flutter的架构。 Flutter最上层是Dart实现的Framework,包含了响应式框架、官网组件库以及实现布局与绘制协议的部分。中间是C实现的FlutterEngine,他是渲染流程的下半部分,提供了一些基础能力,以及将layer合成以及光栅化后输出。最下层的Embedder层,则负责具体platform的一些实现,以实现跨平台。 不难发现,最DartFramework的Widget是对UI的抽象,实现了一套响应式框架,对应到前端就是VueReact等前端框架。而下方的布局协议,可以对应W3C的标准来实现一套基于前端标准的布局与绘制协议。 那么我们就可以得出北海的架构设计。 先看左边,左边还是上面介绍的Flutter的整体架构,Flutter的Widget能力可以通过插件的形式注册到Kraken中去,成为一个前端标准的Tag,JS可以动态化地调用及控制渲染。整个左侧的Flutter架构支撑了上层的Flutter生态,使Flutter生态也可以通过插件的形式融合到整个Kraken的渲染体系中去。 右边是Kraken的架构实现,Kraken的实现并没有把实现侵入到FlutterEngine中去。在Dart层,通过实现W3C标准的一系列布局与渲染能力,为上层提供了一些列标准化的能力,比如Element、CSS、以及各种Web标准的Module等。在上层Kraken的runtime通过JSEngineBinding的方式提供了一系列Web标准的API接口,调用相应API会执行相关逻辑并创建一系列需要发送给Dart层处理的指令,指令通过一个struct进行存储。C通过FFI将相应的指令底层的address发送到Dart这边,最终Dart根据指令调用前面说的标准化能力,以完成对接。通过该实现,为上层的前端生态提供了支撑,凭借丰富的前端生态,开发者可以享受前端生态带来的高效的开发体验。 关键技术特性 首屏的加载性能是一个C端场景的关键指标,长时间的白屏会极大地影响用户体验。 Kraken在首屏初始化时需要创建大量的节点,大量的时间耗费在通信上,所以优化首屏性能迫在眉睫。 在上面技术原理部分我们知道,Kraken需要通过Bridge来完成C(JSEngine)与Dart之间的通信,以达到将指令传递到Dart层的目的,Bridge的架构也进行了三个版本的演进。 最初的第一代方案,我们侵入了FlutterEngine,使数据从JSEngine传递到FlutterEngine中,然后通过nativebingding最终将数据发送给Dart层。这一代的方案非常明显的缺点是侵入了FlutterEngine,开发时需要编译FlutterEngine需要耗费大量的时间。同时,对于Kraken的架构来说,侵入FlutterEngine也并不是一个合理的设计。 后来出现了DartFFI,可以实现C与Dart之间的高效通信,所以产生了第二代方案。第二代Bridge方案通过将JSON数据序列化后,通过DartFFI将数据传递到Dart层,Dart层再通过JSON的反序列化以拿到最终的数据。这代方案比起上一代方案可以解决侵入FlutterEngine的缺点,但是引入了字符串的拷贝以及JSON序列化反序列化的时间长的问题。 为了解决上述问题,于是产生了第三代Bridge方案。第三代Bridge方案通过共享内存的方式定义了一个标准的40Bytes的Struct来存储指令,而通过DartFFI传递的只是指令的地址,C跟Dart两边都依赖地址来访问相关数据。这样做解决了JSON序列化反序列化的问题,节约了时间,并且少一次数据拷贝。同时,由于内存是40Bytes对其的,可以提高内存的访问效率。 下面是一些实际线上页面带来的首屏收益。 无限滚动的长列表是困扰前端开发者很久的历史性问题了,大量的layout导致页面卡顿,以及滚动时Paint的时间长导致滚动掉帧,页面的体验非常糟糕。社区也有非常多的前端的解决方案来处理该问题,而在Kraken上,我们也期望在容器层解决该问题。 在Android跟iOS上也分别有RecyclerView以及TableView来解决该问题,他们的原理分别是在可视区域viewport外定义一块缓冲区域,当节点超过该区域时进行动态释放,进入该区域时动态创建,以及通过一系列节点进行属性替换的方式来保证节点数不爆炸。Flutter中也提供了类似实现Sliver,那么我们能否用Sliver赋能前端解决该问题呢? Kraken定义了一个新的display属性sliver,通过将节点的display属性设置为sliver,则可以直接使用Flutter的Sliver能力,以达到节点超出可视及缓存区域后动态回收的一个能力。可以看到我们使用1000个卡片的DEMO进行测试,sliver下比起block有明显的收益。 同时,该标准也已经在W3C中文兴趣小组进行了讨论,期望在大家讨论充分以及达成共识后,尝试将此提案向W3C进行提交,反哺前端社区。 一个大前端团队往往既有客户端也有前端,会沉淀一系列的端上的能力。不同的需求会有不一样的技术选型,譬如说一个播放器往往是通过Native技术去开发的。我们期望将端上的能力(包括FlutterWidget、Web、Native以及三方SDK等)进行整合,融合成一个大前端的端开发体系,所以在Kraken内我们如何整合端上的这一系列能力呢?同时,我们也期望按需引入,能做到包体积的优化。在不同的业务域,我们期望可以快速地进行定制化开发,快速形成一套垂直业务域的领域能力。 Kraken提供了一套扩展能力来解决上述问题,通过渲染能力扩展接口,开发者可以将开发完成的符合标准的FlutterWidget以及Native的渲染能力快速集成到Kraken体系中去,最终通过JavaScript来提供一个动态化调用的能力。同样的,通过MethodChannel,开发者可以通过该通道调用一些Native或者DartAPI的能力,譬如说一些二方或者三方的SDK能力。 开发者可以通过扩展能力自定义业务域需要的能力,按需拔插以达到包体积优化的目的。同样的,注册到kraken的插件都可以通过JavaScript代码控制,提供了动态性。 下面是一系列在Kraken内部扩展FlutterWidget、NativeAPI以及Native播放器的Demo。 下面是提升可交互性。在介绍Kraken的可交互性之前,我们先来看一下在Web下的一些交互问题。 在Web下开发富交互能力的应用时,前端开发者往往需要引入一个额外的lib来提供增强的手势能力(譬如说Hammer。js这样的手势库)。那么当前端开发者引入lib时,就会导致加载index。html以后,还需要额外的请求对应的JS库,造成一次额外的请求开销的同时延长了首屏的可交互时间。 当用户在屏幕上进行某个操作时,由于用户操作的方式可能是用户的手,也可能是ApplePencil或者鼠标这样的设备。所以在W3C标准中,将用户操作可交互应用的触点抽象为一个pointer,这些pointer会根据操作形成一个手势,分别是down、move、up三个过程,其中move可省略(譬如说click)。 在Web中,需要将这一系列pointer给dispatch到elementtree上,通过冒泡将这些pointer频繁地发送到JS层,然后JS再通过封装TouchAPI来完成对交互的识别。这样做带来几个问题,首先频繁地将pointer从C传递到JS带来了不必要的开销,此外封装标准的能力也会造成额外的开发成本,易用性并不突出。此时,如果使用社区的一些方案,也会导致非标准化使标准不对齐导致同个应用中的不同页面有不一致的交互体验。 为了解决上述问题,我们期望从标准化、易用性、标准化几个方面提供一套标准化的交互能力。通过封装底层的pointer来得到不同的手势能力,使开发者可以快速开发富交互的应用。 下面是Kraken中增强交互能力的流程图。当用户进行某些交互操作以后,每一个触点的pointer会从Native传递到Kraken中,Pointer会同时分发给GestureManager(手势识别器管理类)以及Scroll识别器。GestureManager会识别开发者通过Web标准的监听行为(EventTarget。addEventListener)来注册以及分发给对应的手势识别器,同样Scroll识别器也会被分发pointer。这些识别器被加入到Flutter的竞争场进行手势竞争,以保证只触发某一个具体操作(交互可控)。Scroll识别器会触发滚动区域的滚动操作,手势识别器则会通过标准的Web流程进行冒泡以及dispatch,最终开发者通过监听事件完成自定义行为。 开发应用时,调试能力是必不可少的,前端开发效率高不止要归功于繁荣的生态,友好的开发调试体验一样是提升效率的神器。 Kraken抽象了Inspector以通过ChromeDevToolsProtocol来对接ChromeDevTools,提供了一系列跟前端开发Web应用完全一致的调试体验,无论开发者喜欢使用Console。log还是通过JSDebugger,都可以快速上手。 此外,Kraken也通过支持HMR的所有标准的WebAPI,来提供局部热更新的能力,使开发kraken应用能跟Web下一致的局部热更新的调试体验,大大提升了开发者的开发调试体验。 最后,Kraken的所有代码都已经开源,Kraken提供了开放的TSC机制期望所有开发者可以平等地交流以及决策,使Kraken可以更好地发展,也欢迎更多的开发者一起来共建Kraken。 作者:染陌 原文链接:http:click。aliyun。comm1000298810 本文为阿里云原创内容,未经允许不得转载。