|
滴滴开源新项目Unify:聚焦Flutter与原生通信难题,助力跨端应用落地
刘瑞刚
滴滴技术
滴滴技术 滴滴商业服务有限公司 滴滴官方技术号 185篇内容
2024年08月15日 20:30
北京
引言在移动开发领域,移动跨端技术因其提效收益,逐渐成为业界趋势之一。Flutter 作为近年来热门的跨端技术,以高性能、自渲染、泛跨端著称,得到广泛应用。在滴滴国际化业务中,我们大量应用 Flutter。目前已在滴滴国际化外卖、滴滴国际化出行司机端等业务中大规模落地,整体交付提效 50%+,收益显著。在大规模 Flutter 跨端场景下,存量的原生业务与增量 Flutter 业务间的双向通信成为痛点问题。为此,滴滴国际化外卖自研 Unify 框架,旨在解决大规模跨端落地场景下,Flutter 与原生模块之间的通信问题。Unify 通过平台无关的模块抽象、灵活的实现注入、自动代码生成等特性,为开发者提供高效、灵活、易用的 Flutter 混合通信能力。基于 Unify,滴滴国际化外卖成功将 16+个原生平台能力,26+个原生业务能力高效导入 Flutter,并沉淀出 UniFoundation、UniBusiness 两套业务架构模式,有效支撑外卖业务从零到一实现 Flutter 跨端落地。同时,Unify 也在滴滴国际化出行司机端中推广落地,有效支撑了兄弟业务的大规模跨端落地。目前,Unify 已作为滴滴开源项目,正式开源,欢迎大家试用、体验、star 支持!背景在跨端落地过程中,通常会保留原生实现,以迭代方式逐步试水跨端,先跑通模式,再逐渐扩大跨端落地规模。在原生代码与 Flutter 代码并存前提下,面临一系列实际问题:1.大量原生 SDK 如何高效导入 Flutter2. 大量业务功能如何高效导入 Flutter?3. Flutter 功能模块如何导出给原生?此类 Flutter 与原生代码间的双向通信问题,我们统称为混合通信问题。针对这一问题,Flutter 官方提供了 Channel 通信方案,但在大规模落地场景下,该方案存在一系列不足:手动解析参数引发异常:使用 Channel 需要手动解析调用参数,极易出错。当接口发生变化时,需重新适配也极易引入 Bug。该问题在线上经常出现,并且难以根治。大规模导出难以维护:在大规模能力导出场景下,需要编写大量分支语句和硬编码,难以维护。代码封装繁琐: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 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 代码生成器,生成器会分析接口声明,并通过代码生成技术,生成两部分实现:实现注入接口:用于开发者注入实现逻辑对于原生模块导出至 Flutter 场景,使用 UniNativeModule 声明模块,Unify 会在原生侧 (Android、iOS)生成注入接口。对于 Flutter 模块导出至原生场景,使用 UniFlutterModule 声明模块,Unify 会在 Flutter 侧生成注入接口。三端统一调用接口Unify 会在 Android(Java)、iOS(Objective-C)、Flutter(Dart)生成三端统一调用接口。在任意一端,都能在对应语言下,使用同样的模块接口签名,调用导出能力。值得一提的是,Unify 支持使用 UniModel 声明可嵌套实体类,在三端下也会生成对应实体类,开发者在任意一技术栈下都可操作实体类,由 Unify 抹平底层序列化、反序列化通信,大幅提升开发体验与质量。整体流程如下图所示:具体来说:概念描述举例UniNativeModule声明一个模块,该模块的实现在原生(Android/iOS)注入。通过 Unify 生成后,将生成三端(Android/iOS/Flutter)下的调用接口,实现统一调用。UniFlutterModule声明一个模块,该模块的实现在 Flutter 注入。通过 Unify 生成后,将生成三端(Android/iOS/Flutter)下的调用接口,实现统一调用。UniModelUnify 提供的模板注解之一,主要作用:创建自定义实体(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 getDeviceInfo();}@UniNativeModule 注解表示该模块的实现由原生侧提供。device_info_model.dart声明返回值 Model// device_info_model.dart@UniModel()class DeviceInfoModel { /// 系统版本 String osVersion; /// 内存信息 String memory; /// 手机型号 String plaform;}@UniModel 注解表示这是一个跨平台的数据模型。值得一提的是:Unify 并不限制接口参数的数量,并且参数支持基本类型、List/Map 容器(支持范型)以及实体类。在 Unify 中,实体类支持任意嵌套。通过 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,将会生成两部分代码:实现注入接口:Android:DeviceInfoService.java、DeviceInfoServiceRegister.javaiOS:DeviceInfoService.h、DeviceInfoService.m三端统一调用接口:Flutter:main.dartAndroid:MainActivity.javaiOS:AppDelegate.m注:代码文件源自 Unify/example/01_uninativemodule_demo值得一提的是:除了 Flutter 调用接口外,Unify 也会在 Android 和 iOS 工程内分别以 Java、Objective-C 生成双端调用接口。供开发者在任何一端下,都可以用同样的方法、同样的实体类进行调用。这对于跨端场景下的代码一致性来说,意义是巨大的,避免了跨端多技术栈下,模块抽象不一致的问题。本例是将原生模块导入 Flutter,使用 UniNativeModule,在原生侧提供实现注入接口。如果是将 Flutter 模块导入原生,则使用 UniFlutterModule,将在 Flutter 侧提供注入接口。不论是 UniNativeModule 还是 UniFlutterModule,除了注入接口有区别外,上层的三端统一调用接口是完全一致的,这也体现了 Unify 平台无关的模块抽象的思想,这对于混合栈下的架构分层至关重要。Step3:注入原生实现有了实现注入接口,开发者根据接口分别补充 Android、iOS 端实现。关键代码如下:Android 实现public class DeviceInfoServiceImpl implements DeviceInfoService { @Override public void getDeviceInfo(Result result) { DeviceInfoModel model = new DeviceInfoModel(); ...... result.success(model); }}iOS 实现// DeviceInfoServiceVendor.h@interface DeviceInfoServiceVendor : NSObject@end// DeviceInfoServiceVendor.m@implementation DeviceInfoServiceVendorUNI_EXPORT(DeviceInfoServiceVendor)......#pragma mark - DeviceInfoService协议 实现- (void)getDeviceInfovoid(^)(DeviceInfoModel* result))success failvoid(^)(FlutterError* error))fail { DeviceInfoModel *model = [DeviceInfoModel new]; ...... success(model);}@end对于完整代码,可参见文末「阅读原文」:Android 平台实现:DeviceInfoServiceImpl.javaAndroid 平台注册实现:MainActivity.javaiOS 平台实现类:DeviceInfoServiceVendor.h、DeviceInfoServiceVendor.miOS 平台注册实现:AppDelegate.m注:代码文件源自 Unify/example/01_uninativemodule_demoStep4:在 Flutter 中调用一切就绪! 在 Flutter 代码中,现在可以直接调用 Unify 封装的原生模块了:模块调用OutlinedButton( child: const Text("获取设备信息"), onPressed: () { DeviceInfoService.getDeviceInfo().then((deviceInfoModel) { setState(() { _platformVersion = "\n${deviceInfoModel.encode()}"; }); }); },),效果截图至此,你已经成功通过 Unify 将一个原生模块导入并在 Flutter 中使用。就像调用 Flutter 模块一样简单、直观!小结通过这个示例,我们体验了 Unify 带来的价值:统一模块声明: 在任何平台下,统一的模块接口声明,避免实现不一致UniModel: 支持跨平台透明传输的数据模型相比 Flutter 原生 Channel 方式:避免手动解析参数易出错Android、iOS 双端自动对齐大量 Channel 自动生成,易于维护复杂实体无缝序列化,降低管理成本我们总结了如下决策流程,方便大家根据场景需要,选择 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 中,我们抽象了多种抽象语法节点,每种节点中,都包含对多种语言的生成映射关系:UnifyASTUnifyAST 节点多语言映射基于 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 支持!国际化外卖技术团队正在招聘服务端高级开发工程师、高级数据研发工程师,感兴趣的小伙伴欢迎联系ginasun@didiglobal.com,期待你的加入!更多项目开源信息,欢迎点击「阅读原文」了解!
|
|