本文主要介绍钉钉基于Flutter构建的跨四端应用框架(代号Dutter)。内容主要包括方案设计、最佳实践以及FlutterEngine层面的一些问题。.希望本文的分享能为有类似需求的团队提供一些参考。项目概况1.1什么是DutterDutter,即钉钉Flutter,是钉钉中基于Flutter构建的跨四端研发框架。Dutter项目“始于Flutter但超越了Flutter”。项目的主要目标是利用Flutter的跨平台能力,在不降低用户体验的前提下,提升钉钉端到端的研发效率,缓解钉钉端到端研发资源短缺、各端人力不平衡的问题.1.2目前进展目前,Dutter运营框架已经完成了钉钉的四端融合,并完成了一系列共创服务的灰度和试点。现阶段,钉钉基于Flutter的研发业务包括“日程签到”、“+面板”以及部分内部灰度业务:项目背景我们选择Flutter并启动Dutter项目主要有两个考虑:提高端到端效率-侧研发;跟进Flutter技术。让我们在下面解释这个简单的开发。2.1端侧研发效率提升随着钉钉发展到第七个年头,客户端“业务需求”、“研发资源”和“技术演进”的矛盾越来越激烈:有很多商业产品的优秀学生。为了实现idea,需要在各个端找TL去争取资源,并且因为研发资源不足,需要反复沟通其需求的商业价值;研发同学经常处于“1vsN”的状态,业务需求、稳定性保障、技术支持、BugFix等,每天的工作时间基本饱和;技术团队不仅要满足于现在,更要面向未来。在满足日常业务迭代的同时,我们还需要安排一些资源投入到满足未来3~5年发展的技术项目上。以上几点可以概括为一个问题:我们的技术研发资源不足。为解决上述问题,有两种途径:1、继续扩大技术团队规模;2.提高团队研发效率。从钉钉方面目前近150人的团队规模来看,整体规模不小,继续扩招难度较大。由于团队规模不能无限扩大,我们需要探索研发效率的提升空间:端侧技术同学分为5个平台,划分后每个平台的人手不够;基本处于“孤立”状态,不同平台下的学员无法互补;任何业务需求都需要4+以上的研发资源投入,任何一端人手不足都可能导致落地失败;一个逻辑难以实现多副本,导致完全一致,不同平台业务表现往往不一致,返工和专注进一步影响效率;业务上线后,不同平台分别维护,日常技术支持、BugFix等场景需要多次投入。可以看出,如果我们能够使用跨平台技术,技术同学可以使用“一码覆盖所有端”,而原本需要多个平台、多个同学分别做事的需求,到1~2个同学就可以大大提高我们研发效率。2.2Flutter技术跟进钉钉已经拥有“小程序”、“H5”等跨端技术。如果我们需要提高效率,是否可以直接利用已有的技术栈来达到目的呢?对于钉钉端到端团队来说,选择基于Web的跨平台方案在理论上是可行的,但在实践中很难达到预期的效果。主要原因在于两个方面:“小程序技术”是目前比较流行的跨端技术。其设计定位应满足三方生态多样性场景,架构设计着眼于“大而全”,而不是在单点上反复打磨。这与钉钉业务所强调的“专精”、“追求完美”不同;对于端端同学来说,前端开发模式入门门槛高,研发模式差异较大。高水平的开发需要一定的使用和开发经验,也就是说前期需要有一定的“试错空间”。也很难满足钉钉目前的在线质量要求。Flutter作为近几年发展起来的跨平台技术,有别于Web生态。它基于Native-like的架构设计,有选择地摒弃动态,更注重跨平台。在保证类Native性能和体验的基础上,赋予开发者“一次开发运行多个终端”的能力。所以,相比小程序技术,Flutter更适合解决我们端侧技术团队的痛点。另外,我们在对国内跨平台技术进行深入调研后发现,基于Flutter的跨平台项目后发优势明显,上限高,发展潜力大,更具有长期投资价值。在对业界跨平台解决方案的长期跟踪中,我们发现“自绘引擎”是现阶段的热点,而“自绘引擎”解决方案大多是在Flutter项目开放之后开始的来源并流行起来。这个时间点的巧合并非偶然。我们用下图来说明主流跨平台方案在技术实现上的区别:从上图可以看出,对于跨平台方案的设计者来说,Flutter项目最大的价值在于:提供一个开放的平台源码,设计良好,兼容性好,高性能,生态边界清晰的自绘引擎。基于这个开源的自绘引擎,有技能的团队可以将其应用到自己的跨平台解决方案中,只需稍作修改即可替换Native组件,复用Flutter的跨平台一致性能力,提升解决方案的商业和技术价值.对于钉钉来说,考虑到我们现阶段跨平台的投入和目标,不像其他解决方案那样推出自己的跨平台自画引擎。但从技术角度来说,选择基于Flutter的跨平台方案,一方面可以快速享受Flutter的技术红利,在交付产品的性能和质量上与其他主流方案保持一致;在中心培养相关技术团队,为后续更深层次的定制化改造做好技术储备。方案设计本章将简要介绍钉钉的Dutter跨端框架设计,并对具有代表性的问题做一些补充说明。3.1总体设计Dutter核心模块包括三大包:DutterRuntime;Dutter开发工具包;DutterOPS套件。整体如下图所示:DutterRuntime:建立在Flutter之上的Dutter运行环境是Dutter的核心部分。除了Flutter提供的基础功能外,我们还提供容器化组件、API插件、业务模块化框架等功能。并且基于集团的AliFlutter项目,进一步扩展了Aion的动态功能。DutterRuntime也是我们项目中一直全力投入到现在的部分;DutterDevKit:即开发包,主要目的是解决不同技术栈的同学在跨4+终端开发时的支持和效率问题。目前投入相对有限,未来可与钉钉研发平台整合;DutterOPSKit:运维套件,主要承载Dutter产品发布和运维相关功能,如行情监控等。目前投入相对有限,未来可以与钉钉研发平台整合。将上图展开,得到框架的整体模块图,大致如下:从下到上:左下角是“DutterRuntime”相关模块;右下角是“DutterOPSKit”相关模块;右上角是“DutterOPSKit”相关模块;DutterDevKit”相关模块,左上角为业务部分。3.2数据通信数据通信主要指Flutter与平台端的两种主要通信方式:Channel和FFI。Channel在Flutter应用中使用比较广泛。大部分Flutter与平台的通信就是基于这种模式,优点是集成度高,封装好,使用方便;缺点主要是通信效率高;FFI已经在Flutter2.0中实现,作为官方特性推出,其最大的特点是同步调用、内存共享、执行效率高,但在易用性和扩展性方面仍有提升空间1Channel关于Channel,钉钉的使用和官方文档没有本质区别。我想分享的经验在于Channel数量的管理,官方的原材料不涉及太多内容与渠道管理有关。从钉钉的实际使用体验来看,我们还是建议大家在一个业务中尽量汇聚到1个或2个Channel进行共享,并在共享Channel的基础上封装“响应”和“分发”接口供业务使用。这主要有以下好处:有利于性能稳定,有限的信道可以降低通信异常的概率,提高通信性能;有利于管理,尤其是在“单引擎/多引擎”共存模式下,通过合理的封装来平滑底层差异。以上两点,尤其是第二点,对于钉钉实现移动端与桌面端的兼容具有重要意义。正如《钉钉Flutter桌面应用解决方案》所述,我们现在在移动端采用单引擎架构,在桌面端采用多引擎架构。如果没有对Channel进行合理的封装,让业务同学直接注册并调用FlutterEngine,会大大增加多引擎模式下的代码管理成本,并且会造成移动端和桌面端实现的不一致。我们目前的做法是在Dutter框架内部封装FlutterEngine和Channel,对外暴露一个统一封装的上层接口实例:DutterMethodChannel。对于业务层代码,无需感知底层架构是单引擎模式还是多引擎模式,只需要按照统一的规则和模式注册或调用相关服务即可。通过这种模式,在降低业务使用复杂度的同时,也为底层框架的设计带来了极大的灵活性,为后续的移动端切换多引擎解决方案提供了强有力的支持。2FFIFFI已经在Flutter2.0中正式发布。与Channel相比,它最大的优势在于执行效率更高,更适合对性能要求更高的场景。本章不涉及FFI的具体使用方法,只是想和大家简单分享一下使用FFI时内存管理的注意事项。我们都知道现在的移动端开发(Java、OC、Swift)都有自动内存管理的机制;Flutter使用的dart语言也有基于垃圾回收的自动内存管理。各种语言都可以在自己的范围内按照自己的规则合理管理内存,保证内存空间的合理稳定的应用。但是,FFI作为一种跨函数直调的方式,基于内存共享机制简化了调用环节,但同时也对内存管理提出了更高的要求。在这种模式下,如果不能很好的管理(open&released)内存空间,极有可能造成野指针或者内存泄漏。在官方文档FlutterFFI和DartFFI章节的介绍中,对内存管理的描述比较有限。通过查阅相关接口资料,我们可以看到dart:ffi提供了一种手动管理内存的方式:在此基础上,我们可以定义DutterFFI内存管理策略。首先,我们需要准确定义核心原则:allocation和release同源:必须使用一套alloc和free算法,避免内存分配和释放因实现差异而出现异常;必须遵守"谁分配谁释放"的原则。我们在1和2的基础上,封装了FFI操作相关的接口和数据结构,统一到“DutterFFIBridge”模块中。在充分考虑了覆盖面和复杂度之后,除了DutterFFI接口中默认的基本类型外,我们只增加了对String类型的支持。对于其他数据类型,业务方可以通过序列化的方式传递。在传输过程中,对于定长字符串,可以直接通过“UTF-8编码的char*数组”进行传递;如果是变长字符串(比如调用的返回值),需要使用自定义数据结构DTFUInt8String来传递。具体实现:为了满足“分配和释放同源”的原则,在Dutter中,我们选择了dart:ffi中的allocate和free方法作为统一的分配和释放实现。Dutter框架在启动过程中会做一个接口绑定,将我们自定义的数据结构相关方法传递给Native端。Native端所有的FFI接口内存分配场景都是通过绑定接口来实现的:为了满足“谁分配谁释放”的原则,在DutterFFI接口中,我们默认约定了以下三个原则。在此基础上,可以保证堆内存的分配在DTFUInt8String的控制范围内。只要处理好DTFUInt8String对象的生命周期,就可以保证传输过程中内存管理的安全:3.3消息总线“消息总线”是钉钉的一个特性模块,我们主要是解决基于业务通信的问题在钉钉端不同的技术栈上:比如基于Flutter的业务想要通知一个基于小程序的页面刷新UI,可以通过消息总线实现功能:消息总线定位是一个轻量级的“端”到“端”超级通道,目标是让企业拥有跨运营环境无缝沟通的能力。从逻辑上讲,它包括三个模块:“总线”、“控制器”、“注册和发送”;在实现上,通过“消息持久化”、“流水线分级”、“权限控制”等方式,整体运行可靠、高效、安全。3.4模块化由于钉钉端到端业务的特点,我们非常重视模块化建设。Flutter业务采用的模块化方案是从钉钉Native端模块化框架发展而来的。我们一开始就坚持去掉Flutter业务层的直接耦合:模块化不仅提高了我们的研发效率,而且带来了显着的收益。商业价值和技术价值。例如:对钉钉多版本的强大支持,满足“标准钉”、“大客户钉”、“专属钉”等多版本代码共享的需求;提供良好的兼容性,通过基础模块的灵活插件,满足Dutter框架在移动端和桌面端相同架构的要求;它提供了丰富的可扩展性,例如当我们试图让Flutter动态化时,基于模块化,我们可以以较低的成本对现有模块进行动态改造。不影响其他模块的稳定性。3.5容器化容器化是支持Flutter快速落地钉钉的有力保障。通过钉钉在H5和小程序项目中积累的容器基础,在Flutter场景中继续借鉴容器化的理念,快速对接设计和能力。一方面,现有积累的基础设施可以快速复用;另一方面降低业务开发上手的复杂度,保证原有容器的通用能力在Flutter场景中可以继续使用,技术栈可以延续。从发展时间线来看,钉钉端侧容器大致经历了三个版本:v1.0版本主要解决“存在”问题,定义了容器相关的核心概念;v2.0版本在原有基础上抽象出“能力包”的概念,保证基础业务能力可以跨运行环境复用;v3.0版本在v2.0的基础上进一步抽象了“runtime”和“extension”,核心实现的下层是“containerbase”。弱耦合。基于目前的容器架构,我们可以保证对未来新技术的良好兼容性。在后续开发中,如果需要再次接入类似Flutter的新技术栈,可以按照现有标准快速打通,在概念、能力、基础设施等方面保证最大程度的复用。3.6组件库DingTalkFlutter目前使用了两套组件库:dingui_flutter和dingtalk_uikit。其中,dingui_flutter是我们现阶段的重点建设部分,dingui_flutter是根据钉钉视觉团队提出的DingUI视觉规范实现的一套Flutter版本组件。目前核心组件可以兼容四个终端:dingui_flutter的目标是为社区做贡献,但现阶段由于稳定性、完善等问题,暂时仍被钉钉内部使用,并在后续开发成熟后第一时间开源。目前Flutter在钉钉桌面端的使用方式与移动端基本一致:Flutter是钉钉中的一个功能模块,客户端主体还是基于原来的Native实现。对于一些基于Flutter实现的服务,在启动时通过Dutter框架封装的接口进行转场,根据特定的转场方式执行转场动作。为了达到以上效果,我们在桌面应用中主要解决了以下三个问题:桌面集成模式问题;寡妇32位问题;引擎架构兼容性问题。后面我们会分别对上述问题进行说明。4.1桌面端集成方式存在的问题Flutter目前只支持在桌面端使用FlutterApp方式,而在移动端广泛使用的FlutterModule方式暂不支持。然而,期望通过FlutterApp对现有客户端进行大规模改造既不合理也不现实。因此,我们在桌面端落地Flutter遇到的第一个问题就是如何将Flutter作为一个模块集成到钉钉已有的客户端中。我们在分析Flutter构建产品时发现,其实无论是FlutterApp还是FlutterModule,其核心产品并没有太大区别。以iOS上的FlutterModule和macOS上的FlutterApp为例,如下图所示:我们可以看到,对于App.framework、Flutter.framework、Plugins.framework等核心模块,FlutterApp和FlutterModule都包含在其产品中.主要区别在于FlutterModule中多了一个FlutterPluginRegistrant.framework,用于插件的辅助注册。好在这部分实现并不复杂,我们可以通过自定义工具链轻松生成。按照这样的思路,我们可以梳理出Flutter桌面端集成方案:使用FlutterApp来组织桌面端Flutter相关模块,并在官方工具链的基础上做适当的扩展。从原有构建产品中提取模块化使用所需的部分,最终完成插件注册所需的部分模板代码。最终产品集成到钉钉现有客户端后,与其他二方库在使用上没有本质区别,可以参考现有的FlutterModule方法使用。最终流程如下:Mac和Windows产品整合示意图:4.2Widows32位问题Flutter不支持Windows32位系统,这应该是阻碍Flutter桌面生态在中国部署的核心障碍之一这个阶段。钉钉在解决这个问题的时候,基本上把我们能想到的方案都试过了:从最开始的双进程,到中间整体升级到64位,再到后来的FFW,但是上述方案最终还是由于各种无法登陆的问题。虽然最终没能落地,但是在以上尝试的过程中,我们了解到两个非常重要的信息:DartVM可以运行在Windows32位设备上,但只支持以JIT方式加载dart代码;Skia可以编译Windows32位产品。有了以上两点的支持,钉钉周勇终于摸索出了编译Windows32位FlutterEngine的解决方案,并通过JIT方式加载了Flutter编译产物,终于满足了Windows端的使用需求。为了能够在Windows平台上使用Flutter,在剥离细节之后,我们大致做了以下事情(后面会有文章分享细节):修改FlutterEngine的构建脚本,让它构建一个32位的-位flutter_windows.dll;修改flutter_tool中的FlutterPlugin编译gn参数,使其构建32位产检产品;对相关产品进行安全混淆后,集成到钉钉客户端中。通过以上步骤,我们完成了在Windows32位钉钉上集成Flutter的主要工作。之后无论是JIT还是AOT在功能上都没有本质的区别,但是在性能上却有很大的区别。目前我们在灰度过程中发现的主要问题有:启动速度慢:首页加载时间超过2s;临时内存使用率高:打开一个FlutterEngine对象大约需要70MB内存;代码运行效率低:虽然这个问题在大多数场景下并不明显,但是在极端场景下还是有可能出现性能问题。因此,我们现阶段通过的只能算是一个发布计划,未来我们还需要在这部分加大投入,力争尽快将一个完整的Flutter集成到钉钉Windows中。4.3引擎架构兼容性这是我们在登陆桌面的过程中遇到的第三个问题。由于我们在移动端使用的是基于FlutterBoost的单引擎架构,而桌面端由于环境特殊只能使用多引擎架构:所以给商科学生带来了一些问题,其中最严重的就是多引擎-环境导致的引擎通信阻塞。现阶段我们主要通过业务层兼容来绕过它:我们使用钉钉的“消息总线”来支持多引擎环境下的通信问题。但从长远来看,我们还是需要对多引擎有友好的支持。我们需要将目前在移动端可用的LightWeightEngine能力扩展到桌面端,并在其基础上进行扩展,让业务代码通过隔离充分共享内存。目前该方案正在AliFlutter项目组作为技术项目推进,期待早日实现既定目标!综上所述,目前Dutter项目已基本达到第一阶段目标,我们将在以下五个方面继续投入:基础设施升级:移动端FlutterEngine升级、flutter_boost升级、动态方案探索与落地等;性能体验提升:桌面端性能提升,最大限度解决当前官方支持、基础设施完备性、桌面特性等带来的性能问题,力求与移动端水平接轨;完善的研发配套:为钉钉提供一站式的研发环境。目前,我们希望在阿里盒子的基础上,能够面向钉钉的四端研发场景,定向扩展部分能够满足钉钉的应用开发需求;稳定性增强:解决目前桌面端,尤其是Windows端稳定性存在的风险,满足钉钉的需求。端到端稳定性要求;研发效率提升:扩大业务覆盖面,发布跨端宏利,进一步提升钉钉端到端研发人力效率。
