模块化之后,App 变大了,启动还变慢了。最后把它救回来的,是一个构建设置,代码一行没改。
![]()
事情发生在一个团队里,花了整整一周才找到原因。为了把每个数字都做成可复现的,作者重建了一个演示 App。所有数字都是真实的。
第一幕:3 KB 的代码,装进 73 KB 的盒子
三个模块都是普通的动态框架,也就是 Xcode 默认给你的那种。其中 SupportFeature 大约只有 3 KB 代码,却被打包成了 73 KB 的二进制文件。
相当于用微波炉的箱子寄一根 USB 线。每个动态框架会被切成四段,在 arm64 上每段都按 16 KB 页对齐,哪怕几乎是空的。所以每个框架在装进任何代码之前,先付大约 64 KB 的包装费。除此之外,dyld 每次启动都要单独查找、映射、绑定每一个框架。
用 CocoaPods 的 use_frameworks!,或者动态 SPM 产品?那每一个依赖都相当于每次冷启动多过一道机场安检。这也是“我加了几个 pod,启动就变慢了”在 Stack Overflow 上如此常见的原因。
填充能解释那些小框架,但解释不了 CoreKit:它只装了 88 KB 代码,体积却有 316 KB。
第二幕:指纹
把代码移出 App 目标时,编译器会甩给你大约四百次这个错误:'EventName' is inaccessible due to 'internal' protection level。
于是大家都会这么干:public struct EventName { public static let appOpen = "App Open" }。
作者和链接器有一段“审讯记录”。作者问:根本没人调用 appOpen,为什么不把它剥掉?链接器答:它是 public,框架外面可能有人调用。作者问:谁?链接器答:……某个人。我还保留了它的类型元数据,把它加进了导出表。不客气。
每一个 public 符号都要在三处付出代价:
- 链接器无法死剥的代码(__TEXT)
- 类型元数据(__const)
- 导出表里的一条记录(__LINKEDIT),也就是框架的电话簿
为了证明这一点,作者把同一个 CoreKit(360 个常量、40 个模型)构建了两次,唯一的变化是访问级别。同样的代码,体积小了 30%。电话簿比小镇还大。
第三幕:假线索
团队花了一周时间靠重构往外爬:把 struct 换成 enum,换成带 String 的 enum,把常量嵌套起来,再拍平。每一次之后都看体积报告。
结论是:类型的形状不重要。代码仍然是 public,框架仍然在交包装税。真正的解法是减少 public 声明,但那是四分之一的工作量。团队想要更快的收益,于是换了个问题:如果在我们发布的构建里,框架不必保持独立呢?
突破口:为什么不能两者都要
动态框架构建快、启动慢;静态库启动快、构建慢。Xcode 15 的可合并库(mergeable libraries)看着这个取舍说:都要。
Debug 下什么都不变,App 重新导出每个框架,增量构建保持快速。Release 下链接器把每个框架合并进 App 二进制,顺带去重字符串、选择器和桩。框架文件夹还留在包里放资源,但它们的二进制变成了空壳。
修复:一个设置
选中 App 目标,进入 Build Settings,搜索 Create Merged Binary,设为 Automatic。也就是 MERGED_BINARY_TYPE = automatic。就这样,不改代码,不添新工具。automatic 会合并项目里构建的每一个直接框架依赖。
有一个坑:只有直接依赖会被合并。如果 CoreKit 只是通过 SupportFeature 才到达 App,需要把它显式链接到 App 目标。想要精确控制,就在 App 上设 MERGED_BINARY_TYPE = manual,在每一个想合并的框架上设 MERGEABLE_LIBRARY = YES。
判决
App 可执行文件变大了,因为所有代码都搬了进去。留下的框架二进制是空壳。这只需要一个构建设置、零代码改动。演示里只有三个小框架,真实 App 有几十个。
细则
- Debug 不等于 Release。合并只发生在 Release 构建里,所以要测你真正发布的那个构建。
- 归档会变慢。合并是额外的链接器工作,在大 App 上要测量影响。
- 手动模式容易被遗忘。把 MERGEABLE_LIBRARY = YES 放进共享的 .xcconfig。
- 边界情况:合并框架上的 Bundle(for:) 需要 iOS 12 或更高;dlopen 路径要指向合并后的二进制;崩溃报告会把 App 列为镜像。
- 如果你有不止几个自家框架,并且有人在盯 App 体积或启动时间,值得做。如果只有一两个框架,或者要发布二进制 SDK,跳过。
结案
留三道作业:跑一下 nm -gU YourFramework | wc -l,做好心理准备;把一个不需要 public 的东西改回 internal;在一个分支上设 MERGED_BINARY_TYPE = automatic,对比归档结果。
作者说,在找到那个多年来一直自动敲出来的六字母关键字之前,他怪过 Xcode、怪过 CI 缓存、怪过水星逆行。现在他在怪任何东西之前,先跑 nm。
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.