百度 APP iOS 端包体积 50M 优化实践 (七) 编译器优化
一. 前言
百度 APP iOS 端包体积优化系列文章的前六篇重点介绍了包体积优化整体方案、图片优化、资源优化、代码优化、无用类优化、HEIC 图片优化实践和无用方法清理,图片优化是从无用图片、Asset Catalog 和 HEIC 格式三个角度做深度优化;资源优化包括大资源优化、无用配置文件和重复资源优化,代码优化包括无用类优化、无用模块瘦身、无用方法瘦身、精简重复代码、工具类瘦身和 AB 实验固化。本文重点介绍编译器优化,在百度 APP 实践中,编译器优化包括 GCC 语言编译优化、Swift 编译优化、LTO 优化、剥离调试符号、剥离符号表、剔除未引用的代码、Asset 优化、C++虚函数优化和三方 SDK 编译器方向瘦身。此外,我们重点介绍了指令集架构优化、XCode 升级优化和 Swift 内置动态库优化,这三个模块优化的基础原理都涉及到编译器,所以我们在此篇章一起介绍。
百度 APP iOS 端包体积优化实践系列文章回顾:
2、《百度APP iOS端包体积50M优化实践(二) 图片优化》
3、《百度APP iOS端包体积50M优化实践(三) 资源优化》
4、《百度APP iOS端包体积50M优化实践(四)代码优化》
5、《百度APP iOS端包体积50M优化实践(五)无用类优化和HEIC图片优化实践》
6、《百度APP iOS端包体积50M优化实践(六)无用方法清理》
二. 编译器优化
2.1 方案综述
2.2 GCC 语言编译优化
2.2.1 综述
通过 GCC 编译优化,产生体积更小的二进制产物,对 OC、C、C++都有效果。
2.2.2 Objective C++编译优化
对于 Objective C++采用 XCode 编辑和编译,编译优化配置路径为:Build Settings -> Apple Clang - Code Generation -> Optimize Level,可选参数如下:
Xcode 的默认优化等级是-Os,但我们使用了-Oz 优化方式。在 WWDC 2019 的《What's New in Clang and LLVM》中,链接地址:https://developer.apple.com/videos/play/wwdc2019/409/ ,详细介绍了这种优化的原理。它通过识别编译单元中的跨函数相同代码序列来减少代码大小。重复的连续机器指令被外联成函数,原始代码序列被替换为外联函数实现相同机器代码的瘦身,但会增加函数调用栈的深度,因此对性能有一定影响。随着时间的推移,iPhone 设备的硬件配置越来越高,这种性能损失是可以承受的。下面是一个官方的 demo 示例来说明-Oz 优化的原理。hasse 函数和 kakutani 函数有相同的机器指令,-Oz 优化会生成 OUTLINED_FUNCTION_O 函数,hasse 和 kakutani 指向该函数,从而降低包体积。
官方给出的收益是 25%,从实践效果来看,编译优化参数 -Oz 对 Objective C++编写的代码有 10%的体积收益,对 C&C++有 30%的体积收益。
2.2.3 C&C++编译优化
在 iOS 端,许多底层模块都是使用 C 和 C++实现的,例如网络库、播放内核、视觉处理和端智能等。同时,这些模块也支持 Android 和 iOS 等多个平台。为了实现跨平台,这些模块通常采用 Cmake 和 GN 这两种编译工具。Cmake 是一种常见的跨平台编译工具,其主要工作方式是通过读取 CMakeLists.txt 文件中的指令来生成相应的项目文件。而 GN 编译工具则是 Generate Ninja 的缩写,是一种替代 Cmake 的编译工具。它由 Google 开源,使用 C++编写,主要实现交叉编译,并且可以指定输出平台目标。
无论是使用 CMake 还是 GN,编译器的优化配置都是一样的。对于 C++语言,cppFlags 选项设置为'-Oz',而对于 C 语言,cFlags 选项设置为"-Oz"。
2.3 Swift 编译优化
Swift 编译优化有两个参数 Optimization Level 和 Compliation Mode 配合使用,配置路径为:Build Settings -> Swift Compiler - Code Generation。
Optimization Level 可选参数值如下所示:
Optimize for Size 的核心原理与前面介绍的 GCC 语言编译优化中的-Oz 优化原理相同,都是通过对重复的连续机器指令进行外联并复用,从而降低编译产物的大小。不过,这种优化方式也会对性能产生一定影响,但在当前的硬件设备条件下,这种影响可以忽略不计。
Compliation Mode 可选参数值如下所示:
Optimize for Size[-Osize]和 Whole Module 同时开启会发挥最佳效果,从实践中可以看到它会减少 10%的 swift 包体积大小。
2.4 LTO 优化
LTO,即 Link Time Optimization,是苹果官方提出的一种优化策略。根据官方解释,LTO 是对整个程序代码进行的一种优化方式,是在 LLVM 编译器中在链接阶段进行跨模块间的优化。通过这种优化,编译器可以将部分函数内联化,去除未被调用的冗余代码,并进行整体优化,从而使程序运行得更快,这些优化措施可以有效降低程序的代码大小和提高程序执行效率。
配置路径为:Build Settings -> Apple Clang - Code Generation -> Link-Time Optimization,设置值为 Incremental,需要在主工程以及要优化的 Framework 都开启。
LTO 的优化效果体现在以下三个方面:
1、函数内联化:
LTO 可以将一些函数内联化,即在编译时将函数调用的代码直接嵌入到调用点,以减少函数调用的开销。这可以提高程序的执行效率。
2、去除无用代码:
LTO 可以识别并去除程序中无用的代码,例如未使用的变量、函数和类等。这可以减少生成的二进制文件的大小,从而提高程序的加载速度和运行效率。
3、全局优化作用:
LTO 对程序进行全局优化,可以识别并优化程序中不可能执行的代码分支。例如,如果一个 if 语句的某个分支永远不会被执行,LTO 会将其从生成的二进制文件中移除,这可以提高程序的执行效率和代码质量。
LTO 的负面影响包括:
1、降低 Link Map 的可读性:
Link Map 是链接器生成的一种文件,它描述了目标文件之间的链接关系。在使用 LTO 时,由于进行了全局优化,生成的 Link Map 中的类名可能会以数字开头,如 0.arm64.thinlto.o,这使得 Link Map 的可读性明显降低。如果需要阅读 Link Map,需要先关闭 LTO。
2、增加编译和链接时间:
开启 LTO 会导致编译和链接过程变得更加耗时。这是因为在链接阶段,LTO 会进行大量的全局优化,这需要更多的计算资源和时间。对于线上打包或线下编译,这会导致更长的耗时。
2.5 剥离调试符号
Symbols Hidden by Default 用于设置符号默认可见性,如果设置为 YES,XCode 会把所有符号都定义为”private extern”,包大小会略有减少。动态库设置为 NO,否则会有链接错误。
2.6 剥离符号表
配置路径为:Build Settings -> Strip Linked Product,选择属性值为 YES。
Strip Linked Product 来去除不需要的符号信息 ,去除了符号信息之后我们只能使用 dSYM 文件进行符号化,因此需要将 "Debug Information Format" 修改为 "DWARF with dSYM file"。
Strip Debug Symbols During Copy 与 Strip Linked Product 原理类似,主要是去除拷贝到项目中的第三方库的符号表。只需在 Release 模式下设置为“YES”,而调试模式下仍为“NO”,否则无法对第三方库进行带有符号化的断点调试。
2.7 剔除未引用的代码
配置路径为:Build Settings -> Dead Code Stripping,选择属性值为 YES。
该优化主要是在链接时将 C、C++、Swift 等静态语言无用代码从安装包剔除,但在处理 Objective-C 时无效,因为 Objective-C 是动态语言,基于 Runtime 机制编译,静态编译判定为无用代码可能在运行时使用。
2.8 Asset 优化
Asset 编译优化配置路径为:Build Settings -> Asset Catalog Compiler -> Optimization。
Optimization 可选参数值如下所示:
选择 Space 可以从一定程度优化包大小,收益较小。
2.9 C++减少虚函数的使用
减少虚函数的使用实际上可以减少虚函数表所占用的空间,从而减小程序包的大小。虚函数表是一种用于实现动态绑定的数据结构,其中存储了指向一个类的虚函数的指针。因此,减少虚函数的使用可以减少这些指针的数量,从而减小虚函数表的大小,最终减小程序包的大小。
2.10 三方 SDK 编译器瘦身
上面已经详细介绍了编译器的配置方式及其优化原理,但仅仅修改主工程的优化设置是不足以实现最佳效果的。为了达到最佳优化效果,每个框架(Framework)都必须按照上述配置进行相应的调整。这意味着在每个框架的构建配置中都需要启用优化,并将编译器参数设置为适当的值,以实现所需的具体优化效果。同时,还需要确保每个框架使用的库和依赖项也已正确配置,以确保它们能够与编译器优化一起正常工作。总之,为了使编译器优化真正发挥作用,需要对每个框架进行必要的配置和微调。
百度 APP 作为一款旗舰级应用,内部集成了众多第三方 SDK,例如百度地图、百度网盘、度小满等。因此,需要推动这些第三方 SDK 业务方对其编译器进行优化以实现应用瘦身。这些优化可以包括但不限于图片优化、资源优化、代码优化等。通过这些优化措施,可以有效地减小应用的大小和提升其性能,使用户获得更好的使用体验。
三. 指令集架构优化
3.1 iPhone 常用指令集架构
iPhone 手机采用的都是低功耗的 arm 处理器,arm 指令集架构分为 armv6, armv7, armv7s arm64 四种类型,保持向下兼容,如设备 iphone13 支持 arm64,但是对于 armv7 也是支持的,但是 armv7 无法发挥 iPhone13 设备的更好的硬件属性。模拟器无法运行 arm 的指令集,运行的是 x86 指令集,32 位处理器支持的是 I386 指令集,64 位模拟器支持的是 x86_64 架构,不同设备支持的指令集架构如下所示。
随着硬件设备的不断更新,早期设备(如 iPhone4、iPhone5 和 iPad)的市场占有率已经变得微不足道。因此,对于移动设备,我们只需要支持 arm64 架构即可。同理,对于模拟器,我们只需要支持 x86_64 架构,从包体积的优化的角度来看,目前我们的每个库只需要支持 arm64 和 x86_64 架构,其他架构没必要支持。
优化指令集架构可以减小上传到 AppStore 的包体积,但对用户下载的包大小没有优化效果。这是因为苹果的 App Thinning 机制根据不同设备型号的硬件架构生成不同的编译产物,因此不同设备的用户从 AppStore 下载的包也会有所不同。
3.2 指令集架构设置
Architectures 选项,Build Settings -> Architectures,值为 Standard architectures - $(ARCHS_STANDARD),在真机的编译下实质是(armv7 和 arm64)在模拟器的时候是(x86_64,i386,arm64)
Build Active Architectures Only 选项,Build Settings -> Build Active Architectures Only,当其值为 Yes 时,表示只编译当前一个架构,真机的话一般是 arm64, 模拟器是 x86_64, 如果为 No 的时,那就是同时编译第一支持的架构;
Excluded Architectures 选项,Build Settings -> Excluded Architectures,其值是要排除的架构,例如,如果将其设为 arm64,表示产物里面没有 arm64 架构;
3.3 去除无用架构
通过 lipo 命令从老的 framework 中的 mach-o 文件拆分出指定架构二进制文件,然后合并,最后用合并后的二进制文件替换老的 framework 的 mach-o 文件。
用 lipo -info 命令查看 framework 包含的指令集架构信息,如下所示,AbcSDK.framework 支持的指令集是 x86_64、i386、arm64 和 armv7;
lipo 命令抽取指定架构,如下所示,从 AbcSDK.framework 抽取出 arm64 架构,放在 AbcArm64,抽取出 x86_64 架构,放在 AbcArmX86_64。
验证 AbcArm64 和 AbcArmX86_64 架构信息
lipo 命令合并架构,合并 AbcArm64 和 AbcArmX86_64,生成新的 newAbc,按预期 newAbc 有两个架构 x86_64 和 arm64,用 lipo -info 命令验证。
替换原先的二进制文件,经过上面这些操作将有 x86_64、i386、arm64 和 armv7 四种指令集架构的 AbcSDK.framework,瘦身变成了只支持 x86_64 和 arm64 两种指令集的组件。
四. XCode 升级优化
苹果公司一直致力于提高开发者的生产力,每年都会推出新版本的 XCode,并对其进行大量的优化。在包体积方面,他们也采取了积极的措施。例如,在 22 年 10 月发布的 Xcode 14,不仅具备全新的增强功能,更拥有更强大的并行编译能力,能够显著提高项目构建速度。同时,对包体积的优化也相当明显。
为了寻找 Xcode 14 优化包体积的具体技术点,看了很多 WWCD 资料,终于在官方文档《improve app size and runtime performance》找到答案,链接地址为https://developer.apple.com/videos/play/wwdc2022/110363/#,Xcode 14 从以下三个方面进行了优化包体积:
Meesage send 函数调用占用从 12 bytes 降低到 8 bytes;
Retain and release 函数调用占用从 8 字节降低到 4 字节;
autorelease 优化,移除自动释放省略中的 mov 指令,体积降低 4 bytes;
五. Swift 内置动态库优化
自 2014 年 WWDC 发布以来,在苹果公司的强力推动下,Swift 语言取得了显著的发展。其优点代表着 iOS 开发的发展趋势,随着使用率的不断提升,Swift 有望最终取代 Objective-C 成为 iOS 开发的首选语言。目前,Swift 已经成为各大公司和应用程序的必备开发语言,国内日活跃用户排名前二十的 APP 中,除了拼多多以外,其他公司都已经采用了 Swift 进行开发。
然而,在只要采用 Swift 语言开始开发,就会发现 iPA 包中新增了 Swift 系统库。这是因为对于低于 iOS12.2 的系统,没有内置的 Swift 系统库,因此 XCode 在打包生成 iPA 包时会一并包含 Swift 库,在 iPA 包的 Frameworks 动态库目录发现如下 Swift 系统库。
更进一步来说,如果 APP 自带的 WatchApp 也使用了 Swift 语言,那么在 Watch 的动态库中还会有一份 Swift 系统库,这样 iPA 包中就会包含两份内置库。
优化方法非常简单,只需将 APP 支持的最低版本修改为 12.2 即可。因为 12.2 及以上的系统自带 Swift 系统库,不需要在 APP 内置。在百度 APP 包体积优化实践中发现,优化后 iPA 包体积减少 30M+,30M 动态库已经在 ipa 包不存在,提交 AppStore 后,从 connect 后台看数据,有如下收益:
iPhoneX 及以下的机型,如 iPhoneX、iPhone8、iPhone7,安装包体积减少 20M,下载包大小减少 10M;
iPhoneX 以上的机型,如 iPhone11、iPhone12,iPhone13 没收益,苹果自身做了优化,我们再做这个优化,价值不会体现;
对于百度 APP 来说,iPhoneX 及以下机型的占比不到 5%,但是提高 APP 支持的最低版本号会导致部分用户流失。综合考虑这两个因素,决定不采用 Swift 内置动态库的优化方案。
六. 总结
相比于代码优化、资源优化和图片优化,编译器优化在包体积优化中的投资回报率(ROI)是最高的。然而,编译器优化的影响范围也是最大的,因为每个库的编译器配置修改会影响该库的所有代码。因此,必须对优化质量进行严格控制。在百度 APP 的优化实践过程中,编译器方向的优化成功减少了 30M 的包体积,实现了自身库的全部收益,此外,按照体积排名的前 15 个三方 SDK 也全部实现了此收益。
本文系统介绍了百度 APP 的编译器优化方案,包括 GCC 语言编译优化、Swift 编译优化、LTO 优化、剥离调试符号、剥离符号表、剔除未引用的代码、Asset 优化、C++虚函数优化和三方 SDK 编译器方向瘦身等多种手段。此外,还介绍了指令集架构优化、XCode 升级优化和 Swift 内置动态库优化等其他优化方案。后续我们会针对其他优化详细介绍其原理与实现,敬请期待。
——END——
参考资料:
[1]gcc 编译器配置:https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html
[2]LTO 使用方法:https://llvm.org/docs/LinkTimeOptimization.html
[4]What's New in Clang and LLVM:https://developer.apple.com/videos/play/wwdc2019/409/
[5]XCode14 介绍:https://developer.apple.com/documentation/xcode-release-notes/xcode-14-release-notes
[6]improve app size and runtime performance:https://developer.apple.com/videos/play/wwdc2022/110363/#
推荐阅读:
版权声明: 本文为 InfoQ 作者【百度Geek说】的原创文章。
原文链接:【http://xie.infoq.cn/article/56d5f42321c1007f3539f828a】。文章转载请联系作者。
评论