Flutter 是当前跨平台技术中最火的一项,在提供极好的用户体验的同时,还能解决多端一致性问题,并有效降低人力成本。字节跳动希望把 Flutter 打造成下一代研发体系,支撑众多 App 的各种使用场景,为此,团队在 Flutter 上大力投入,覆盖了引擎技术、平台服务、开发框架等多个维度。本次将详细介绍字节跳动在 Flutter 技术上的进展和工程实践,内容整理自字节跳动高级研发工程师林帅斌在 GMTC 深圳 2019 的演讲。
今天主要分享 Flutter 在字节跳动的现状以及工程实践的经验。先介绍一下我所在的移动平台部,移动平台部是字节跳动的基础技术部门之一,服务于公司的各个 App。几乎所有的主流 App 都是我们的客户,所以我们的方案更专注于通用性以及工程实施的难度,主要是为了方便各种 App 的接入。移动平台部所涵盖的方向包括:基础架构、研发体系、APM 、端智能、跨平台等,而 Flutter 就是 APM 端智能跨平台下的研究方向之一。
Flutter 在字节跳动的现状
Flutter 的优势与业务现状
为什么选择 Flutter
选择 Flutter 主要有以下四个原因:
性能体验
开发效率
高度一致性
高可控制性
性能体验与开发效率作为官方一直在宣传的两个重点,这里就不过多赘述。除此之外,我们还看中了 Flutter 的高度一致性,这里的高度一致性不仅仅指各平台 UI 一致,更重要的是各个平台运行的是同一份代码。以前一份需求在 iOS 与 Android 上需要各实现一份,在迭代的时候就会带来额外的协商成本,对于迭代速度很快的字节跳动来说,Flutter 可以很好地抹平这个成本。
什么是高可控制性?Flutter 对宿主的依赖很低,宿主提供一个画布就可以自己运行起来,还有渲染流程和时间派发都是自行运作的。换句话说,无论是修改内部实现还是优化内部逻辑,我们都可以很轻松地做到,这点和过去的 Native 应用开发有很大区别,使用 Native 开发需要各种 Hook,API 还有较高的风险。
由此我们觉得 Flutter 的确是一个值得研究与投入的选择。
业务落地
具体到业务落地,我们早期拿 Flutter 实现了一个比较简单的页面,基本上达到了 Native 的效果,在滚动和手持操作方面的体验也几乎与 Native 一致,也正是因为这一次的实验让我们对 Flutter 更加有信心,也向着这个方向继续深耕,将一些需求直接使用 Flutter 来实现。目前,公司内部大部分 App 都在使用 Flutter,这里简单列举几个:
建设进展与规划简介
作为字节跳动的基础技术部,我们究竟对 Flutter 进行了怎样的规划与建设?
总的来说,我们希望将 Flutter 打造成下一代大移动端开发体系。所谓大移动端,与大前端并不那么相似,大移动端更多主张移动优先,同时再去兼顾其他端。而且最好是可以用一套技术栈统一它们,Flutter 就刚好符合这个定位。我们以这个目标为愿景,围绕着这个愿景我们主要在三个方向进行努力:
提升基础能力
第一个就是基础能力,Flutter 与 Native 毕竟存有一些差距,我们需要研究在基础能力这方面如何抹平差距,甚至让它超越 Native,所以我们在架构层面投入了大量的人力。
完善周边配套
第二点是周边配置,一个语言、一个平台是否好用,能不能有更好的发展,往往不是这个语言或平台自己决定的,更多要看它的周边配套工具,包括 IDE、Debug 工具等等,而 Flutter 作为新生的平台,在这方面存在较大的劣势。我们也将此作为一个主要方向投入研究,提供一个更好的工具。
推广 Flutter 技术
第三点就是针对于 Flutter 的推广,这个问题很容易理解,更多的开发者使用 Flutter,建设 Flutter 的资源就会越多,这样就会形成一个积极的循环。
建设规划
围绕着以上三点,我们做了一个“3 + 1”建设规划,就是应用框架、基础服务和引擎框架三大方面加上周边建设:
引擎框架目标很明确,就是为了提高 Flutter 的基础能力,所以我们对其做了大量的性能优化,包括渲染卡顿、内存回收、启动速度等。在启动速度方面我们提升了将近百分之一百,与此同时体积也压缩了百分之五十,除了这些基础的性能优化外,我们还支持多窗口、端内复用等更多的能力;
基础服务方面主要是针对研发流程,线上高可用和降级的灰度策略等,换句话说就是除了敲代码之外的研发流程;
应用框架层我们对外说的比较少,应用框架层主要是为了打造一个开发者生态,或者称之为应用开发生态,更注重基础组件、工程方案、工程效率、最佳实践等,帮助业务更好地实施他们的工程;
除了左面的三大点外,我们也很注重 Flutter 的周边建设,包括对 Flutter 技术体系的培养,推出培训课程、建立 Flutter 知识库等,同时我们也很注重开发者的交流,建立交流大群、信息同步群等等。
工程实践
接下来分享三个我们觉得通用性比较强的方案,看一下为什么我们会觉得它很强。
FlutterW 研发套件
背景
为什么开发 FlutterW 研发套件呢?主要是源于三个问题:
第一点,作为基础技术部,我们要服务的业务很多,我们希望他们使用 Flutter 的前提就是可以更简单地安装配置好 Flutter。但 Flutter 的安装配置很繁琐,同时不同技术背景的开发者配置起来也会有不同的感受,比如说专攻 H5 的同学配置这个环境就会很繁琐。Flutter 引擎是一个全局变量,它需要安装在每一个同学的系统里面,而整个业务团队内每个人所使用的引擎版本不尽相同,就会导致因环境问题而产生异常现象,花大量时间处理这个问题显然是很不划算的;
第二点,Flutter 作为一个跨平台语言,无论是开发、编译还是调试,它的整个链路都特别长。简单来说就是:一旦应用中出现了一个问题,它可能发生在 Dart 上,可能发生在 Native 上,还有可能发生在这个业务同学的电脑环境配置上,导致问题排查起来十分不方便;
第三点,当我们有方案需要实施的时候,很难把方案立即部署到具体的业务团队的设备上,在实施方案的时候也具备一定的难度。
功能介绍
基于以上三点,我们需要一个能把用户的整个开发流程包裹起来的研发套件。比如说安装配置 Flutter、使用 Flutter 进行开发、发布 Flutter 的产物、调试 Flutter 的问题等一系列的开发流程。目前 FlutterW 主要具备四大方面的能力,如下图:
第一个是环境方面,它需要把开发环境处理好,具体来说就是帮助开发者快速部署好 Flutter,还要保证环境的基本属性一致、引擎版本一致;需要有一个独立的沙盒,某一个项目的配置不应该在一台设备上相互影响;还需要有一个依赖配置表。
同时 FlutterW 还需要具备一些工程方案的能力,可以让我们的业务通过它进行简单的实现,为什么不用文档呢?因为在各式各样的业务方团队面前,文档的效率并不高,所以我们让这个套件作为一个载体让业务团队在实施方案的时候更轻松、更快并且失误率更小。
第三个是左下方的橙色部分,也就是 Support 能力。业务方往往会遇到各种问题,找到我们的时候,我们该如何解决?跨团队、跨部门的合作已属不易,更何况还有很多是跨地区的。由此我们给 FlutterW 加了一个功能:采集用户的错误信息,直接调用我们的解决方案并部署。
最后的绿色部分是 Dryrun 功能,就是用来体验 Flutter 的功能。我们在公司内部做推广的时候发现,很多团队都有体验 Flutter 的意向,但是 Flutter 的安装势必会污染本地的电脑环境,导致他们放弃体验。FlutterW 的能力就是可以直接运行 Flutter,不需要配置。
总结下来 FlutterW 的功能中最核心的只有两点:
便准化工程环境
自动化工程能力
标准化工程环境
以一个标准的 Flutter 工程为例,由于每个用户的电脑内的环境都不一样,那么在你关注这个工程本身的同时还需要关注用户本地的环境配置。我们不可能永远知道业务团队同学在使用什么样的配置,每次处理问题都进行沟通的话,就会在无形之中多出了太多的成本。Flutter 把环境直接配置在了项目内部,通过 FlutterW 安装的项目,它的内部除了代码资源外,还会有一个依赖配置表,里面会将此项目的一些信息,诸如基础的依赖、SDK 版本、打包工具等,都描述清楚,然后根据这个配置表来自动拉取相应的资源从而形成一个沙盒环境,在这个环境内进行独立开发。
自动化工程能力
第二个是自动化工程能力。如上图,当业务团队需要反馈问题的时候,FlutterW 会收集更多信息提交给我们团队,我们基础技术部收到信息后会将问题还原并提出解决方案,这时候我们可以将解决方案部署到 FlutterW 的云端服务器,FlutterW 会自动更新这些方案从而为业务团队解决问题。通过 FlutterW 这个媒介,我们可以尽量地拿到用户信息并将解决方案实时部署。
以上两个是 FlutterW 最核心的两个能力,简单总结来说就是:
标准化工程环境,保证项目成员环境⼀致,问题可回溯。
⾃动化工程方案能力,协助排查问题、解决问题,实施⽅案。
提供低成本体验 Flutter 能力,降低 Flutter 的推广难度。
配合研发流程打包发布 Flutter 产物。
容器化工程方案
背景
这里的容器化工程方案更多的是指 API 的容器化。Flutter 是一个跨平台语言,跨平台框架,身为一名开发者听起来实在是很美好——做一个 App 就可以各个端去跑。但当开发者真正着手开发一个跨平台应用的时候,就会发现需要了解的实在是太多了:Dart 是不可避免的,还有 Java、OC、平台相关语言… 因为我们终究还是要依赖平台的能力,这样要求还是比较高的。正因为一般的开发很难兼顾好各个平台,所以我们很多业务开发会同时配置一个 iOS 和一个 Android,当平台出现问题时分别跟进,但这样很浪费人力。那有没有办法让我们的业务只关注在 Dart 上呢?不用再去管这些所谓的平台能力呢?显然是可以的。
多端、多业务交叉部署
我们引入了一层标准化的 API,用来规范化各个平台提供的所有能力。平台实现这样一套标准能力后,Dart 业务的开发方就不需要再烦心于各个平台的问题,面向这一层 API 进行编程就可以了。也正是引入这一层,使我们可以达到真正的多端、多业务交叉部署。
Flutter 作为一个渲染框架,自身就保证了运行环境的渲染能力的统一,而容器化 API 又保证了各个平台、各个端的基础能力的统一,有了渲染能力和基础能力的统一,业务方就可以真真正正的用一套代码跑在各个端上了。
业务隔离部署
这其实是对于开发者的开发体验的一个优化。既然是面向 API 编程,那么就不需要关注最终运行在哪里了。对于开发来说,在开发时运行一个 H5 版本岂不是更方便、更轻巧?最起码不用再连接一部手机。在实际交付的时候,再打包成产物,交付到实际的设备上。
体系
那么容器化 API 是一个什么样的体系呢?
如上图,整个容器化 API 最重要的部分是上方的红色部分,我们需要固定 API 层并将其标准化,所以它有一个独立的 API 版本管理。但仅仅提供一层 API 也是不够的,以字节跳动为例,公司内部的 App 特别多,每一个应用都要重复实现这些接口的实现并不科学,所以我们为它提供了一套默认实现——而且这个方案可以支持他们随便移出这些实现,修改成自己的实现或者做一些其他的扩展。
经历了这么多的考量,最终得出了这个容器化 API 方案的最终产品,大概总结下来就是:
容器化标准化了各端的基础能⼒;
分离开发关注点,降低开发难度;
业务开发、部署环境可分离,可以⾯向 Web 端提供更快更好的开发体验,最终部署在 Native 获得更好的⽤户使⽤体验。
ByteRedux 状态管理方案
背景
Flutter 是一种典型的响应式 UI 框架,这种框架的特点就是 ui = f(state),重要是状态而不是 UI。只要我们能管理好一个应用的状态,那我们整个应用的页面或者行为就始终受控。但 Flutter 这种框架,它状态分散在各个组件当中,各个页面之间要互动、流转,从而导致这个操作十分烦琐,很难维护。
选择
所以我们要尝试研究之前前端的一些经验,目前业界的主流就是 Redux,那它有什么优势呢?
如上图,左侧的图更像我们平时的一些应用开发,各个元素间相互调用,各个对象间也经过各种循环来调用,一旦项目相对庞大,就会没有人敢随便动这个项目,项目就会逐渐失去可维护性。反观 Redux,它提供了一个角色,把所有的状态收归在其内部,除了参与元素外,主要是对 UI 的监听,监听那些状态有着什么样的改变。同时,我们也更容易知道它内部会发生哪些变化。
简单看一下 Redux 的原理,上文提到的管理角色叫做 Store,Store 内持有了整个应用的所有 State,同时它也持有着所有的 Reducer——用来更新这些状态的。正因为数据和更新方法都在控制范围内,整个应用就拥有较高的可控性。
大致流程就是:我们的页面元素 View 发送一个事件通知 Store,在 Store 内部会根据这个事件进行处理并遍历所有的 Reducer,哪一个 Reducer 对这个事件感兴趣就会更新的状态,同时 View 元素就会自动改变。重点在于这是一个单向的数据流和单一数据源。
缺陷
当我们真正使用 Redux 的时候,就发现 Redux 有一些不足之处:
入门成本偏高,但维护成本显著降低
全局只有⼀个 Store 是个大问题
牵⼀发动全身,全局计算导致性能问题
无法多人协作开发
状态是静态定义的
数据结构复杂化
状态缺乏生命周期
由此,我们也尝试比较了一下目前市面上其他的状态管理方案:
可以看得出来,各个解决方案都有其优缺点,但结合我们自身应用的规模化考虑还是觉得 Redux 这种强管理、数据状态可控的模式更适合我们。
方案介绍
那么之前的问题关键在于哪里呢?
关键问题:Store“超重”,Store 需要负责具体 UI 互动细节,又要负责管理全局状态两个角色;
核心思路:引入更高⼀级的概念取代 Store 成为单⼀数据源,管理全局。
回想我们的状态本身,状态分布于页面上,而且状态之间往往会有一些归属关系,例如有的页面在登陆后才会存在,有的信息在登陆后才会有。由此我们选择了一个树状结构,也就是我们的自研方案——ByteRedux。
高性能、低成本学习
我们引入了一个 StoreTree 来承担这个单一数据源这个角色,引入这个角色以后有什么好处呢?第一个就是高性能、低学习成本。为了避免引入开发时候的误区,我们选择了让 Store 来代理这棵树让 Store 和单一数据源完成整个交互。
举一个例子,我们的 UI 发送一个 Action 到这个最左边的 Store 的时候,这个 Store 不会直接在内部处理而是转发到根节点,从而投派给 StoreTree 这个角色,让 StoreTree 管理这个 Action,帮你去寻址到你想发送的 Store,这时它内部才会进行计算并更新内部的状态。这个过程对于我们普通开发者来说,和原本的 Redux 用法基本上没区别,并不需要额外的学习成本。高性能又是怎么理解呢?如下图,这个 UI 元素能影响到的仅有它的目标 Store,其余四个节点的状态完全不会受干预,也不会导致其他无关的界面的任何的 ReBuilt。
支持模块化开发
要支持多元开发和模块化开发,有一个问题必须要重视。如上图所示,模块 A 在宿主应用内的运行时,根结点会继续寻找合适的挂载节点,然后加入之前的应用的单一数据源——StoreTree 里面,并让自己开始运行。那么在模块开发时该如何运行呢?
我们的 Store 根结点会自己生成一个根节点,从而保证生成单一数据源的这个对象在一个小模块内依然可以正常运行。但仅仅保证可以运行还是不够的,开发者做模块开发时,总会依赖一些外部的属性。例如,很多情况下要依赖登录信息和用户状态等,我们的根结点可以 Match 一些外部的状态,从而加速我们的整个开发流程。
复⽤已有社区资源
ByteRedux 作为我们自研的框架,如何复用社区已有资源也很重要。整个方案的核心就是维护 StoreTree,具体到每一个 StoreTree 是否需要自己实现则不是关键问题。但如果我们可以通过一个转换器用上社区中所有的轮子肯定是最好的,所以最终我们通过一个转换器实现了对社区已有资源的复用。
架构
ByteRedux 的架构需要关注的只有中间的部分,图中的 ByteRunner 就负责刚才的模块化开发,包括判断节点是否需要、如何寻找已有的单一数据源、如何创建单一数据源等,同时它也是支持 Mork 数据的关键。
关于 ByteRedux 的优点总结下来就是:
具备 Redux 的所有优势(强管控、可预测、测试友好);
高性能;
支持模块化开发、运行;
学习成本低;
复用已有社区资源;
支持全局、局部⼴播等新特性。
总结与展望
Flutter@字节跳动 Flutter 已经成为字节跳动新业务的首选技术栈之一;建设方向覆盖了引擎、框架、基础服务等方面。工程实践 Flutterw 标准化 Flutter 的开发环境,提供了方案统⼀实施的能⼒。容器化方案让运行环境提供统⼀基础能力,真正实现“Write Once, Run Everywhere”。ByteRedux 状态管理方案以很低的成本解决了 Redux 的问题,保留了 Redux 的优势,使 其适应大中小型应用。
未来 Flutter 在字节跳动内部会继续作为下一代大移动端开发技术这个角色推进。在工程实践上,我们也会更注重开发者体验,致力于降低使用成本,赋予更多能力。
作者介绍:
林帅斌,就职于字节跳动移动平台部,曾就职百度、阿里巴巴,参与过百度日文输入法、东南亚 Lazada 电商等重量级应用研发。目前负责 Flutter 应用框架层基础建设,主要的研究方向包括工程自动化接入、状态框架、组件容器化方案、研发流程等。字节跳动急招大量技术岗位,联系邮箱:linshuaibin@bytedance.com。
活动推荐:
2023年9月3-5日,「QCon全球软件开发大会·北京站」 将在北京•富力万丽酒店举办。此次大会以「启航·AIGC软件工程变革」为主题,策划了大前端融合提效、大模型应用落地、面向 AI 的存储、AIGC 浪潮下的研发效能提升、LLMOps、异构算力、微服务架构治理、业务安全技术、构建未来软件的编程语言、FinOps 等近30个精彩专题。咨询购票可联系票务经理 18514549229(微信同手机号)。
评论 3 条评论