查看原文
其他

Flutter混编工程之轻量化改造

徐宜生 群英传 2022-12-21

点击上方蓝字关注我,知识会给你力量


轻量化改造的意义

轻量级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脚本。

#!/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调用。

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,并实现协议中关于网络请求的方法,借助前面几节的内容,我们可以很方便的实现下面的代码。

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页面。

SingleFlutterActivity.start(activity, "main");

而在Flutter界面中,可以通过协议非常方便的调用原生方法。

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请求。

@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获取接口数据,并在界面展示返回数据。

  • 测试结果:测试通过,数据正常请求并展示。

通过上面两个测试场景,可以得出结论,该方案具有可行性。

向大家推荐下我的网站 https://xuyisheng.top/  点击原文一键直达

专注 Android-Kotlin-Flutter 欢迎大家访问



往期推荐


本文原创公众号:群英传,授权转载请联系微信(Tomcat_xu),授权后,请在原创发表24小时后转载。
< END >
作者:徐宜生

更文不易,点个“三连”支持一下👇


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存