揭秘滴滴开源Unify如何解决Flutter与原生通信难题

发表时间: 2024-08-16 10:16

引言

移动开发领域,移动跨端技术因其提效收益,逐渐成为业界趋势之一。Flutter 作为近年来热门的跨端技术,以高性能、自渲染、泛跨端著称,得到广泛应用。在滴滴国际化业务中,我们大量应用 Flutter。目前已在滴滴国际化外卖、滴滴国际化出行司机端等业务中大规模落地,整体交付提效 50%+,收益显著。在大规模 Flutter 跨端场景下,存量的原生业务与增量 Flutter 业务间的双向通信成为痛点问题。


为此,滴滴国际化外卖自研 Unify 框架,旨在解决大规模跨端落地场景下,Flutter 与原生模块之间的通信问题。Unify 通过平台无关的模块抽象、灵活的实现注入、自动代码生成等特性,为开发者提供高效、灵活、易用的 Flutter 混合通信能力。


基于 Unify,滴滴国际化外卖成功将 16+ 个原生平台能力,26+ 个原生业务能力高效导入 Flutter,并沉淀出 UniFoundation、UniBusiness 两套业务架构模式,有效支撑外卖业务从零到一实现 Flutter 跨端落地。同时,Unify 也在滴滴国际化出行司机端中推广落地,有效支撑了兄弟业务的大规模跨端落地。


目前,Unify 已作为滴滴开源项目,正式开源,欢迎大家试用、体验、star 支持!


背景

在跨端落地过程中,通常会保留原生实现,以迭代方式逐步试水跨端,先跑通模式,再逐渐扩大跨端落地规模。


在原生代码与 Flutter 代码并存前提下,面临一系列实际问题:

1. 大量原生 SDK 如何高效导入 Flutter?

2. 大量业务功能如何高效导入 Flutter?

3. Flutter 功能模块如何导出给原生?

此类 Flutter 与原生代码间的双向通信问题,我们统称为混合通信问题


针对这一问题,Flutter 官方提供了 Channel 通信方案,但在大规模落地场景下,该方案存在一系列不足:


  1. 手动解析参数引发异常:使用 Channel 需要手动解析调用参数,极易出错。当接口发生变化时,需重新适配也极易引入 Bug。该问题在线上经常出现,并且难以根治。
  1. 大规模导出难以维护:在大规模能力导出场景下,需要编写大量分支语句和硬编码,难以维护。
  2. 代码封装繁琐:Channel API 较为底层,开发者还需二次封装,才能提供给业务方便调用,这一过程较为繁琐。


除 Channel 外,Pigeon 是一个更加强大的解决方案。Pigeon 由 Google 推出,该方案基于代码生成技术,有效提升了工程质量,降低了接入成本。但通过实际使用,我们发现在大规模模块导出场景下,Pigeon 的开发效率还有进一步提升的空间。


基于这一背景,Unify 通过批量接口声明、批量模块生成,简化了工程复杂度,进一步提升了开发效率。同时,Unify 也逐渐演化出自身特色,比如更加符合开发者习惯的多工程文件组织方式。

Unify 介绍

Unify 由滴滴出行国际化外卖团队自研,目前已经广泛应用于滴滴国际化外卖及国际化出行业务,有力支撑了业务的 Flutter 化进程。


Unify 的亮点特性包括:

  • 平台无关的模块抽象: 允许开发者使用 Dart 语言声明与平台无关的模块接口与实体。
  • 灵活的实现注入: 开发者可以灵活地选择注入原生实现(Android/iOS)或 Flutter 实现。
  • 自动代码生成: 借助强大的代码生成引擎,Unify 可以自动生成 Flutter、Android、iOS 多平台下统一调用的 SDK。


下面是一个使用 Unify 声明原生模块的示例:

@UniNativeModule()abstract class DeviceInfoService {  Future<DeviceInfoModel> getDeviceInfo();}

