安装包瘦身

近期我们的主应用收到了appStore的邮件,说项目超过了100m,需要瘦身。其实当应用超过100m的时候只是说不能使用流量下载,只能在wifi环境下下载,其他的倒是没什么影响,但是毕竟是用户超过几千万的应用,所以主应用要求我们sdk要进行瘦身,目标至少20%。

收到这个任务以后,我们就着手进行处理。主要从以下几个方面。

一、资源文件

1.删除无用资源

解压ipa文件,检查是否有无用资源存在。

现在应该没有APP需要支持iPhone4以下的机型了,所以1X的图片可以全部删掉。3X的图片是保留还是删掉看具体情况。

重复的图片分两种,一种是名字一样的图片,如果你使用.xcassets来管理图片,那么Xcode的左边栏会有警告提示图片名字重复,直接按提示一一处理即可。另一种是名字不一样但是文件一样的图片,我们使用了一个Python脚本(@甘超江 大神出品)来扫描,每次编译的时候执行该脚本,如果有扫描命中则会让Xcode编译失败,此时需要人工去处理。需要注意的一点就是使用.xcassets来管理图片的时候回存在一个映射关系,通过imageNamed:方法使用的名字和图片的真实名字有可能不一样,脚本扫描的时候需要特别处理下。

未使用的图片可以通过LSUnusedResources扫描出来,不过要注意的是可能会有误伤,该工具是全匹配,一些拼接名字来使用的图片要注意手动剔除。笔者就因为误删图片被惩罚过o(╯□╰)o

一些音频、视频和多余的plist文件以及readme文件什么的目测只能肉眼扫描了,我们没用到这些资源暂时没这个问题。

NOTE
这里也可以使用工具slender

2.资源压缩

首先是图片压缩,ImageOptim工具可以实现无损压缩。

另外关于图片,建议使用Apple推荐的.xcassets来管理,它会把里边的所有png格式的图片压缩成一个Assets.car文件,压缩比率比其他方式管理图片要高。不过测试发现jpg图片不会在Assets.car文件里。

另如果你有用到音频或视频资源,也可以考虑压缩。

3.H5页面远端化

如果你的H5有本地页面和资源,可以考虑全部远端化。本地资源主要是一些js、html文件和图片。

4.资源远端化

如果你本地有大量的图片资源或者其他乱七八糟的资源,则可考虑将一些不会影响用户体验同时兼顾服务器压力的资源放到远端,通过请求的方式去下载,之后在缓存到本地。

5.使用Icon Font

每个iconfont只是一小段文本,size要比图片形式的icon小一个数量级,我们可以像使用字体一样来使用图片。

生成iconfont需要 矢量图 。一些网站提供生成iconfont的服务,比如icomoonFontello阿里巴巴矢量图标库easyicon提供大量优秀的矢量图。
icomoonFontello均可以通过导入SVG图标或者选择网站自身提供的图标来生成iconfont。值得一提的是,icomoon还可以生成PNG,PDF,CSH等格式。
在生成的文件夹中,可以找到扩展名为 .ttf 的字体集文件。

6.使用WebP

WebP是Google推的方案,无损压缩后的WebP比PNG少了45%的size,即使PNG经过其他工具压缩之后,WebP还是可以减少28%的size。SDWebImage实现了加载WebP格式图片的功能,但是没有做缓存功能,可能需要我们自己来实现了。

参考

FontAwesomeKit
using-icon-font-in-ios

二、Bitcode

严格来说App Thinning不会让安装包变小,所以想通过这种方式来减小安装包的大小,就不要考虑了。但用户安装应用时,苹果会根据用户的机型自动选择合适的资源和对应CPU架构的二进制执行文件(也就是说用户本地可执行文件不会同时存在armv7和arm64),安装后空间占用更小。

App Thinning 的其中一个方面是Bitcode。Bitcode有些抽象,但在本质上它也是苹果在用户下载前优化app的新方式。Bitcode使得app无论在何设备上都能快速高效地运行。Bitcode使用最新的编译器自动编译app并且针对特定架构进行优化。(例如,针对 iPhone 6s和 iPad Air 2等 64 位处理器的 arm64)

Bitcode 不会下载应用针对不同架构的优化,而仅下载与特定设备相关的优化,使得下载量更小,同时与前文所述的 App Thinning 技术紧密合作。

Bitcode 是 iOS 上较新的功能,对于新的项目需要手动开启。这可以通过选择Build Settings(编译设置)下的项目设置,将 bitcode 设为 YES 来完成。

