English version: The cross-platform boundary: share what must not diverge。本文是交易类 App 架构系列第四篇。
跨平台的讨论通常被当成框架对比来进行,这也是它从来吵不出结果的原因。真正决定大局的是前一个问题:你的应用里,哪些部分在 iOS 和 Android 上必须一模一样,哪些部分不应该一样?
回答了它,框架选择就收敛到两三个合理选项。跳过它,再多的基准测试也没用,因为你是在需求还没定义的时候争论工具。
判据:分叉是缺陷,还是特性
拿你应用里的任意一层,问一句:如果两个平台的行为不同,会发生什么?
对于订单校验、费用计算、期权定价规则、状态机、协议编码:分叉是缺陷。同一条金融规则的两份独立实现一定会漂移,不是“可能”。它们先在边界情况上漂移,而在这个领域里,边界情况同时也是昂贵的情况。几个月内没人发现,然后缺陷以“某位用户的数字对不上”的形式送到你面前。
对于导航转场、手势手感、日期选择器、权限流程、无障碍、分享面板:分叉是正确的。用户有平台预期,抹平它们的代码会做出一个在两端都隐隐别扭的应用。花在统一这一层上的力气,买到的是负价值。
规则就这一条:共享那些“不同即缺陷”的层,把那些“不同才是重点”的层留给原生。
请注意这是一个正确性论证,不是效率论证。DRY 通常被解释成“只写一遍”,于是很容易被“有时候复制更便宜”反驳回去。这个反驳对 UI 成立,对领域规则则完全不相干,因为在那里,复制的代价不是打字,而是关于钱的两份互相分叉的真相。
共享真正的代价
共享不是免费的,而且代价是结构性的,不那么显眼:
多出一条边界。 共享代码和平台代码必须对话。这个接口需要双方都理解的类型、序列化、以及错误映射。它从此成为一个需要设计和版本管理的东西。
多出一个运行时。 第二个运行时意味着内存、启动开销,以及一个再也看不到全貌的调试器。当问题恰好发生在边界上时,你调试的是两个系统外加它们之间的接缝。
团队被切开。 必须有人精通共享层和它的工具链。在小规模下这个人只有一个,那不是方案,那是巴士系数。
所以问题不是“共享有没有好处”,而是“这一层的分叉风险,是否超过边界成本”。对领域逻辑,通常是;对一个设置页面,通常否。
三种站位
完全原生,写两遍。 平台质感最高,边界成本为零,每条规则写两次。当共享逻辑很薄、或者平台特性本身就是产品主体时,这是合理的。
共享逻辑,保留原生 UI。 领域、模型、校验放在一份代码里,各平台各自渲染。它精确地命中了“分叉即缺陷”的那一层,而放过了不是的那一层。代价就是那条边界和它的工具链。
连 UI 一起共享。 一份代码画两个平台。这是单个团队做出功能完整应用最快的路径,也是大多数小团队应该从这里起步的原因。代价会晚一点到来,而且来的方向可以预测:平台能力。每一个相机、生物识别、后台、通知特性,最终都需要一个插件,而插件就是原生代码加上额外的步骤。从一开始就为它留预算,而不是等它来吓你一跳。
混合模式,以及它真实的成本
大型应用常常最终是混合的:一个原生外壳,其中若干页面嵌入跨平台 UI 框架。这通常不是犹豫不决,而是一次迁移的诚实形态;当你无法为了重写而停止交付时,它就是正确的模式(下一篇会展开)。
在选择它之前,有几项真实成本值得点名:
- 运行时内存。 每个嵌入的引擎都有内存占用,多个同时存活会成倍放大;而缓解办法(缓存并复用引擎)本身又是一个你从此要负责的生命周期问题。
- 两个导航栈。 原生栈和框架的路由必须被统一,否则返回键行为会成为一个长期的缺陷来源。
- 两侧都有状态。 会话、主题、语言从此存在两份,而且必须保持一致。每一份被复制的状态,都是一次未来的失步。
- 跨运行时调试。 调用栈会在边界处中断。崩溃和性能工具必须被配置成能看见两侧,否则你有一半的线上问题只能靠猜。
把边界做粗
无论你共享什么,共享核心与平台之间的接口都应该是粗粒度的。
跨越边界的成本(序列化、封送、运行时切换)是按次调用付费的,不是按字节。一个碎嘴的接口,几十次小的属性读取,可能比一次返回完整对象的调用慢得多,尽管它搬运的数据更少。这和第一篇里“在原生边界上批量提交更新”是同一条教训,而且可以推广:把边界设计成少数几个有意义的操作,而不是远程字段访问。
这样做还有设计上的好处。粗接口表达的是意图(“下这一单”“给我这个页面的数据”),它在底层实现变化时保持稳定;细接口暴露的是结构,而结构一直在变。
诚实地做选择
两个变量决定结论:你的产品里有多少是不允许分叉的领域逻辑,以及你有多少人。
- 一个小团队,产品主要是接口之上的界面:全部共享。一份代码,平台要求时再写原生模块。
- 小团队,但有实质领域逻辑:仍然全部共享,但把那部分逻辑放进独立的一层,不依赖任何 UI,这样以后可以抽出来而不必重写。
- 多个平台团队,且领域逻辑可观:共享逻辑,UI 保持原生。在这里,边界成本被明确地赚了回来。
- 只有一个平台:这个问题不存在。不要为一种你没有证据会用到的可选性付费。
代价最大的错误,是按一个你还没有的团队规模去选共享策略。为一个你也许会建立的组织提前设计架构,正是小团队最终以小团队的产能,去维护一个大团队的开销的原因。
参考
- J. Waldo, G. Wyant, A. Wollrath, S. Kendall,“A Note on Distributed Computing”,Sun Microsystems Laboratories TR-94-29,1994——地址空间之间的边界无法被抹平;延迟与部分失败按每次调用计价。
- A. Hunt, D. Thomas,The Pragmatic Programmer,1999——DRY 被表述为关于知识的规则,而不是关于打字。
- M. E. Conway,“How Do Committees Invent?”,Datamation,1968——系统结构会复刻构建它的组织的沟通结构。
- Flutter 文档,Add Flutter to an existing app 与 Multiple Flutter instances——引擎预热、
FlutterEngineGroup,以及多个存活引擎的代价。