...
Back

为一台你没有的硬件做移植

只有模拟器、没有真机时的平台适配。它到底验证了什么、验证不了什么,以及怎样在发布说明里诚实地说清这个差别。

为一台你没有的硬件做移植

为一台你没有的硬件做移植 📺

在一个成熟生态里新增一个平台,是个奇怪的项目。API 是熟悉的。语言是同一种。构建系统名义上已经知道该怎么做。然而这活儿既不平凡,而且在你做完之后也并没有真的做完 —— 因为模拟器不是真机,而这一点无法靠多花力气买过去。

以下是我们把一个网络客户端移植到"能构建但无法完整测试"的平台时学到的东西。


原生核心才是全部问题

如果你的应用是纯应用层代码,新增一个平台切片基本上是构建设置的练习。如果它包含一个原生核心 —— 一个由另一种语言编译、以二进制框架形式引入的库 —— 那么整个移植完全取决于这个核心能否为新平台的架构编译出来。

也就是说,真正的问题不是"我的应用支持这个平台吗",而是"我的工具链能为它产出一个切片吗"。我们按受阻顺序踩到的三件事:

交叉编译器可能根本不知道这个平台存在。 我们用的绑定生成器有主流平台的目标,对更新的那个则一无所有。修法是教会它 —— 一个补丁,加上目标三元组、SDK 名称和平台最低版本。不体面、纯机械,而且在做完之前完全卡死。

模拟器 SDK 有自己的名字。 每个平台都有真机 SDK 和模拟器 SDK,而模拟器那个名字很少是显而易见的那个。写错了会得到一条"未知 SDK"的报错,而不是"不支持的平台" —— 于是把你送去错误的方向找原因。

二进制框架打包的元数据会静默覆盖。 当你组装一个多平台框架时,各切片的元数据文件可能撞名,结果是一个能构建、能链接、但把自己描述错了的包。什么都不报错。你会在之后、在一个和原因无关的地方发现它。

我们会带走的教训是:去审计产物里实际存在哪些切片,而不是相信"构建成功了就等于产出了它们"。列出二进制里的架构只要一条命令,而它不止一次帮我们抓到过缺失的切片。


模拟器诚实地验证了什么

值得说精确,因为人总是忍不住把"模拟器里能跑"当成"能用"。

模拟器可靠地验证:你的代码能为这个架构编译、应用能启动、视图层级能布局、导航能用、状态管理行为正常、业务逻辑能跑。对相当大比例的应用来说,这确实覆盖了大部分风险。

模拟器验证不了任何触及硬件或平台真实信任边界的东西:真实射频下的网络栈、权限(entitlement)的强制执行、后台执行与挂起、温度与内存压力、真实输入设备,以及 —— 对我们至关重要的 —— 任何依赖某项"模拟器用桩替代而不是实现"的能力的东西。

最后这一类藏着最难缠的意外,因为桩是成功返回的。在真机上会被拒绝的代码,在模拟器里返回成功,于是这个失败不只是"未被验证",而是被主动掩盖了。


诚实地发布

你完全可以构建、甚至发布一个没在真机上测过的平台移植。你不能做的,是把它和一个你测过的平台用同样的方式描述。

三条让这件事可存活的实践:

  1. 写明哪些平台是真机验证过的。 在内部、在发布检查清单里、在与用户相关的地方。"能为 X 构建"和"在 X 上测试过"是两个不同的断言,而把它们混为一谈,正是一个平台带着故障发布整整一年的原因 —— 每个人都以为别人手里有台设备。
  2. 维护一份"只有真机能回答"的清单。 在移植的时候写下来,那时你确切知道模拟器把哪些代码路径打了桩。半年后没人记得,而这份清单会在硬件到手的那一刻直接变成测试计划。
  3. 让这个平台切片的维持成本足够低。 如果它只在有人想起来的时候才构建,移植就会腐烂。把每个切片都放进 CI,这样一个破坏未测试平台的改动会立刻失败而不是静默通过 —— 构建覆盖不等于测试覆盖,但它是让移植不至于退化成虚构的那件事。

到底该不该做

诚实的算法:一个你无法测试的平台移植,在增量成本低、且切片能在 CI 里持续构建时,是值得做的 —— 因为另一个选项(等到手里有硬件)通常意味着永远不开始,而编译器和工具链问题本来就是最长的那根杆子。早点解决它们意味着设备真到手时,你调的是行为,不是构建系统。

如果你打算把它当作"已支持"来宣传,那就不值得做。这不是技术判断,这只是"一次移植"和"一个承诺"之间的区别。