轻量化改造的意义
轻量级Flutter渲染引擎的核心是将Flutter作为一个「渲染器」,它的唯一功能就是将Native端传来的数据绘制成相应的界面,其它所有交互操作,都通过Channel桥接到Native端进行处理,这样做的好处有下面几点:
- 复用Native侧的所有网络请求逻辑,避免因为引入第二套网络库,导致多端请求不一致的问题
- 复用Native的已有本地图片资源,减少重复的资源浪费
- 降低混合栈逻辑复杂度,利用EngineGroup来管理混合栈渲染,而数据内容桥接于Native侧,从而解决EngineGroup数据隔离的问题
可能有人要说了,为什么要做轻量化改造,直接在Flutter中使用不好吗,就像网络请求,在Flutter中接入DIO等网络库,同样也不复杂。
的确,对于很多项目来说,引入Flutter的意义在于降低开发成本,提高开发效率,但是当你在一个相对比较成熟的原生项目上进行混编的时候,如果Flutter的所有能力都需要重新实现,那么前期代价是相对较高的,就拿网络请求来说,在Flutter内部将请求数据全部包掉后,Flutter需要实现原生网络请求的所有逻辑,例如拦截器,加密,重定向等等功能,同时,如果以后对网络逻辑有所改动,那么原生侧和Flutter都需要进行调整。
所以,Flutter轻量化改造重要原因,就是需要「尽可能多的复用原生已有的逻辑」,例如图片框架、网络、埋点,而不是在Flutter中去全部再实现一遍。
同时,Flutter轻量化改造也是对EngineGroup架构的最佳实践,在EngineGroup架构下,我们需要将数据源放到原生侧,从而保证多Engine的数据共享。
最后,Flutter轻量化改造,也是渐进式接入混编Flutter的最佳方式,这种方式可以以比较小的前期基建成本来快速接入Flutter来提高开发效率,同时在后期大量接入Flutter后替换为完全的Flutter开发,可以非常方便的将接口层替换。
轻量化改造实践
首先,我们通过Pigeon生成接口协议和调用代码,原生侧分别基于当前协议来进行开发。
不过,我们需要解决Pigeon CLI脚本只能有一个协议文件的问题。
根据前面的几篇文章,我们修改下之前的代码,先根目录下创建Pigeon文件夹,将不同的协议,分别写入不同的协议文件,例如:SchemaBookSearchAPI、SchemaUserAPI等等。
然后修改之前的run_pigeon.sh脚本。
代码语言:javascript复制#!/bin/sh
cd pigeon
for file in `ls`;do
filename=${file%.*}
flutter pub run pigeon --input pigeon/${file%.*}.dart
--dart_out lib/${file%.*}_api.dart
--java_out ../QDReaderGank.App/src/main/java/com/qidian/QDReader/flutter/${file%.*}Api.java
--java_package "com.qidian.QDReader.flutter"
done
脚本其实很简单,就是在Pigeon目录下,循环所有的文件来分别执行原有的CLI脚本。
这样就会生成多个协议的不同调用文件,分别对应不同协议的实现。
在这个方案下,每个业务场景会创建一个XXXFlutterActivity,并在XXXAPI下,由Native侧分别创建不同的协议实现。
但是这个方案有一个致命的缺陷,那就是原本是为了提高效率而引入的Flutter,在这个场景下,依然需要原生侧的人力来进行开发,虽然工作量不大,但是能否将这部分人力也去掉呢?
所以,我们需要对轻量化Flutter框架做进一步改造。
首先,依然是借用Pigeon的那一套东西,生成相应的Channel代码,之所以要使用Pigeon来生成代码的原因,主要还是Pigeon使用了BasicMessageChannel来进行Channel通信,效率相对于几种不同的Channel来说是最高的,其次,生成代码屏蔽了Channel的一些原始调用方法,使得调用更加方便了。
所以,我们现在只保留一套通用协议,该协议中只包含3个方法,Get请求、Post请求和ActionURL调用。
代码语言:javascript复制import 'package:pigeon/pigeon.dart';
@HostApi()
abstract class NativeNetApi {
@async
String getNativeNetBridge(String path, Map<String, Object> params);
@async
String postNativeNetBridge(String path, Map<String, Object> params);
void doActionUrlCall(String actionUrl);
}
接下来,依然是通过Pigeon生成三方的协议代码,在Android中,我们创建一个通用的FlutterActivity,并实现协议中关于网络请求的方法,借助前面几节的内容,我们可以很方便的实现下面的代码。
代码语言:javascript复制class SingleFlutterActivity : FlutterActivity() {
private val engine: FlutterEngine by lazy {
val app = activity.applicationContext as QDApplication
val dartEntrypoint =
DartExecutor.DartEntrypoint(
FlutterInjector.instance().flutterLoader().findAppBundlePath(),
intent.getStringExtra("EntryName").toString()
)
app.engines.createAndRunEngine(activity, dartEntrypoint)
}
companion object {
@JvmStatic
fun start(context: Context, flutterEntryName: String) {
context.startActivity(Intent(context, SingleFlutterActivity::class.java).also {
it.putExtra("EntryName", flutterEntryName)
})
}
}
private class NetBridgeApiImp(val context: Context, val lifecycleScope: LifecycleCoroutineScope) : NetBridgeApi.NativeNetApi {
override fun getNativeNetBridge(path: String?, params: MutableMap<String, Any>?, result: NetBridgeApi.Result<String>?) {
path?.let {
lifecycleScope.launch {
try {
val data = XXXRetrofitClient.getCommonApi().getNetBridge(path, params)
result?.success(data.toString())
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}
override fun postNativeNetBridge(path: String?, params: MutableMap<String, Any>?, result: NetBridgeApi.Result<String>?) {
path?.let {
lifecycleScope.launch {
try {
val data = XXXRetrofitClient.getCommonApi().postNetBridge(path, params)
result?.success(data.toString())
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}
override fun doActionUrlCall(actionUrl: String?) {
if (context is BaseActivity) {
context.openInternalUrl(actionUrl)
}
}
}
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
NetBridgeApi.NativeNetApi.setup(flutterEngine.dartExecutor, NetBridgeApiImp(this, lifecycleScope))
}
override fun provideFlutterEngine(context: Context): FlutterEngine {
return engine
}
override fun onDestroy() {
super.onDestroy()
engine.destroy()
}
}
这样在Start这个Activity的时候,传入对应Flutter的路由名即可路由到对应的Flutter页面。
代码语言:javascript复制SingleFlutterActivity.start(activity, "main");
而在Flutter界面中,可以通过协议非常方便的调用原生方法。
代码语言:javascript复制void _loadData() async {
String result = await NativeNetApi().getNativeNetBridge(
"/apipath/xxxxx",
{"itemId": 1111, "pg": 1, "pz": 20},
);
setState(() {
model = BookModel.fromJson(json.decode(result)).data?.items ?? [];
});
}
这样一来,原生侧只需要搭建好一套类似JSSDK的环境即可满足混编开发的需求,不用再根据不同的接口来进行重复的开发,而Flutter一侧,只需要设置API path和参数即可。
最后,我们需要在原生侧增加通用接口的封装即可,首先,实现通用的Get和Post请求。
代码语言:javascript复制@GET("{path}")
suspend fun getNetBridge(
@Path(value = "path", encoded = true) path: String,
@QueryMap param: @JvmSuppressWildcards Map<String, Any>?,
): JsonObject
@FormUrlEncoded
@POST("{path}")
suspend fun postNetBridge(
@Path(value = "path", encoded = true) path: String,
@FieldMap mapParam: @JvmSuppressWildcards Map<String, Any>?,
): JsonObject
原生侧网络依然使用OKHttp进行封装,这里有一个需要注意的就是在Kotlin中使用Retrofit,如果参数类型是Any的话,需要使用@JvmSuppressWildcards注解来将Any标记为Object类型。
通过上面的操作,我们就打通了整个链路。
❝其它对应需要桥接原生的能力,只需要新增接口即可,例如埋点,新增曝光和点击接口,在Flutter中调用协议即可实现。 ❞
轻量化下的开发流程
在使用Flutter开发新的业务需求时,首先需要在Flutter中创建相应的路由名,然后在main中配置相应的业务页面,接下来即可进行正常的Flutter业务开发,在网络请求等需要桥接原生的地方,利用接口协议进行桥接,在接口还未上线时,可以通过Mock的方式进行调试,或者在Flutter中增加一层Mock配置,这样可以以不参与原生编译的方式单独进行开发,极大的利用了Flutter的开发效率高的特性。
在接口上线后,即可发布aar到原生项目,从而参与调试。
这样就完成了整个改造的闭环,使用轻量级Flutter框架进行业务开发,缩减了一半的原生人力成本,同时也提高了UI的统一程度,方便的视觉走查,另外,对相应的测试成本也有缩减,大部分功能只需要在一个平台上进行测试,其它一些兼容性测试,在分端设备上测试即可。
性能Benchmark
大数据量场景
使用Mock接口数据的方式测试,字符数120000,应该是常规开发中比较大的接口,经测试,可以正常传递数据。
- 测试方法:Mock Native请求接口数据,替换为新的数据,获取数据后展示到界面上。
- 测试结果:Channel耗时统计10次,Debug包下,均值在12ms左右,Release包下,均值在7ms左右,满足使用条件。
频繁请求场景
使用普通接口数据,连续请求10次,目前常规开发中的接口请求场景,大部分为1到3次,可以满足几乎目前所有的使用场景。
- 测试方法:循环10次,连续调用Native API获取接口数据,并在界面展示返回数据。
- 测试结果:测试通过,数据正常请求并展示。
通过上面两个测试场景,可以得出结论,该方案具有可行性。