NOTE
需要注意的是,如果你想支持Bitcode,需要工程里面所有用到的静态库,动态库全部支持Bitcode(也就是生成编译的时候打开Bitcode选项),否则会导致开启失败。
同样,如果你是sdk,你开启了Bitcode,主应用未开启,也是没什么用的。

参考

App Thinning
iOS:BitCode的介绍

二、编译选项优化

1.编译器优化级别

Build Settings->Optimization Level有几个编译优化选项,release版应该选择Fastest, Smalllest,这个选项会开启那些不增加代码大小的全部优化,并让可执行文件尽可能小。

Strip Debug Symbols During Copy:DEBUG下设为NO,RELEASE下设为YES
Strip Debug Symbols During Copy设置为YES时,打开对应.app文件的“显式包内容”,可以看到,/PlugIns/Today.appex文件的大小变小了。(不过这些只能在使用模拟器时奏效)
Strip Debug Symbols During Copy置为YES的时候,today extension中的断点将不会中断,但是打印[NSThread callStackSymbols]时的类名和方法名还是可以看见的。

Build Settings -> Dead Code Stripping 设置成 Yes。参考
用于删除对象文件中不需要加载的符号,减小二进制文件大小

Build Settings -> Use Separate Strip设置为Yes。参考
剥离静态库中的二进制符号。

2.去除符号信息

Strip Linked Product / Deployment Postprocessing / Symbols Hidden by Default DEBUG下设为NORELEASE下设为YES

NOTE
这个最有用的一个选项是Deployment PostprocessingStrip Linked Product,两个需要都设置为YES才有用。原理是打开这两个选项后构建ipa会去除掉symbol符号,就是那些类名啊函数名啊啥的。这样子的影响就是运行时你没法进行线程回溯,符号都没了回溯了也是乱码。但是不会影响正常的崩溃日志生成和解析。在本机专门测试过,如果使用符号表来解析崩溃日志,则完全不受影响。
Symbols Hidden by Default会把所有符号都定义成private extern,详细信息见官方文档。这些选项目前都是XCode里release的默认选项,但旧版XCode生成的项目可能不是,可以检查一下。其他优化还可以参考官方文档—CodeFootprint.pdf
特别注意:
Generate Debug Symbols这个选项千万不能设置为NO(默认为YES),设置为NO的时候会拿不到DSYM,也就解析不到崩溃日志信息了。

3.去掉异常支持

Enable C++ ExceptionsEnable Objective-C Exceptions设为NO,并且Other C Flags添加-fno-exceptions,可执行文件减少了27M,其中gcc_except_tab段减少了17.3M,text减少了9.7M,效果特别明显。可以对某些文件单独支持异常,编译选项加上-fexceptions即可。但有个问题,假如ABC三个文件,AC文件支持了异常,B不支持,如果C抛了异常,在模拟器下A还是能捕获异常不至于Crash,但真机下捕获不了(有知道原因可以在下面留言:)。去掉异常后,Appstore后续几个版本Crash率没有明显上升。个人认为关键路径支持异常处理就好,像启动时NSCoder读取setting配置文件得要支持捕获异常,等等

NOTE
经实测不行。因为将Enable Objective-C Exceptions设为NO以后无法使用`try catch`,按照编译选项加上-fexceptions,实际上并不行,故而失败。

需要注意的是:
如果你是主工程项目就没什么说的了,但是如果你瘦身的是sdk,如果主工程已经进行了如上设置,其实你是没必要的,有时候还可能引起异常错误。

参考

Xcode中和symbols有关的几个设置
iOS IPA file size - xcode-archive vs. xcodebuild command
crash log调用栈看不到的解决方案(debug symbols不起作用)

三、可执行文件瘦身

必杀技:AppCode

维护时间较长的代码,可能会出现废弃的类出现。在代码中,这种无用的文件比较难找,可以通过 linkMap 文件来分析。设置 Project -> Build Settings -> Write Link Map FileYES,并设置 Path to Link Map File,build 完后找到 linkMap 文件,来分析该文件,该文件默认位于

1
~/Library/Developer/Xcode/DerivedData/XXX-eumsvrzbvgfofvbfsoqokmjprvuh/Build/Intermediates/XXX.build/Debug-iphoneos/XXX.build/

该文件的介绍,可以参考下 这里

1.无用类

方案一:
以往C++在链接时,没有被用到的类和方法是不会编进可执行文件里。但Objctive-C不同,由于它的动态性,它可以通过类名和方法名获取这个类和方法进行调用,所以编译器会把项目里所有OC源文件编进可执行文件里,哪怕该类和方法没有被使用到。

