# 跨平台的边界：共享那些不允许分叉的部分

> “选哪个跨平台框架”不是第一个该问的问题。真正该问的是：哪些层在两个平台上必须完全一致（因为在那里分叉就是缺陷），哪些层本就应该不同（因为在那里分叉才是正确的）。

- Canonical (HTML): https://kaguc.com/blog/cross-platform-boundary-zh/
- Date: 2026-08-28


*English version: [The cross-platform boundary: share what must not diverge](/blog/cross-platform-boundary/)。本文是[交易类 App 架构](/blog/trading-app-architecture-zh/)系列第四篇。*

跨平台的讨论通常被当成框架对比来进行，这也是它从来吵不出结果的原因。真正决定大局的是前一个问题：**你的应用里，哪些部分在 iOS 和 Android 上必须一模一样，哪些部分不应该一样？**

回答了它，框架选择就收敛到两三个合理选项。跳过它，再多的基准测试也没用，因为你是在需求还没定义的时候争论工具。

## 判据：分叉是缺陷，还是特性

拿你应用里的任意一层，问一句：如果两个平台的行为不同，会发生什么？

对于订单校验、费用计算、期权定价规则、状态机、协议编码：分叉是**缺陷**。同一条金融规则的两份独立实现**一定**会漂移，不是“可能”。它们先在边界情况上漂移，而在这个领域里，边界情况同时也是昂贵的情况。几个月内没人发现，然后缺陷以“某位用户的数字对不上”的形式送到你面前。

对于导航转场、手势手感、日期选择器、权限流程、无障碍、分享面板：分叉是**正确的**。用户有平台预期，抹平它们的代码会做出一个在两端都隐隐别扭的应用。花在统一这一层上的力气，买到的是负价值。

规则就这一条：**共享那些“不同即缺陷”的层，把那些“不同才是重点”的层留给原生。**

请注意这是一个正确性论证，不是效率论证。DRY 通常被解释成“只写一遍”，于是很容易被“有时候复制更便宜”反驳回去。这个反驳对 UI 成立，对领域规则则完全不相干，因为在那里，复制的代价不是打字，而是关于钱的两份互相分叉的真相。

## 共享真正的代价

共享不是免费的，而且代价是结构性的，不那么显眼：

**多出一条边界。** 共享代码和平台代码必须对话。这个接口需要双方都理解的类型、序列化、以及错误映射。它从此成为一个需要设计和版本管理的东西。

**多出一个运行时。** 第二个运行时意味着内存、启动开销，以及一个再也看不到全貌的调试器。当问题恰好发生在边界**上**时，你调试的是两个系统外加它们之间的接缝。

**团队被切开。** 必须有人精通共享层和它的工具链。在小规模下这个人只有一个，那不是方案，那是巴士系数。

所以问题不是“共享有没有好处”，而是“这一层的分叉风险，是否超过边界成本”。对领域逻辑，通常是；对一个设置页面，通常否。

## 三种站位

**完全原生，写两遍。** 平台质感最高，边界成本为零，每条规则写两次。当共享逻辑很薄、或者平台特性本身就是产品主体时，这是合理的。

**共享逻辑，保留原生 UI。** 领域、模型、校验放在一份代码里，各平台各自渲染。它精确地命中了“分叉即缺陷”的那一层，而放过了不是的那一层。代价就是那条边界和它的工具链。

**连 UI 一起共享。** 一份代码画两个平台。这是单个团队做出功能完整应用最快的路径，也是大多数小团队应该从这里起步的原因。代价会晚一点到来，而且来的方向可以预测：平台能力。每一个相机、生物识别、后台、通知特性，最终都需要一个插件，而插件就是原生代码加上额外的步骤。从一开始就为它留预算，而不是等它来吓你一跳。

## 混合模式，以及它真实的成本

大型应用常常最终是混合的：一个原生外壳，其中若干页面嵌入跨平台 UI 框架。这通常不是犹豫不决，而是一次迁移的诚实形态；当你无法为了重写而停止交付时，它就是正确的模式（[下一篇会展开](/blog/modular-evolution-zh/)）。

在选择它之前，有几项真实成本值得点名：

- **运行时内存。** 每个嵌入的引擎都有内存占用，多个同时存活会成倍放大；而缓解办法（缓存并复用引擎）本身又是一个你从此要负责的生命周期问题。
- **两个导航栈。** 原生栈和框架的路由必须被统一，否则返回键行为会成为一个长期的缺陷来源。
- **两侧都有状态。** 会话、主题、语言从此存在两份，而且必须保持一致。每一份被复制的状态，都是一次未来的失步。
- **跨运行时调试。** 调用栈会在边界处中断。崩溃和性能工具必须被配置成能看见两侧，否则你有一半的线上问题只能靠猜。

## 把边界做粗

无论你共享什么，共享核心与平台之间的接口都应该是**粗粒度**的。

跨越边界的成本（序列化、封送、运行时切换）是按**次调用**付费的，不是按字节。一个碎嘴的接口，几十次小的属性读取，可能比一次返回完整对象的调用慢得多，尽管它搬运的数据更少。这和[第一篇](/blog/realtime-data-pipeline-zh/)里“在原生边界上批量提交更新”是同一条教训，而且可以推广：**把边界设计成少数几个有意义的操作，而不是远程字段访问。**

这样做还有设计上的好处。粗接口表达的是意图（“下这一单”“给我这个页面的数据”），它在底层实现变化时保持稳定；细接口暴露的是结构，而结构一直在变。

## 诚实地做选择

两个变量决定结论：你的产品里有多少是**不允许分叉的领域逻辑**，以及你有多少**人**。

- 一个小团队，产品主要是接口之上的界面：全部共享。一份代码，平台要求时再写原生模块。
- 小团队，但有实质领域逻辑：仍然全部共享，但把那部分逻辑放进独立的一层，不依赖任何 UI，这样以后可以抽出来而不必重写。
- 多个平台团队，且领域逻辑可观：共享逻辑，UI 保持原生。在这里，边界成本被明确地赚了回来。
- 只有一个平台：这个问题不存在。不要为一种你没有证据会用到的可选性付费。

代价最大的错误，是按一个你还没有的团队规模去选共享策略。为一个你也许会建立的组织提前设计架构，正是小团队最终以小团队的产能，去维护一个大团队的开销的原因。


## 参考

- J. Waldo, G. Wyant, A. Wollrath, S. Kendall，["A Note on Distributed Computing"](https://scholar.harvard.edu/files/waldo/files/waldo-94.pdf)，Sun Microsystems Laboratories TR-94-29，1994——地址空间之间的边界无法被抹平；延迟与部分失败按每次调用计价。
- A. Hunt, D. Thomas，[*The Pragmatic Programmer*](https://pragprog.com/titles/tpp20/)，1999——DRY 被表述为关于知识的规则，而不是关于打字。
- M. E. Conway，["How Do Committees Invent?"](https://www.melconway.com/Home/Committees_Paper.html)，*Datamation*，1968——系统结构会复刻构建它的组织的沟通结构。
- Flutter 文档，[Add Flutter to an existing app](https://docs.flutter.dev/add-to-app) 与 [Multiple Flutter instances](https://docs.flutter.dev/add-to-app/multiple-flutters)——引擎预热、`FlutterEngineGroup`，以及多个存活引擎的代价。

---

*上一篇：[本地优先](/blog/local-first-sync-zh/) · 下一篇：[模块化与迁移](/blog/modular-evolution-zh/)*

