2020.04补充:
-
本文最开始是2019/04写的,时隔一年后,趁着假期的时间,把之前的的不足和新的体会补充补充。
-
之前做包优化,主要是清理无用的图片,这个见效果快;然后根据修改编译器选项来优化包大小,这个也比较省心;
-
后续在优化中,遇到些困惑和质疑,最终算是勉强解决吧;这些过程中,也沉淀了两篇文章PNG图片原理二三事 和Mach-O文件周边二三事
一、概述【2020.04新增】
1、致谢Apple
- 在Mach-O文件周边二三事中指出,Apple在2019年9月,iOS13正式版本后,直接放开了蜂窝网络下App下载大小的限制,刚好国内流量费用也下降了;
- 截止2020/01/27,iOS12及其iOS13以上的设备占比超过93%,iOS项目几乎都最低支持iOS 9起步,所以Apple的iOS 9之后的隐藏福利:二进制文件中所有
__TEXT部分的总和不超过500 MB排上用场了,再也不用被60MB魔咒困住了; - 有鉴于此,App的瘦身的优先级变得不那么高优了;我们可以放心大胆去搞业务了。
2、App瘦身要做的理由
- App瘦身还是要做,主要是两个方面考虑:
- 一方面,从用户角度看,App保持高速迭代,几乎每周都有更新,频繁让用户去下载那么大体积的App,用户有点不开心,积极性不高;
- 另一个方面,从高速迭代角度看:不同团队不断合入代码和资源,不加以约束或优化,包只可能无限制增长下去,最终火急火燎再优化,比较难受;
- 对于追求更高品质的App,希望在竞品中拔得头筹,还是需要在包大小方面花很大功夫的,不是搞几个技术需求就能搞定的。
3、App瘦身的目的
- 摸清基本盘:搞清楚现在的基本情况,然后优化当前存在的问题:资源和二进制部分的优化;
- 规划后续的事情:"打压"各业务包大小,预期每个业务将包大小控制在合适的范围;建立规范和检查机制,无效的资源和代码不得合入;定期对各业务的包大小进行review,有则改之,无则加勉;
4、【早年】缓解__TEXT大小Simple办法
-
现在不需要了,但是办法很有意思;简单说:就是将
__TEXT中的__cstring,__gcc_except_tab,__const从__TEXT段移到__DATA段中;因为这三个段的数据其实就是数据,并不是代码,放在__DATA段里面也行。 -
配置也简单,在工程的Build Setting 里面的 Other Linker Flag里面添加如下Flag:
-Wl,-rename_section,__TEXT,__cstring,__RODATA,__cstring -Wl,-rename_section,__TEXT,__gcc_except_tab,__RODATA,__gcc_except_tab -Wl,-rename_section,__TEXT,__const,__RODATA,__const -
不过需要注意的是:
__TEXT段包含的数据只能读,而__DATA中的数据是可读可写的;因此,这种做法还有点风险,不过基于对iOS封闭系统的信任,姑且认为这个风险很低;
5、【当前】缓解__TEXT大小主流办法
-
iOS 8之后可以使用自定义的动态库减少__TEXT大小问题;
-
iOS8之前,不允许自定义动态库,但是iOS8之后,Apple允许使用自定义的动态库,但是这种动态库和系统动态库不同,只允许APP和APP Extension共享,不能被其他进程使用;不过这个可以帮助减少Mach-O文件大小,减少
_TEXT段压力,这些库最终在包的Frameworks文件夹中。【Embedded Framework】
二、安装包组成分析
1、组成情况
将IPA包修改后缀名为ZIP,解压缩后,获取payload中的App文件,查看App文件的内容,你会发现该文件主要包含以下内容
- Exectutable: 可执行文件
- Resources:资源文件
- 图片资源:Assets.car/bundle/png/jpg 等
- 视频/音频资源:mp4/mp3 等
- 静态网页资源:html/css/js 等
- 视图资源:xib/storyboard 等
- 其他:文本/字体/证书 等
- Framework:
- SwiftSupport: libSwiftxxx 等一系列 Swift 库
- 其他依赖库:Embeded Framework
- Pulgins:Application Extensions
- appex:其组成大致与 ipa 包组成一致
2、组成分析
- 一般来说,可执行文件、图片资源(asset.car)和动态库的占比最大,如果是Swift和OC混编,可执行文件比纯OC大很多
- 从优化的效果上看,优化图片资源的ROI比较大,如果是首次优化,建议从图片资源的优化开始。
- 项目中使用Swift,会增加安装包大小,因为FrameWork中会加入为了支持 Swift 的动态库集合,如果纯Swift项目,不会引入这些东西。
3、补充一点
- 包大小的评估很长一段时间是看IPA文件大小,这是错误的。应该以AppStore中看到的包大小为准。
三、资源文件优化
理论上,资源文件包括:图片**、视频、**音频和字体等;实际上,视频和音频文件一般不会集成到安装包中,在安装包中的资源文件主要是图片。图片一般采用PNG图片;
1、必选App Slicing
- iOS 9之后提供了App Thinning三件套:App Slicing、On Demand Resouces、Bitcode;使用好App Slicing,能轻松帮助实现图片资源的瘦身;
| App Thinning | 理想 | 现实 |
|---|---|---|
| App Slicing | 将App Bundle资源根据不同的设备特性分为不同的版本。对于图片资源,会根据设备所需图片分辨率不同分发给对应设备所需对应的图片资源。 | 主要是图片资源的Slicing,我们有自己的方案,没有采用 |
| On Demand Resources | App的资源只有要使用时才下载,如果其他资源需要空间这些资源可以被移除 | 更适合游戏类App,项目没有使用 |
| Bitcode | Bitcode可以作为中间产物一起提交AppStore。包含Bitcode配置的程序将会在AppStore上被编译和链接。Bitcode允许苹果在后期重新优化我们程序的二进制文件,而不需要我们重新提交一个新的版本到AppStore上 | 使用BitCode的要求所有代码都支持BitCode,改动项目较大,没有使用 |
- 在项目中引入图片时候,直接在 Assets.xcassets中添加就可以(资源文件用Asset Catalog管理),这样能使用到App Slicing功能,这样当用户从App Store上下载App时,可以只下载适用于其设备的App架构版本和所需资源,从而减少App所占的空间。
- 使用Assets.xcassets还有一个隐藏福利:在构建的时候,会将Assets.xcassets中图片进行(默认)压缩(压缩过程叫compile asset catalog),使用的是压缩工具pngcrush,它存放在
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/usr/bin目录下;最后Assets.xcassets中的所有文件会被打包为Assets.car的文件。 - 补充:iOS 9没有出来时候【那年我还是小鲜肉o(╥﹏╥)o】,项目中使用了PDF矢量图(存在Bundle中)来支持支持不同的设备;根据不同设备,解析PDF源文件,将矢量图绘制成所需大小的位图,从而实现了一套图片适配多机型。了解PDF解析并绘制位图实现可以看YHPDFImageLoader
2、高ROI:清理无用和重复的图片
-
清理无用资源永远是有用的,图片优化中,这部分收益很大;网络上也有现成的工具LSUnusedResources ;虽然工具可能将有些拼接出来的图片名的图片误判为无用图片,但是只需要业务上确认下即可;ROI比较高;
-
LSUnusedResources的思路是,先获取图片文件(imageset, jpg, png, gif)集合A,然后搜索代码文件中所有字符串名称得到B,然后从A集合中排除集合B就得到未使用的图片资源。
-
清理重复的图片资源,也是一个不错的办法;遍历图片文件,将MD5值相同相同的图片抛出来,选择一个使用记录,其他都删除吧。
3、其他手段
- 使用iconfont代替项目中纯色小图标,也省去很多@2x和@3x的图片切图。
- 将某些不常用模块的图片放在服务器上,App启动后,在某个比较空闲的时间离线下载图片资源,到了这些模块再使用下载好的图片,如果没有下载好,重新下载;(资源按需加载)
4、补充Compress PNG Files知识
-
当工程目录下有PNG图片才会出现
Compress PNG Files选项;但是对于一个规范的工程,PNG图片都放在Assets.xcassets, 项目下没有PNG图片,项目中就没有该选项,同样没有Remove Text Medadata From PNG Files -
Compress PNG Files使用的是也是压缩工具pngcrush;不要把压缩包大小的事情寄托这个pngcrush工具,因为它的本质:并不是为了压缩图片大小,而是将PNG图片转换成iOS更好处理的格式,加快处理速度;有些图片经过该工具,图片反而变大了,最终的包还变大了; -
Remove Text Medadata From PNG Files:设置为YES,能帮助我们移除 PNG 资源的文本字符,比如图像名称、作者、版权、创作时间、注释等信息。(如果PNG图片都在Assets.xcassets不用care这个事了哈 )
5、pngcrush教我对PNG压缩要佛系
-
PNG图片是一种无损压缩的图片格式,大部分PNG图片的压缩都是无损压缩,但是大神们还是设计出了有损压缩算法,如pngquant算法。
-
市面上,压缩工具中的
ImageAlpha是无损的, ImageOptim、tinypng是有损的(内含pngquant算法),此外ImageOptim还贴心加入了pngcrush工具; -
不管你的PNG图片放在
Assets.xcassets,还是项目中;最后编译时候都会被pngcrush工具处理;这些图片即使之前被(无损 or 有损)压缩处理了,只要经过了pngcrush的处理,图片大小方面是没什么收益的,甚至会变更大。 -
所以,不必要在图片压缩上特意下功夫,佛系些,摘抄pngcrush介绍中最开始部分
Pngcrush is an optimizer for PNG (Portable Network Graphics) files. It can be run from a commandline in an MSDOS window, or from a UNIX or LINUX commandline. Its main purpose is to reduce the size of the PNG IDAT datastream by trying various compression levels and PNG filter methods. It also can be used to remove unwanted ancillary chunks, or to add certain chunks including gAMA, tRNS, iCCP, and textual chunks.
四、二进制文件优化 之 编译器选项
1、概述
- Xcode 支持编译器层面的一些优化优化选项,可以让我们介于更快的编译速度、更小的二进制大小和更快的执行速度之间自由选择想要进行的优化粒度;
2、编译器优化级别
-
Optimization Level:Release选择
Smallest, Aggressive Size Optimizations [-Oz],这样尽可能地减小代码体积,但是可能会编译可能要话更多的时间; Debug没必要那么担心包大小,设置None[-O0]就行; -
使用Clang编译器Objective-C,Optimization Level取值如下
None[-O0]: 不优化。在这种设置下, 编译器的目标是降低编译消耗,保证调试时输出期望的结果。程序的语句之间是独立的:如果在程序的停在某一行的断点出,我们可以给任何变量赋新值抑或是将程序计数器指向方法中的任何一个语句,并且能得到一个和源码完全一致的运行结果。 Fast[-O1]: 大函数所需的编译时间和内存消耗都会稍微增加。在这种设置下,编译器会尝试减小代码文件的大小,减少执行时间,但并不执行需要大量编译时间的优化。在苹果的编译器中,在优化过程中,严格别名,块重排和块间的调度都会被默认禁止掉。 Faster[-O2]: 编译器执行所有不涉及时间空间交换的所有的支持的优化选项。在这种设置下,编译器不会进行循环展开、函数内联或寄存器重命名。和‘Fast[-O1]’项相比,此设置会增加编译时间和生成代码的性能。 Fastest[-O3]: 在开启‘Fast[-O1]’项支持的所有优化项的同时,开启函数内联和寄存器重命名选项。这个设置有可能会导致二进制文件变大。 Fastest, Smallest[-Os]: 优化大小。这个设置开启了‘Fast[-O1]’项中的所有不增加代码大小的优化选项,并会进一步的执行可以减小代码大小的优化。 Fastest, Aggressive Optimizations[-Ofast]: 这个设置开启了“Fastest[-O3]”中的所有优化选项,同时也开启了可能会打破严格编译标准的积极优化,但并不会影响运行良好的代码。 Smallest, Aggressive Size Optimizations [-Oz]: 与相似-Os,但会进一步减小代码大小,并且可能需要更长的时间才能运行。 -
使用SwiftLang来编译Swift语言,同样也是基于 LLVM 后端的。Xcode 9.3 版本之后可以在Build Setting -> Optimization Level 设置,Release下为Optimize for Speed[-O],这可能会增加安装包大小
No optimization[-Onone]:不进行优化,能保证较快的编译速度。 Optimize for Speed[-O]:编译器将会对代码的执行效率进行优化,一定程度上会增加包大小。 Optimize for Size[-Osize]:编译器会尽可能减少包的大小并且最小限度影响代码的执行效率说明:Xcode 9.3/Swift4.1编译器不是特别稳定,特别是开启 Osize 选项之后,编译器很多情况下会莫名其妙的崩溃(Segmentation fault),目前放弃 [-Osize],选择[-O]
3、去除符号信息
-
可执行文件中的符号:程序中的所有的变量、类、函数、枚举、变量和地址映射关系,以及一些在调试的时候使用到的用于定位代码在源码中的位置的调试符号,符号和断点定位以及堆栈符号化有很重要的关系。
-
Strip Style表示的是我们需要去除的符号的类型的选项,可以在
Build Setting -> Strip Style设置, Release下为All Symbols,三个选择项的意义如下:All Symbols: 去除所有符号,一般是在主工程中开启。 Non-Global Symbols: 去除一些非全局的 Symbol(保留全局符号,Debug Symbols 同样会被去除),链接时会被重定向的那些符号不会被去除,此选项是静态库/动态库的建议选项。 Debug Symbols: 去除调试符号,去除之后将无法断点调试。说明:iOS 的调试符号是 DWARF 格式的,使用 Xcode 编译打包的时候会先通过可执行文件的 Debug Map 获取到所有对象文件的位置,然后使用 dysmutil 来将对象文件中的 DWARF 提取出来生成 dSYM 文件。
-
Strip Linked Product:去除不必要的符号信息,去除了符号信息之后我们就只能使用 dSYM 来进行符号化了,所以需要将
Debug Information Format修改为DWARF with dSYM file。Release下为YES。需要注意的是,Strip Linked Product选项在 Deployment Postprocessing 设置为 YES 的时候才生效,而在 Archive 的时候 Xcode 总是会把 Deployment Postprocessing 设置为 YES,Debug下,Deployment Postprocessing 设置为 NO。 -
Strip Debug Symbols During Copy:将那些拷贝进项目包的三方库、资源或者 Extension 的 Debug Symbol 去除掉,在Build Settings -> Strip Debug Symbols During Copy设置,Release下设置为YES。
-
Cocoapods 管理的动态库(use_framework!)的情况就相对要特殊一点,因为 Cocoapods 中的的动态库是使用自己实现的脚本 Pods-xxx-frameworks.sh 来实现拷贝的,所以并不会走 Xcode 的流程,当然也就不受 Strip Debug Symbols During Copy 的影响。当然 Cocoapods 是源码管理的,所以只需要将源码 Target 中的 Strip Linked Product 设置为 YES 即可。
-
Strip Swift Symbols能帮助我们移除相应 Target 中的所有的 Swift 符号,这个选项也是默认打开的。Strip Swift symbols需要在打包的发布选项中勾选(默认勾选),在Swift ABI 稳定之前,Swift 标准库是会打进目标文件的。
4、BitCode
-
Bitcode可以作为中间产物一起提交AppStore。包含Bitcode配置的程序将会在AppStore上被编译和链接。Bitcode允许苹果在后期重新优化我们程序的二进制文件,而不需要我们重新提交一个新的版本到AppStore上。 -
开启 BitCode 之后编译器后端(Backend)的工作都由 Apple 接管了。所以假如以后苹果推出了新的 CPU 架构或者以后 LLVM 推出了一系列优化,我们也不再需要为其发布新的安装包了。
-
工程开启 BitCode 之后必须要求所有打进 Bundle 的 Binary 都需要支持 BitCode,也就是说我们依赖的静态库和动态库都是含有 BitCode 的,不然就会打包失败。对于 Cocoapods 等源码管理工具来管理的依赖库来说操作会比较简单,我们只需要开启 Pods 工程中的 BitCode 就行。但是对于一些三方的闭源库,我们就无能为力了。
-
开启 BitCode 之后,由于最终的可执行文件是 Apple 自动生成的,同时产生新的符号表文件,所以我们使用原本打包生成的 dSYM 符号化文件是无法完成符号化的。所以我们需要在上传至 App Store 时需要勾选
Include app symbols for your application to receive symboilcated crash logs from Apple:勾选之后 Apple 会给我们生成 dSYM,然后就可以在Xcode -> Organizer或者iTunes Connect中下载对应的 dSYM 来进行符号化了
5、推荐个靠谱配置
-
Generate Debug Symbols:DEBUG和RELEASE下均设为YES(和Xcode默认一致); -
Debug Information Level:DEBUG和RELEASE下均设为Compiler default(和Xcode默认一致); -
Deployment Postprocessing:DEBUG下设为NO,RELEASE下设为YES,这样RELEASE模式下就可以去除符号缩减app的大小(但是似乎设置为YES后,会牵涉一些和bitcode有关的设置,对于bitcode暂时还不太了解(´・_・`)); -
Strip Linked Product:DEBUG下设为NO,RELEASE下设为YES,用于RELEASE模式下缩减app的大小; -
Strip Style:DEBUG和RELEASE下均设为All Symbols(和Xcode默认一致); -
Strip Debug Symbols During Copy:DEBUG下设为NO,RELEASE下设为YES; -
Debug Information Format:DEBUG下设为DWARF,RELEASE下设为DWARF with dSYM File,dSYM文件需要用于符号化crash log(和Xcode默认一致)。
五、二进制文件优化 之 清理代码
1、Dead Code Stripping的不足
- Xcode 默认会开启此选项,C/C++/Swift 等静态语言编译器会在 link 的时候移除未使用的代码,但是对于 Objective-C 等动态语言是无效的。
- 这是因为 Objective-C 是建立在运行时上面的,底层暴露给编译器的都是 Runtime 源码编译结果,所有的部分应该都是会被判别为有效代码。
- 因此,在清理代码方面,我们需要具体定制方案
2、清理代码方案
- 我们要清理的是:无用的类、无用的协议和无用的办法。我们采用曲线解决问题的办法:利用Mach-O文件找出无引用的类、协议和办法,然后根据LinkMap文件,将这些划分会不同业务,交给具体的业务进一步确认,确认无用后删除;
- 无引用的类 =
__objc_classlist - (__objc_classrefs+__objc_superrefs) - 无引用的方法 =
__objc_classlist的(instanceMethods - __objc_selrefs) + clasMethods - __objc_selrefs - 无引用的协议 =
__objc_protolist - (__objc_classrefs+__objc_superrefs) 的protocol_list
3、方案实践
-
(自己从头写,好辛苦);直接使用已有工具Snake,需要简单设置下;
- 将其中的snake拷贝到目录中,比如
$HOME/custom-tool/bin目录下; - 打开~/.bash_profile文件:vi ~/.bash_profile,在文件最上方加一行:
export PATH=$HOME/custom-tool/bin/:$PATH,然后保存并退出 - 执行
source ~/.bash_profile; - 至此,Snake工具生效。
- 将其中的snake拷贝到目录中,比如
-
找到无引用类、方法和协议
snake -l app_name-LinkMap.txt app_name.app/app_name -c > app_name_unref_class.txt snake -l app_name-LinkMap.txt app_name.app/app_name -s > app_name_unref_selector.txt snake -l app_name-LinkMap.txt app_name.app/app_name -p > app_name_unref_protocol.txt -
将这些信息同步给业务同学,辛苦他们确认并删除。
六、补充
- 对于业务开发者说,及时清理无用代码、无用资源和无用库;代码合入中,不带无用的代码和资源,这些都是好习惯了;
- 对于包优化同学,完善检查机制,无效的资源和代码不得合入;
- 对于团队,定期对各业务的包大小进行review,有则改之,无则加勉;