通过 Unify,上面的 Dart 接口可以自动映射到 Android、iOS、Flutter 平台,开发者只需在各平台下填入具体实现即可。在 Flutter 中使用时,调用方式就像普通的 Flutter 模块一样简单、直观:

DeviceInfoService.getDeviceInfo().then((deviceInfoModel) {  print("${deviceInfoModel.encode()}");});

Unify 的整体架构如下:


Unify 核心概念


在进行混合通信开发时,典型场景包括

  • 将原生模块导出至 Flutter 调用
  • 将 Flutter 模块导出至原生调用
  • 在接口中传递复杂实体类

在 Unify 中定义了一系列核心概念,能够高效满足上述场景。以上场景分别对应于 UniNativeModule、UniFlutterModule、UniModel。


在具体使用时,开发者首先声明模块接口,接口声明使用 Dart 语言,以抽象类形式编写。接下来执行 Unify 代码生成器,生成器会分析接口声明,并通过代码生成技术,生成两部分实现:


  1. 实现注入接口:用于开发者注入实现逻辑
  1. 对于原生模块导出至 Flutter 场景,使用 UniNativeModule 声明模块,Unify 会在原生侧 (Android、iOS)生成注入接口。
  2. 对于 Flutter 模块导出至原生场景,使用 UniFlutterModule 声明模块,Unify 会在 Flutter 侧生成注入接口。

  1. 三端统一调用接口
  1. Unify 会在 Android(Java)、iOS(Objective-C)、Flutter(Dart)生成三端统一调用接口。
  2. 在任意一端,都能在对应语言下,使用同样的模块接口签名,调用导出能力。
  3. 值得一提的是,Unify 支持使用 UniModel 声明可嵌套实体类,在三端下也会生成对应实体类,开发者在任意一技术栈下都可操作实体类,由 Unify 抹平底层序列化、反序列化通信,大幅提升开发体验与质量。

整体流程如下图所示:


具体来说:


概念

描述

举例

UniNativeModule

声明一个模块,该模块的实现在原生(Android/iOS)注入。

通过 Unify 生成后,将生成三端(Android/iOS/Flutter)下的调用接口,实现统一调用。

UniFlutterModule

声明一个模块,该模块的实现在 Flutter 注入。

通过 Unify 生成后,将生成三端(Android/iOS/Flutter)下的调用接口,实现统一调用。

UniModel

Unify 提供的模板注解之一,主要作用:

  • 创建自定义实体(Model/Entity)。

跨端传输时,可以把它的对象实体作为参数,

直接跨端发送。


Getting Start

前面的介绍有些抽象,在本节中,我们将通过实际案例,看是如何将原生模块是导入 Flutter中,来进行介绍的。


在本节中,假设有一个系统信息 SDK,在 Android、iOS 下分别实现。现在我们需要对两端进行封装,向 Flutter 侧提供统一能力。基于 Unify,这一任务能够快速、简单、高效、高质量完成。

注:完整代码实现可于文末点击「阅读原文」查看。


Step1:模块声明

第一步,开发者需要对模块接口进行声明。在 Flutter 工程根目录下创建一个 interface 目录,所有 Unify 的模块声明均位于该目录中。

interface 下包含两个声明文件,均以 Dart 抽象类方式编写。


device_info_service.dart

声明原生模块

// device_info_service.dart@UniNativeModule()abstract class DeviceInfoService {  /// 获取设备信息  Future<DeviceInfoModel> getDeviceInfo();}

@UniNativeModule 注解表示该模块的实现由原生侧提供。


device_info_model.dart

声明返回值 Model

// device_info_model.dart@UniModel()class DeviceInfoModel {  /// 系统版本  String? osVersion;  /// 内存信息  String? memory;  /// 手机型号  String? plaform;}

@UniModel 注解表示这是一个跨平台的数据模型。


