iOS 开发进阶 文章汇总
目录
- Mach-O体积优化
- 链接时间优化(LTO)
- dead strip
- Code Generation Options
- strip
- 查看App Size报告
- 参考链接
Mach-O体积优化
什么是Bitcode
?英文原意是:
Bitcode is an intermediate representation of a compiled program. apps you upload to App Store Connect that contain bitcode will be compiled and linked on the App Store. Including bitcode will allow Apple to re-optimize your app binary in the future without the need to submit a new version of your app to the App Store.
For iOS apps, bitcode is the default, but optional. For watchOS and tvOS apps, bitcode is required. If you provide bitcode, all apps and frameworks in the app bundle (all targets in the project) need to include bitcode.
Bitcode
是编译后生成汇编之前的中间表现:
底层编译流程:
包含Bitcode
并上传到App Store Connect
的Apps
会在App Store
上编译和链接。包含Bitcode
可以在不提交新版本App
的情况下,允许Apple
在将来的时候再次优化你的App
二进制文件。
在Xcode
中,默认开启Bitcode
。如果你的App
要支持Bitcode
,App
使用到的其他二进制形式也要支持Bitcode
:
链接时间优化(LTO)
Link Time Optimization (LTO)
链接时间优化是指:
- 链接阶段执行模块间优化。
通过整个程序分析和跨模块优化来获得更好的运行时性能的方法。
在编译阶段,clang
将发出LLVM bitcode
而不是目标文件。
链接器识别这些Bitcode
文件,并在链接期间调用LLVM
以生成将构成可执行文件的最终对象。
接下来会加载所有输入的Bitcode
文件,并将它们合并在一起以生成一个模块。
通俗来讲,链接器将所有目标文件拉到一起,并将它们组合到一个程序中。链接器可以查看整个程序,因此可以进行整个程序的分析和优化。通常,链接器只有在将程序翻译成机器代码后才能看到该程序。
LLVM
的LTO
机制是通过把LLVM IR
传递给链接器,从而可以在链接期间执行整个程序分析和优化。所以,LTO
的工作方式是编译器输出的目标文件不是常规目标文件:它们是LLVM IR
文件,仅通过目标文件的文件扩展名伪装为目标文件。
LTO
有两种模式:
-
Full LTO
是将每个单独的目标文件中的所有LLVM IR
代码组合到一个大的module
中,然后对其进行优化并像往常一样生成机器代码。 -
Thin LTO
是将模块分开,但是根据需要可以从其他模块导入相关功能,并行进行优化和机器代码生成。
进行LTO
而不是一次全部编译的优点是(部分)编译与LTO
并行进行。对于完整的LTO(-flto=full)
,仅并行执行语义分析,而优化和机器代码生成则在单个线程中完成。对于ThinLTO(-flto=thin)
,除全局分析步骤外,所有步骤均并行执行。因此,ThinLTO
比FullLTO
或一次编译快得多。
使用的编译链接参数有:
clang:
-flto= 设置LTO的模式:full或者thin,默认full。
-lto_library 指定执行LTO方式的库所在位置。当执行链接时间优化(LTO)时,链接器将自动去链接libLTO.dylib,或者从指定路径链接。
在Xcode Build Setting
中的设置为:
通过实例来分析一下:
--- a.h ---
extern int foo1(void);
extern void foo2(void);
extern void foo4(void);
--- a.c ---
#include "a.h"
static signed int i = 0;
void foo2(void) {
i = -1;
}
static int foo3() {
foo4();
return 10;
}
int foo1(void) {
int data = 0;
if (i < 0)
data = foo3();
data = data + 42;
return data;
}
--- main.c ---
#include
#include "a.h"
void foo4(void) {
printf("Hi\n");
}
int main() {
return foo1();
}
进入终端运行:
- 将
a.c
编译生成bitcode
格式文件
clang -flto -c a.c -o a.o
- 将
main.c
正常编译成目标文件
clang -c main.c -o main.o
- 通过
LTO
将a.c
和main.c
通过LTO
方式链接到一起
clang -flto a.o main.o -o main
按照LTO
优化方式:
- 链接器首先按照顺序读取所有目标文件(此时,是
bitcode
文件,仅伪装成目标文件)并收集符号信息。 - 接下来,链接器使用全局符号表解析符号。找到未定义的符号,替换
weak
符号等等。 - 按照解析的结果,告诉执行
LTO
的库文件(默认是libLTO.dylib
)那些符号是需要的。紧接着,链接器调用优化器和代码生成器,返回通过合并bitcode
文件并应用各种优化过程而创建的目标文件。然后,更新内部全局符号表。 - 链接器继续运行,直到生成可执行文件。
我们的实例中,LTO
整个的优化顺序为:
- 首先读取
a.o
(bitcode
文件)收集符号信息。链接器将foo1()、foo2()、foo4()
识别为全局符号。 - 读取
main.o
(真正的目标文件),找到目标文件中使用的符号信息。此时,main.o
使用了foo1()
,定义了foo4()
. - 链接器完成了符号解析过程后,发现
foo2()
未在任何地方使用它将其传递给LTO
。foo2()
一旦可以删除,意味着发现foo1()
里面调用foo3()
的判断始终为假,也就是foo3()
也没有使用,也可以删除。 - 符号处理完毕后,将处理结果传递给优化器和代码生成器,同时,将
a.o
合并到main.o
中。 - 修改
main.o
的符号表信息。继续链接,生成可执行文件。
查看最后生成的可执行文件main
的符号表信息:
可以看到,链接完成之后,我们自己声明的函数只剩下:main
、foo1
和foo4
。
这个地方有个问题,foo4
函数并没有在任何地方使用,为什么没有把它干掉?
因为LTO
优化以入口文件需要的符号为准,来向外进行解析优化。所以,要优化掉foo4
,那么就需要使用一个新的功能dead strip
。
dead strip
链接器的-dead_strip
参数的作用是:
Remove functions and data that are unreachable by the entry point or exported symbols.
简单来讲,就是移除入口函数或者没有被导出符号使用到的函数或者代码。
现在foo4
正是符合这种情况,所以,可以通过-dead_strip
来删除掉无用代码。
放大到动态库,在创建动态库时可以使用-mark_dead_strippable_dylib
:
Specifies that the dylib being built can be dead strip by any client. That is, the dylib has no initialization side effects. So if a client links against the dylib, but never uses any symbol from it, the linker can optimize away the use of the dylib.
指明,如果并没有使用到该动态库的符号信息,那么链接器将会自动优化该动态库。不会因为路径问题崩溃。
同时,你也可以在App
中使用-dead_strip_dylibs
获得相同的功能。
Code Generation Options
代码生成约定的选项:
-
None[-O0]
不优化:
在这种设置下, 编译器的目标是降低编译消耗,保证调试时输出期望的结果。程序的语句之间是独立的:如果在程序的停在某一行的断点出,我们可以给任何变量赋新值抑或是将程序计数器指向方法中的任何一个语句,并且能得到一个和源码完全一致的运行结果。
-
Fast[-O1]
大函数所需的编译时间和内存消耗都会稍微增加:
在这种设置下,编译器会尝试减小代码文件的大小,减少执行时间,但并不执行需要大量编译时间的优化。在苹果的编译器中,在优化过程中,严格别名,块重排和块间的调度都会被默认禁止掉。此优化级别提供了良好的调试体验,堆栈使用率也提高,并且代码质量优于None[-O0]。
-
Faster[-O2]
编译器执行所有不涉及时间空间交换的所有的支持的优化选项:
是更高的性能优化Fast[-O1]。
在这种设置下,编译器不会进行循环展开、函数内联或寄存器重命名。和Fast[-O1]项相比,此设置会增加编译时间,降低调试体验,并可能导致代码大小增加,但是会提高生成代码的性能。
-
Fastest[-O3]
在开启Fast[-O1]
项支持的所有优化项的同时,开启函数内联和寄存器重命名选项:
是更高的性能优化Faster[-O2],指示编译器优化所生成代码的性能,而忽略所生成代码的大小,有可能会导致二进制文件变大。还会降低调试体验。
-
Fastest, Smallest[-Os]
在不显着增加代码大小的情况下尽量提供高性能:
这个设置开启了Fast[-O1]项中的所有不增加代码大小的优化选项,并会进一步的执行可以减小代码大小的优化。增加的代码大小小于Fastest[-O3]。与Fast[-O1]相比,它还会降低调试体验。
-
Fastest, Aggressive, Optimizations[-Ofast]
与Fastest, Smallest[-Os]
相比该级别还执行其他更激进的优化:
这个设置开启了Fastest[-O3]中的所有优化选项,同时也开启了可能会打破严格编译标准的积极优化,但并不会影响运行良好的代码。该级别会降低调试体验,并可能导致代码大小增加。
-
Smallest, Aggressive Size Optimizations [-Oz]
不使用LTO
的情况下减小代码大小:
与-Os相似,指示编译器仅针对代码大小进行优化,而忽略性能优化,这可能会导致代码变慢。
总结:
strip
strip
:移除指定符号。在Xcode
中默认strip
是在Archive
的时候才会生效,移除对应符号。
strip -x:除了全局符号都可以移除 (动态库使用)
strip -S:移除调试符号(静态库使用)
strip:除了间接符号表中使用的符号,其他符号都移除(上架App使用)
-
Deployment Postprocessing
的英文原意是:
If enabled, indicates that binaries should be stripped and file mode, owner, and group information should be set to standard values.
也就是打开后,在编译阶段就会运行strip
。
-
Strip Debug Symbols During Copy
的英文原意是:
Specifies whether binary files that are copied during the build, such as in a Copy Bundle Resources or Copy Files build phase, should be stripped of debugging symbols. It does not cause the linked product of a target to be stripped。
通俗来讲,就是当你的应用在编译阶段copy
了某些二进制文件时,打开该选项会脱掉该二进制的调试符号。但是不会脱去链接的最终产物(可执行文件\动态库)的符号信息。要脱去链接的产物(App
的可执行文件)的符号信息。
-
Strip Linked Product
的英文原意是:
If enabled, the linked product of the build will be stripped of symbols when performing deployment postprocessing.
如果没有打开Deployment Postprocessing
,则会在Archive
处理链接的最终产物(可执行文件)的符号信息。否则,在链接完成之后就会处理符号信息。
查看App Size报告
- 方式一: 通过
App Store Connect
提供准确的App
大小; - 方式二:通过
Xcode
内置报告工具,创建App
尺寸报告
a.Archive App
;
b. 通过Ad Hoc
、Development
或者Enterprise
等分发方式导出Archive App
;
c. 在设置开发分发选项的列表中,选择All compatible device variants
以进行应用程序精简,然后启用Rebuild from Bitcode
;
d. 签名并且导出。
此过程将创建一个包含App
的文件夹,里面有:
a. 一个Universal IPA
,包含多个平台的资源文件和二进制程序;
b. 一个Thinned IPA
,指定平台的资源文件和二进制程序。
同时还包含一个App Thinning Size Report.txt
,里面详细记录了App
的体积占用情况:
App Thinning Size Report for All Variants of ExampleApp
Variant: ExampleApp.ipa
Supported variant descriptors: [device: iPhone11,4, os-version: 12.0], [device: iPhone9,4, os-version: 12.0], [device: iPhone10,3, os-version: 12.0], [device: iPhone11,6, os-version: 12.0], [device: iPhone10,6, os-version: 12.0], [device: iPhone9,2, os-version: 12.0], [device: iPhone10,5, os-version: 12.0], [device: iPhone11,2, os-version: 12.0], and [device: iPhone10,2, os-version: 12.0]App + On Demand Resources size: 6.7 MB compressed, 18.6 MB uncompressed
App size: 6.7 MB compressed, 18.6 MB uncompressed
On Demand Resources size: Zero KB compressed, Zero KB uncompressed// Other Variants of Your App.
- 方式三:通过脚本的方式指定输出
App Size
报告:
xcodebuild -exportArchive -archivePath iOSApp.xcarchive -exportPath Release/MyApp -exportOptionsPlist OptionsPlist.plist
参考链接
LLVM Link Time Optimization: Design and Implementation
ThinLTO: Scalable and Incremental LTO
Link Time Optimization (LTO)
Selecting optimization options
Reducing Your App’s Size