Android 动态加载 JS — Flutter Kraken / Open WebF
一、背景
在基于 GSL 的跨端 UI 框架中,如果我们只依赖事先制定的已有组件来支撑业务,从业务可变性和开发成本来说,都会存在不可规避的瓶颈问题。试想:如果 Web 端完成一个复杂的日历组件开发,并希望其能在设备端上渲染。
在没有动态性的支撑下,设备端一是无法支持(无法识别日历组件),二是需要复刻这个日历组件开发一遍,发布软件版本,并祈祷所有线上用户都能升级到最新软件。
基于这样的背景,端的动态渲染便有其存在的必要和使命。
二、方案
本文标题便已点明我们探究的是哪种解决方案,即阿里开源的、基于 Flutter 的 Kraken 渲染引擎来实现对 JS 的动态加载。
相信搞过 Flutter 开发的同学都了解 Flutter 导入业务有两种模式:一种是基于纯 Flutter 的开发,另一种是 Flutter 与原生平台的融合开发。而第二种方式中 Flutter 在 Native 平台上又有三种 UI 渲染载体,分别为:
- FlutterActivity
- FlutterFragment
- FlutterView
结合我们自研的跨平台 UI 框架对动态性渲染的诉求:
- 动态渲染的 JS 组件作为子节点融合进原生 View 树
- 渲染组件支持与外部作双向通信
那么我们基本敲定思路:在 Android 中通过动态创建 FlutterView 连接 Flutter,并与之做数据的传输;而 Flutter 便借助 Kraken 专注于接收 JS 数据并执行渲染,同时支持其组件交互事件的跨平台响应。
三、实现
Step 1 - Android 引入 Flutter module
这部分已经很成熟了,官文 诚不欺我,自行在掘金上搜搜也能找到其他指南:即如何在原生项目中,引入 Flutter 模块。
Step 2 - Android 使用 FlutterView 渲染 Dart UI
正如 官文 所提及的 “Integrating via a FlutterView requires a bit more work than via FlutterActivity and FlutterFragment previously described.” 集成 FlutterView 需要更多的人为操作,没有 FlutterActivity 那么方便。
Github add_to_app 是官方提供的 Demo,基本阐述了一个 FlutterView 在 Android 平台中的接入过程,即:
- 创建 FlutterEngine(一个 FlutterView 对应一个 FlutterEngine)
- 创建 FlutterView 并添加进 Android View 容器
- 确保 FlutterEngine 在恰当的时机调用FlutterEngine.lifecycleChannel.appIsResumed()
- FlutterEngine 执行自定义或默认的 Dart EntryPoint
- FlutterView 与 FlutterEngine 完成连接(attach)
上面步骤都需要我们在 Android Native 侧完成,具体代码见 Demo 即可,这里便不赘述。
需要特别指出的是:FlutterView 默认的 renderSurface 是 SurfaceView,这并不支持透明背景,且无法融入原生 View 树的,所以在构建 FlutterView 的时候应该使用 TextureView 作为其 renderSurface,即:
val flutterTextureView = FlutterTextureView(this)
flutterTextureView.isOpaque = false
val view = FlutterView(this, flutterTextureView)
Step 3 - Flutter 基于 Kraken 渲染 JS UI
根据 Kraken 官网,将 Kraken 在 Flutter 中跑起来是非常清楚且简单的事情,建议把 官方文档 大体过一遍,对 Kraken 会有个较为全面的认识。
虽然问题不大,但难免还是踩了一些小坑:
- 本地 Bundle 的加载与官方描述不符
// 官方描述:
Kraken kraken = Kraken(
bundle: KrakenBundle.fromUrl('assets://assets/bundle.js')
);
// 实际有效:
Kraken kraken = Kraken(
bundle: KrakenBundle.fromUrl('assets:///assets/bundle.js')
);
// 这两者的差别仅在于 assets: 后是两个 '/' 还是三个 '/',着实是坑 🙂
- JS 组件打包
根据公司内部主流前端技术栈,我们通过 React Kraken 来完成 JS 组件的开发,按照 Kraken 官方提供的 React Demo 中关于 webpack.config.js 的修改,将其同步至自己的 React 项目,并通过 Kraken Cli 完成项目的 Debug。
注意:一定要确保 webpack 的配置跟 Kraken 的配置 同步,不然通过 npm run build 打出来的 bundle.js 在 Android 端是无法使用的,具体详见 kraken-react-demo。
- JS 组件适配(尺寸)
首先我们必须知道,一个 JS 组件的渲染尺寸,是由多个平台决定的:
(1)Android FlutterView(LayoutParams)
(2)Flutter Dart UI
(3)Kraken viewPort size
(4)React JS widget size
注意:Android add FlutterView 的时候如果不指定大小,FlutterView 默认撑满父布局
过多的因素无疑会造成复杂性的陡增,所以结合我们自己的业务特点,进行范围的收窄。 由此规定:
- Android 添加 FlutterView 的时候明确指定 JS 组件能显示的最大范围(GSL 框架协议指定);
- Flutter Dart UI 不干涉尺寸设定,默认撑满外部容器
- Kraken 不指定 ViewPort size,默认撑满外部容器
- React JS 即可指定具体尺寸,亦可设置自适应尺寸
假设按一倍图的标准进行尺寸的设定,比如一个长宽是 200px 的方块,JS 的尺寸设定为:
// React JS
const styles = {
width: '200px',
height: '200px'
}
Android add FlutterView 时的尺寸则为:
// Android
addView(flutterView,
DisplayUtil.dip2px(this, 200f),
DisplayUtil.dip2px(this, 200f)
)
Step 4 - Multi Flutter Kraken View
Kraken 方案是否可用的关键因素之一在于是否支持添加多个视图,而多 FlutterView 的关键是多 FlutterEngine。
Flutter 2.0 之后优化了 FlutterEngine 多实例的资源占用,使得我们在同一进程,同一界面同时处理多个 Flutter Kraken View 具备了可行性。为此实验 Demo 做了三种 FlutterEngine 构造和使用方式进行验证,分别是:
- new FlutterEngine()
最原始的方法,将一个 FlutterEngine 所需要的所有资源都构建出来,占用内存最大。单个构建成本:瞬时暴增 124.2MB(89.1->213.3),稳定一段时间后,经历两段释放降为 192.1MB 至 165.5MB
再次添加 FlutterView,构建 FlutterEngine,每次增量约 64MB
- FlutterEngineGroup.createAndRunEngine()
借助 FlutterEngineGroup 生成的 FlutterEngine 具有常用共享资源(例如 GPU 上下文、字体度量和隔离线程的快照)的性能优势,能加快首次渲染的速度、降低延迟并降低内存占用,但每个 FlutterEngine 又保持其独立性,各自维护路由栈、UI 和应用状态。其构建成本:首个 FlutterEngine 128.4MB(89.7->218.1),后续衍生构建的 FlutterEngine,官方称 180KB,实测 197KB 基本吻合(如下方图 2 所示)
- FlutterEngineCache.put() / get()
尝试构建一个指向某个 EntryPoint 的 FlutterEngine 后,存入 FlutterEngineCache,要用时再取出复用是否可行?
官方给出了明确提示:一个 FlutterView 应该对应一个 FlutterEngine,并且同一个 FlutterEngine 不允许执行多次 executeDartEntrypoint(DartEntrypoint point)。如果强行将某个已经 attach FlutterView 的 FlutterEngine 绑到另一个 FlutterView 又会有什么反应呢?请见下图:
原本 attach 在第 1 个 FlutterView 的 FlutterEngine,不断地更换 attach 对象到第 2 个、第 3 个
经过实验,直接给结论:
-
FlutterEngine 会将数据同步给后面 attach 的 FlutterView,也就是新创建的 FlutterView 跟 FlutterEngine 上一个 attach 的 View 长得一模一样
-
被 FlutterEngine attach 的 FlutterView 能够响应 FlutterEngine 中的数据变化,而“失去” FlutterEngine 的 FlutterView 将不再响应数据变化,更新 UI
-
失去 FlutterEngine 的 FlutterView 被点击后,引起的数据更新依然能同步至被 FlutterEngine attach 的新的 FlutterView
所以,此方式并不是处理多 FlutterView 的良药,同时缓存 FlutterEngine 本身并不能减少 Multi FlutterEngine 的内存占用,它的意义在于利用空间换时间,减少 FlutterEngine 构造并执行 DartEntrypoint 所占用的时间。
最后,有个值得你注意的地方:当你在 Flutter Dart 中构建 Kraken 对象时,如果传入了 ChromeDevToolsService 那么你的 FlutterEngine 的内存就无法释放,导致内存泄露。
var kraken = Kraken(
background: Colors.green,
bundle: WebFBundle.fromUrl('assets:///jss/bundle.js'),
javaScriptChannel: javaScriptChannel,
// 🙂 坑呐!开启 DevToolService 会导致内存泄露
devToolsService: ChromeDevToolsService(),
);
Step 5 - Kraken 的跨平台通信
Kraken 的跨平台通信无非要打通两条路,即:
- Native —— Flutter
- Flutter —— Kraken
打通两条路其实也很简单,在官方文档都有教程,在 Kraken React 部分跟大家稍微提个醒,当 JS 项目通过 Kraken cli 运行后,Kraken 会在其 JS window 对象挂上 kraken 对象,如果你用的是 Kraken 的 Fork 项目 OpenWebF,那 window 上挂载的对象则是 webf。具体通信代码如下(Demo 节选,后续会抽成 Bridge Library):
// React JS Demo
const krakenObj = window.kraken;
// Kraken invoked Flutter method
krakenObj.methodChannel.invokeMethod('onJSCall', new Date().getTime().toString(), ['Param Two'], {
value: 'Param Three',
})
.then(result => {
console.log('Received reply from Kraken', result);
setNativeReply(result);
})
.catch(err => {
console.log('Some error occured', err);
});
// Flutter invode Kraken method
krakenObj.methodChannel.clearMethodCallHandler();
krakenObj.methodChannel.addMethodCallHandler((method, args) => {
var request = method + ' method invoked' + '\n' + 'Its params is : ' + args;
console.log('Received request from Kraken : ' + request);
setNativeRequest(request);
});
四、渲染性能
如上图所示,两对图片分别展示了渲染 FlutterView Dart UI 和 Kraken UI 各个环节所占用的时间。而每对照片左右两张对比的则是:首次加载和复用 Engine 的二次加载
对其「关键性能数据」做一番记录可得:
渲染场景 | 初始化 FlutterView | 创建 FlutterEngine | 连接 View to Engine | 渲染 Dart UI | 渲染 JS UI | 总耗时 |
---|---|---|---|---|---|---|
首次渲染 FlutterView | 3ms | 75ms | 11ms | 744ms | \ | 833ms |
二次渲染 FlutterView | 0ms | 2ms | 7ms | 142ms | \ | 151ms |
首次渲染 Kraken FlutterView | 2ms | 72ms | 16ms | 869ms | 133ms | 1092ms |
二次渲染 Kraken FlutterView | 0ms | 1ms | 7ms | 170ms | 78ms | 256ms |
当然上面的数据只是参考,跟机型、跟代码的统计方式都有关系,但总的来说,在初始化一个 FlutterEngine 后对其复用,所达到的总耗时是可接受的。
五、写在最后
网传 Kraken 不在维护了?
事实确实如此,原有的团队已不再维护 Kraken,但另起炉灶,基于 Kraken fork 了 OpenWebF,目前支持 Flutter 3.0,其用法除了 API 的名字要从 Kraken 改成 WebF 外,其他使用方式与 Kraken 并没有什么不同。注意前端项目在 Debug 的时候应换成 OpenWebF cli 运行。
Kraken 是个好东西,思路好,具有学习价值,也能实际解决我们的业务痛点,业内一些大厂的轮子也有一些是基于 Kraken,Kraken 在跨端渲染也算是一个里程碑了。
Demo Github
- Android : github.com/TDForLife/N…
- React : github.com/TDForLife/k…
转载自:https://juejin.cn/post/7145699631609954335