值得一提的是:

  1. Unify 并不限制接口参数的数量,并且参数支持基本类型、List/Map 容器(支持范型)以及实体类。
  1. 在 Unify 中,实体类支持任意嵌套。
  2. 通过 Unify 生成器,interface 中声明的实体类(UniModel)将同时生成 Android(Java)、iOS(Objective-C)、Flutter(Dart)实现代码,在任意一端下,开发者都以同样方式使用实体类,由 Unify 实现底层序列化、反序列化及透传。


Step2:执行 Unify 生成器

接口声明完成后,执行如下命令生成跨平台代码:

flutter pub run unify api\  --input=`pwd`/interface \  --dart_out=`pwd`/lib \  --java_out=`pwd`/android/src/main/java/com/example/uninativemodule_demo \  --java_package=com.example.uninativemodule_demo \  --oc_out=`pwd`/ios/Classes \  --dart_null_safety=true \  --uniapi_prefix=UD

在命令中,指定了 interface 接口目录,Android、iOS 输出位置等配置信息。


在 2.1 节中说到,对于 UniNativeModule,将会生成两部分代码:

  1. 实现注入接口:
  1. Android:DeviceInfoService.java、DeviceInfoServiceRegister.java
  2. iOS:DeviceInfoService.h、DeviceInfoService.m
  1. 三端统一调用接口:
  1. Flutter:main.dart
  2. Android:MainActivity.java
  3. iOS:AppDelegate.m

注:代码文件源自
Unify/example/01_uninativemodule_demo


值得一提的是:

  1. 除了 Flutter 调用接口外,Unify 也会在 Android 和 iOS 工程内分别以 Java、Objective-C 生成双端调用接口。供开发者在任何一端下,都可以用同样的方法、同样的实体类进行调用。这对于跨端场景下的代码一致性来说,意义是巨大的,避免了跨端多技术栈下,模块抽象不一致的问题。
  2. 本例是将原生模块导入 Flutter,使用 UniNativeModule,在原生侧提供实现注入接口。如果是将 Flutter 模块导入原生,则使用 UniFlutterModule,将在 Flutter 侧提供注入接口。不论是 UniNativeModule 还是 UniFlutterModule,除了注入接口有区别外,上层的三端统一调用接口是完全一致的,这也体现了 Unify 平台无关的模块抽象的思想,这对于混合栈下的架构分层至关重要。

Step3:注入原生实现

有了实现注入接口,开发者根据接口分别补充 Android、iOS 端实现。关键代码如下:

Android 实现

public class DeviceInfoServiceImpl implements DeviceInfoService {    @Override    public void getDeviceInfo(Result<DeviceInfoModel> result) {        DeviceInfoModel model = new DeviceInfoModel();        ......        result.success(model);    }}

iOS 实现

// DeviceInfoServiceVendor.h@interface DeviceInfoServiceVendor : NSObject<DeviceInfoService>@end// DeviceInfoServiceVendor.m@implementation DeviceInfoServiceVendorUNI_EXPORT(DeviceInfoServiceVendor)......#pragma mark - DeviceInfoService协议 实现- (void)getDeviceInfo:(void(^)(DeviceInfoModel* result))success fail:(void(^)(FlutterError* error))fail {    DeviceInfoModel *model = [DeviceInfoModel new];    ......    success(model);}@end


对于完整代码,可参见文末「阅读原文」:

  • Android 平台实现:DeviceInfoServiceImpl.java
  • Android 平台注册实现:MainActivity.java
  • iOS 平台实现类:DeviceInfoServiceVendor.h、DeviceInfoServiceVendor.m
  • iOS 平台注册实现:AppDelegate.m

注:代码文件源自 Unify/example/01_uninativemodule_demo


Step4:在 Flutter 中调用

一切就绪! 在 Flutter 代码中,现在可以直接调用 Unify 封装的原生模块了:


模块调用

OutlinedButton(  child: const Text("获取设备信息"),  onPressed: () {    DeviceInfoService.getDeviceInfo().then((deviceInfoModel) {      setState(() {        _platformVersion = "\n${deviceInfoModel.encode()}";      });    });  },),


