OC包体结构与优化
Objc包结构
1、背景
iOS系统对应用程序的二进制包大小有严格的限制,ios7、ios8系统TEXT字段只支持60M(60 *1000字节)大小的应用程序,目前XX音乐app已多次超出这个限制。为解决包大小问题,需要深入了解整个包的结构,并找出解决办法。
2、编译过程
要了解包结构,先从objc包的编译过程进行讲解,如果大家有了解过,应该都知道目前新版的xcode都是用llvm进行源码编译的,llvm编译的好处有很多文章介绍过,今天主要要讲解的是llvm的编译过程及其附属产物,以及怎么验证。
(1)llvm的编译架构设计主要分为三层:
llvm结构前端——代码优化器——后端
(2)clang编译过程
通过命令了解整个编译过程:
clang -ccc-print-phases sample.m,查看完整的编译过程
0: input, "sample.m", objective-c
1: preprocessor, {0}, objective-c-cpp-output
2: compiler, {1}, ir
3: backend, {2}, assembler
4: assembler, {3}, object
5: linker, {4}, image
6: bind-arch, "x86_64", {5}, image
clang -### sample.m,显示编译过程用到的命令
clang -### sample.mApple LLVM version 9.0.0 (clang-900.0.39.2)Target: x86_64-apple-darwin16.7.0Thread model: posixInstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang" "-cc1" "-triple" "x86_64-apple-macosx10.12.0" "-Wdeprecated-objc-isa-usage" "-Werror=deprecated-objc-isa-usage" "-emit-obj" "-mrelax-all" "-disable-free" "-disable-llvm-verifier" "-discard-value-names" "-main-file-name" "sample.m" "-mrelocation-model" "pic" "-pic-level" "2" "-mthread-model" "posix" "-mdisable-fp-elim" "-fno-strict-return" "-masm-verbose" "-munwind-tables" "-target-cpu" "penryn" "-target-linker-version" "305" "-dwarf-column-info" "-debugger-tuning=lldb" "-resource-dir" "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/9.0.0" "-fdebug-compilation-dir" "/Users/zerryzarax/Desktop/llvm例子" "-ferror-limit" "19" "-fmessage-length" "80" "-stack-protector" "1" "-fblocks" "-fobjc-runtime=macosx-10.12.0" "-fencode-extended-block-signature" "-fobjc-exceptions" "-fexceptions" "-fmax-type-align=16" "-fdiagnostics-show-option" "-fcolor-diagnostics" "-o" "/var/folders/0j/wwr16xvd6k73wsgplg230p4c0000gn/T/sample-f35851.o" "-x" "objective-c" "sample.m" "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/ld" "-demangle" "-lto_library" "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/libLTO.dylib" "-no_deduplicate" "-dynamic" "-arch" "x86_64" "-macosx_version_min" "10.12.0" "-o" "a.out" "/var/folders/0j/wwr16xvd6k73wsgplg230p4c0000gn/T/sample-f35851.o" "-lSystem" "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/clang/9.0.0/lib/darwin/libclang_rt.osx.a"
可以看到这里执行了两大程序,clang和ld,所以一般我们直接调用clang命令是包含完整的编译过程的
clang --analyze sample.m,就是平时xcode所用的analyze功能,最终会生成plist文件,把所有的分析结果都汇总在里面
clang -E sample.m -o samplepre,预编译文件,会把引用到的代码都会加到同一个文件上(对应过程1)
clang -emit-llvm -S sample.m -o sample(ir).ll,只进行编译,并输出ir代码。(对应过程2)
clang -S sample.m -o sample(x86).ll,编译成x86汇编代码(对应过程3)
xcrun -sdk iphoneos clang -arch arm64 -S sample.m -o sample(arm64).ll(对应过程3)
clang -c sample.m,编译成.o文件,在pc上运行默认编译成x86的命令(对应过程4)
clang -fembed-bitcode -c sample.m,会编译成.o文件,但是不一样的是,这次会多了以下llvm段数据
Section
sectname __bitcode
segname __LLVM
addr 0x0000000000000090
size 0x0000000000000be0
offset 1088
align 2^4 (16)
reloff 0
nreloc 0
flags 0x00000000
reserved1 0
reserved2 0
Section
sectname __cmdline
segname __LLVM
addr 0x0000000000000c70
size 0x0000000000000042
offset 4128
align 2^4 (16)
reloff 0
nreloc 0
flags 0x00000000
reserved1 0
reserved2 0
/usr/bin/ld 对应过程5、6
(3)额外,ast树查看
通过clang -Xclang -ast-dump -fsyntax-only sample.m命令可以查看语法树生成的情况
3、objc包结构总览
直接命令:clang -framework Foundation sample.m
(1)把生成的a.out文件拖入到hopper,可以观察到包的总体结构分为以下几部分:
二进制包结构4、linkmap对比
(1)打开xcode的link map file功能:build setting->write link map file设置为YES,path to link map file修改为你想要创建文件的位置。如下图所示:
link map配置(2)文件结构如下图所示:
object file section symbols可以看出主要分为三大块内容:object file、section、symbols。
object file中可以看出,编译过程总共处理了哪些文件;section基本与二进制文件中描述的分段保持一致;symbols上就列举了代码中的方法信息、字符串信息等。
今天研究二进制包主要是为了解决包大小问题,从背景中提到TEXT区域超出限制大小,而这种问题的主要解决办法是需要删减无用的代码。在认真观察link map内容后,我们发现link map虽然提供了一些基本信息,但是不足以满足我们删代码的要求(无法得出完整的类结构),所以我们需要另辟蹊径。
ps:虽然link map不能满足删减代码的工作,但是可以看出,编译过程中的最后一步是需要利用这个文件把所有的.o文件合并,并重新生成二进制包每个段的偏移地址的。
5、otool命令
通过上面的包结构分析,明显发现,要解决TEXT段超出上限的这个问题需要从二进制包作为突破口。幸运的是xcode为我们提供了个便利的工具:otool
(1)otool -oV
这个神奇的命令帮我们处理了大量的工作,命令里面包含主要内容:__DATA __objc_classlist、__DATA __objc_classrefs、__DATA __objc_superrefs、__DATA __objc_protolist、__DATA __objc_imageinfo,备注:这些信息大部分已经翻译好,省去了大量的工作,且满足剔除无用类的判断
(2)算法查找过程
查找无用类的过程(3)otool -v -s
直接返回二进制包对应区域、段落的内容出来,并且如果可以成功翻译的情况下会进行字符串翻译。利用这个命令otool -v -s __DATA __objc_selrefs(返回所有会被执行的方法引用),并结合第一个命令,我们可以查找无用方法。过程如下:
查找无用方法(4)查找动态生成的类
利用otool -l缓存__TEXT __stubs的起始地址和结束地址,objdump -lazy-bind缓存NSClassFromString的调用地址,otool -v -s __TEXT __stubs获取所有动态库命令地址,从中找出执行命令地址和NSClassFromString的映射关系;otool -v -s __TEXT __text遍历命令,查找出NSClassFromString命令号,并反查出入参字符串,最终确定动态调用的类
需要18分钟,2000w行代码,有误判情况,有特殊情况无法查找。(不建议使用)
(5)无用方法排除协议误判
利用otool -v -s __TEXT __objc_classname缓存类名,otool -v -s __DATA __objc_const缓存协议寻址结构,otool -v -s __TEXT __objc_methname缓存方法名,通过otool -v -s __DATA __data类协议和协议方法名的关系,最终汇总所有方法名和协议。然后从所有类方法里面,排除掉类引用了协议的方法。
6、目前包大小处理缺陷
目前包大小处理缺陷有以下几点:
1、无法完全处理动态生成的类,虽然目前提供的方法可行,但是效率太低,没有实际用在生产环境上
2、代码写得不规范容易出现误删情况,无法自动化
3、系统基类方法协议无法追踪(目前已经有相关解决思路,可能后面会补充)