结合LinkMap文件的TEXT.text,通过正则表达式([+|-][.+\s(.+)]),我们可以提取当前可执行文件里所有objc类方法和实例方法(SelectorsAll)。再使用otool命令otool -v -s DATA objc_selrefs逆向DATA.objc_selrefs段,提取可执行文件里引用到的方法名(UsedSelectorsAll),我们可以大致分析出SelectorsAll里哪些方法是没有被引用的(SelectorsAll-UsedSelectorsAll)。注意,系统API的Protocol可能被列入无用方法名单里,如UITableViewDelegate的方法,我们只需要对这些Protocol里的方法加入白名单过滤即可。

另外第三方库的无用selector也可以这样扫出来的。

NOTE
上面这个是微信提供的方案,本人试了一下,还是有些复杂的。

方案二:
支持第三方库大小统计,在该脚本的基础上,添加了分析 objc-class-ref 段,查找未引用的类的方法,代码点击这里

1
2
//使用方法
node js脚本 linkMap.txt(linkMap的txt文件) -hl

方案三:
这里有一个脚本工具

1
python py文件目录 Xcode工程文件根目录

会将结果以.txt的方式存储在Xcode工程文件目录中.

NOTE
但是需要注意的是,由于.a以及FrameWork的存在,还有注释等客观因素存在,扫描出来的结果仅供参考,还需要仔细甄别。

2.无用方法

查找无用oc类有两种方式,一种是类似于查找无用资源,通过搜索”[ClassName alloc/new”、”ClassName *”、”[ClassName class]”等关键字在代码里是否出现。另一种是通过otool命令逆向DATA.objc_classlist段和DATA.objc_classrefs段来获取当前所有oc类和被引用的oc类,两个集合相减就是无用oc类。

方案1
使用Python脚本,通过对执行文件(我们编译wan),objc_cover

方案2
使用Swift3开发了个macOS的程序可以检测出objc项目中无用方法,然后一键全部清理

3. 扫描重复代码

可以利用第三方工具simian扫描。南非支付copy代码就是这样被发现的。但除此成果之外,扫描出来的结果过多,重构起来也不方便,不如砍功能需求效果好。

4.功能重复的第三方库

比如model层的mantle和realm,JsonKit和SBJson等功能类似的只需要一个就好,或者直接用系统自带的Json序列化工具。可以肉眼扫描也可以自己写脚本去扫描。如果使用了Cocoapods来管理第三方库的话,查询起来会更方便一些。

5.空函数及默认实现的函数都可以删掉

有些函数只是实现了一个[super function],例如didReciveMemoryWarning,或者viewDidAppear如果没做额外的处理其实都是可以删除的。

6.第三方静态库或动态库的架构

这个其实不用做的,做了之后只是减小了你提供给别人的包的大小,在主应用编译的时候会自动把静态库中不需要的架构去掉的。xcode这一点做的还是比较智能的。

6. iOS瘦身之删除无用的mach-O文件

以下内容参考iOS瘦身之删除无用的mach-O文件

  1. category是需要过滤的, 这货有点特别
  2. 删除找出来的.o文件之后, 可能会引起一些特殊的情况, 当然一般是crash, 因为有一些特别的代码他们用法并不是直接引用某个方法, 而是通过NSString相关的方法来获得Sel或者Class
  3. 把查找.o文件的操作放在本地, 而在编译器上进行编译的时候就直接执行删除, 不占用编译器的时间(我们的项目要使用六个小时以上的时间来进行查找)
  4. 建议进行操作再跑一遍回归测试, 确保各个功能模块正常

NOTE
这种方式风险较大,不建议仓促下使用。

7.统计项目中各工程在可执行文件的大小占比

项目里会引入很多第三方静态库,如果能知道这些第三方库在可执行文件里占用的大小,就可以评估是否值得去找替代方案去掉这个第三方库。我们可以从linkmap中统计出这个信息,对此写了个node.js脚本,可以通过linkmap统计每个.o目标文件占用的体积和每个.a静态库占用的体积。

第一种:
采用node + js 的方式分析,过大的可以考虑缩减和替换

1
node js文件路径 xxlinkmapxxx.txt路径 -hl

NOTE
这种方式可能需要装node。

第二种:
采用python脚本

第三种:
LinkMap解析工具,检查每个类占用大小

四、大致流程思路

NOTE
为了防止上面的脚本在访问的时候失效,特地做了备份,失效了可以访问我的git地址

参考

手机APP安装包缩减方案
iOS编译过程的原理和应用
iOS可执行文件瘦身方法
我的 App 『减肥计划』(一)
《iOS安装包瘦身指南》
iOS微信安装包瘦身
总结下在iOS瘦身上的一些方向和优化点
iPhone安装包的优化