效果截图


至此,你已经成功通过 Unify 将一个原生模块导入并在 Flutter 中使用。就像调用 Flutter 模块一样简单、直观!


小结

通过这个示例,我们体验了 Unify 带来的价值:

  1. 统一模块声明: 在任何平台下,统一的模块接口声明,避免实现不一致
  1. UniModel: 支持跨平台透明传输的数据模型
  1. 相比 Flutter 原生 Channel 方式:
  1. 避免手动解析参数易出错
  2. Android、iOS 双端自动对齐
  3. 大量 Channel 自动生成,易于维护
  4. 复杂实体无缝序列化,降低管理成本

我们总结了如下决策流程,方便大家根据场景需要,选择 UniNativeModule、UniFlutterModule:


Unify 核心原理

Unify 之所以能提升跨端通信的开发效率,关键在于 Unify 实现了一套多语言代码生成器,通过该生成器,能够自动解析开发者声明的 Dart 抽象接口,并自动生成三端注入、调用代码,将开发者从繁重的胶水代码中解脱出来。在本节中,介绍 Unify 底层代码生成原理,并介绍与同类方案的对比。


Dart 代码静态分析

我们选择 Dart 语言作为模块接口声明语言,并基于 Dart Analyzer 库,实现对接口声明的静态分析,将 Dart 源代码转换为 Dart AST。在 Unify 中,我们基于 Dart AST 定义了 Unify AST,这是一套适用于模块导出场景的简化 AST,特色为内置了对多语言(Java、Dart、Objective-C)代码生成的映射关系,保证了后续多语言代码生成器实现的简洁。


从开发者接口声明,通过 Dart Analyzer 库静态分析,到产出 Unify AST 的整体流程如下:

Unify 多语言代码生成器

基于这套 Unify AST,Unify 自研了一套多语言代码生成器,能够基于一套 AST 同时生成多端、多语言代码(Java、Dart、Objective-C),这也是 Unify 高效开发的关键。


在 Unify AST 中,我们抽象了多种抽象语法节点,每种节点中,都包含对多种语言的生成映射关系:


Unify AST


Unify AST 节点多语言映射


基于 Unify AST,以 UniModel 为例,开发者声明的 UniModel 将被转换为 Model AST 实例:

有了 Model AST,Unify 声明了 UniModel 在多端下的生成代码模板。在 Unify 中,我们自研了一套类似于 Flutter 组件化的代码生成模板语法,相较于其它框架手动拼接字符串的方式,Unify 代码生成模板结合 Unify AST 具备更高的模版编写效率,同时代码质量和可维护性更高。以 UniModel 为例,部分模版如图:



Unify 代码生成器的作用是将 UniModel 的 Model AST 与各技术栈下的生成模版相结合,从而生成 UniModel 在各平台下的多语言实现。最终的生成代码如图:


同类方案对比

Unify 通过平台无关的模块抽象、灵活的实现注入、自动代码生成等特性,为开发者提供高效、灵活、易用的 Flutter 混合通信能力。同时,Unify 也逐渐演化出自身特色,比如参数支持任意嵌套的实体类、集合类范型,以及贴近 Flutter 开发者的纯 Dart 语言的接口声明方式。


Unify 还支持批量接口声明、批量模块生成,简化了工程复杂度,进一步提升了开发效率。外卖大规模 Flutter 落地之初,面临数10+基础能力的批量导出,如果逐个搭建 Git 库导出,维护成本和导出成本过高。基于 Unify 的批量导出能力,我们在短时间内完成了对平台能力的批量封装。


基于前文的使用介绍、原理介绍,相信大家对 Unify 有了深入的了解。在本节中,我们将 Unify 与其它同类框架对比,帮助大家选型、决策。



通过对比可以看出,不同方案各有特色,适合于不同的场景。概括来说,如果业务中有大量封装导出场景,Unify 能够实现更高的批量导出效率,同时保持了较低的工程复杂度,易于维护。如果是对单模块进行封装导出,或者需要支持更多语言,尤其是 C++ 封装支持,Pigeon 则是较好的选择。

Unify 业务最佳实践

在滴滴国际化外卖业务 Flutter 大规模落地的初期,面临十余个公司平台能力 SDK 需要导出的 Flutter 侧,同时业务中存在大量混合通信,需要保证高可靠性。基于这一背景,在调研已有方案后,我们自研了 Unify,解决了大量模块的批量导出问题。并且在此过程中,我们沉淀出两套架构模式 UniFoundation 和 UniBusiness,成为业务混合通信最佳实践。


UniFoundation 是我们基于 Unify,高效完成公司16+ 个 SDK 批量导出,形成一套能够在 Android、iOS、Flutter 三端统一调用的基建能力。UniFoundation 是一套可复用基建,支撑了国际化外卖商家端、用户端、骑手端三端 Flutter 大规模落地。同时,作为通用基建,UniFoundation 成功推广到国际化出行司机端,助力兄弟业务的 Flutter 大规模落地,并实现跨团队合作共建。


在 UniFoundation 落地之后,在各端业务中,也存在大量业务模块与 Flutter 之间混合通信的场景,于是我们沿用 UniFoundation 的模式延伸出 UniBusiness。UniBusiness 是业务端内部,基于 Unify 批量抽象出的平台无关的业务模块,能够在三端,以统一的方式实现模块调用、复杂实体透传。随着 Flutter 落地规模的扩大,有越来越多业务模块由 Flutter 实现,并经过 Unify 封装,实现三端统一调用。


UniFoundation 和 UniBusiness 在业务中多端落地如图所示:

落地收益

滴滴国际化外卖业务包含用户端、骑手端、商家端三端,目前均已实现 Flutter 大规模业务落地,并且 Flutter 均已覆盖各端核心主流程,实现跨端复用,整体交付提效 50%+,收益显著,并且是一项持续性提效的收益。其中,国际化外卖骑手端 90%+ 以上代码均为 Flutter 跨端实现,已线上稳定运行两年多时间。


目前,Unify 已成为滴滴 DiFlutter 技术体系的核心架构组件之一,稳定支撑着各端业务,并在业务中大量使用,解决了基础模块、业务模块的混合通信问题,彻底解决了由 Channel 通信导致的参数手动解析错误、Android/iOS 双端接口抽象不一致等问题。


滴滴国际化外卖 Flutter 部分业务落地场景展示:


总结与未来展望

滴滴国际化外卖在完成大规模 Flutter 跨端落地之后,我们意识到 Flutter 跨端仍然存在进一步提效空间,目前在向纯 Flutter 化方向演进。对于未来 Unify 的演进,我们希望将 Unify 打造成一套 Flutter 混合开发领域的标准化解决方案,帮助业务解决 Flutter 大规模落地过程中的痛点难点问题。


目前,Unify 已经完成混合通信能力的沉淀,未来我们将持续迭代,提供更多功能,让跨端混合通信开发更加高效、可靠。今年上半年,我们也调研了 Flutter PlatformView 嵌原生能力,目前 Unify 正在提供一套基于嵌原生的混合路由方案,解决大规模 Flutter 落地场景下的混合页面跳转问题。


新的混合路由相较于业界已有方案,更加轻量化,大幅降低复杂度。我们希望这套路由能够助力业务,向纯 Flutter 化方向演进、过渡。经过多年验证稳定后,我们也荣幸得将 Unify 作为滴滴官方开源项目,将这套实践分享给业内同行。欢迎大家试用、体验、star 支持!

作者:刘瑞刚

来源-微信公众号:滴滴技术

出处
:https://mp.weixin.qq.com/s/Di8czdY3KCqDAYrzEvePrg