背景
移动开发中,对于包大小优化是项目开发中需要考虑的,尤其对于航母级App,比如QQ、手淘等。网上关于包大小优化的文章很多,每篇文章说的都不尽相同,因此想根据已有的文章、知识结合自己的理解、实践,做一份梳理。整理自己的包大小优化逻辑,不光要知道怎么做可以让包大小变化,还要知道为什么这么做能产生效果。
分析
想要优化安装包大小,首先需要弄清楚影响安装包大小的因素有哪些?在做这件事之前要先思考、分析、最后再去做,针对安装包大小,首先分析影响安装包大小的因素,有:Xcode的设置、资源、代码三个方面。那针对这三个方面要如何优化?又如何查看每一步优化的结果?
首先是怎么优化的问题:
Xcode的编译设置优化,Xcode设置影响的是生成包的大小,通过Xcode编译选项优化的设置,让生成的ipa包变小,比如不含断点调试、去掉异常支持等等。
资源文件的优化,资源不光有图片资源,也包含代码资源和其它导入的资源,可以通过分析安装包构成,看里面哪些部分比较大、不合理,从而进行优化。
代码的优化,通过Link Map生成Link Map File,在编译时开启Xcode Build Settings中的Write Link Map File开关,Xcode就会生成一份Link Map File, 分析Link Map File各文件占用,结合iOS 上的可执行文件 (Mach-O文件)进行分析优化。
Link Map File 分为三部分:Object Files、Sections 和 Symbols。
· Object Files 包含了代码工程的所有文件;
· Sections 描述了代码段在生成的Mach-O里的偏移位置和大小;
· Symbols 会列出每个方法、类、block,以及它们的大小。
然后是怎么查看每一步优化的结果的问题:
查看每一步的优化结果,可以通过分析打包出来的ipa的大小以及ipa的组成与初始的ipa包大小比较,即可直观得到优化的结果。但可以更进一步分析ipa的构成,对比优化后的构成,看每一步的操作具体影响的是包的哪一块儿,从而导致包的大小发生了变化。所以先来看一下一个ipa包含哪些内容,然后每一步优化之后,对应ipa的哪一部分发生了变化。
安装包的构成
iOS打包出来的ipa本质上是一个压缩包,所以可以将.ipa的后缀改为.zip,然后进行解压缩,之后会得到一个Payload文件夹,里面又一个xxx.app的文件,这个xxx.app就是包含所有文件的包了,选中xxx.app,右键显示包内容,我们就可以直观地看到安装包中的内容了,大致如下:
了解了ipa包的组成之后,我们再回过头来,按照Xcode的编译优化、资源文件优化、代码优化的步骤来一步步分析。
Xcode编译设置
一般这一步容易被人忽略,因为提到优化最先能想到的就是资源优化,比如图片压缩、无用代码删除等等,而对于Xcode自身的编译优化提及的反而不多。而且由于网上提供的参考针对每个项目可能结果都不一样,有些编译选项的设置是需要针与实际项目结合起来才可以,所以这里整理如下:
Xcode编译优化相关:
1.Build Settings中去掉异常支持,Enable C++ Exceptions和Enable Objective-C Exceptions设置为NO,Other C Flags添加-fno-exceptions;
注意:Enable C++ Excptions和Enable Objective-C Exceptions是指项目支持对错误的异常处理,比如try catch、throw之类的;所以如果项目中使用的有类似的异常处理的,这个关闭了之后会报错(Cannot use '@try' with Objective-C exceptions disabled)。包括宏定义中使用的有try{}、@finally{}之类的,比如@strongify等,如果关闭了最后打包的时候也会报错。
-fno-exceptions的意思是禁用异常机制,参考GCC如下图,同样当项目中有try throw的时候,就不要设置这个选项为NO
2.Build Settings -> Architectures,Release下设置为arm64
Architectures指定工程被编译成可支持哪些指令集类型,支持的指令集越多,就会编译出多个指令集代码的数据包,ipa包就会变大。默认的参数standard architectures(armv7,arm64) ,打的包里面有32位、64位两份指令集。如果不需要32位的,可以在other中更改支持的指令集,从而使ipa包变小。
还有另外一种设置方法,Architectures不修改,Excluded Architectures中设置Release模式下 Any iOS SDK -> armv7,也可以实现同样的效果。设置了之后,就是Release下把armv7的指令集排除在外。选中target会发现默认设置了 Any iOS Simulator SDK -> arm64,意思是模拟器的时候排除arm64指令集。
3.Build Settings -> Generate Debug Symbols设置为NO
Generate Debug Symbols的意思是生成调试符号,当这个选项设置为YES时,每个源文件在编译成.o文件时,编译参数多了-g和-gmodule,意思是generate complete debug info,所以产生的.o文件会大,从而最终生成的可执行文件也就会变大。
注意Generate Debug Symbols设置为NO时,在Xcode中设置的断点不会中断,即不能断点调试。且最后不能生成dSYM文件,即使Debug Information Format设置了,也不能生成,因为首先要有调试信息然后才能生成dSYM文件,而设置为NO,意味着不产生调试信息,所以也就没办法生成dSYM文件。所以建议不要设置。
4.Build Settings -> Deployment Postprocessing,Debug模式下设置NO,Release下设为YES
Deployment Postprocessing是Strip配置的总开关,只有这个设置为YES之后,下面的Strip Linked Product、Strip Debug Symbols During Copy的设置才会生效。
a.Build Settings -> Strip Linked Product,Debug下设置为NO,Release下设置为YES
对最后生成的二进制文件进行strip,去除不必要的符号信息,Release下可以为YES。注意,如果Deployment Postprocessing不打开,该选项没有作用。去除了符号信息之后需要使用dSYM来进行符号化,所以需要将 Debug Information Format 修改为DWARF with dSYM file(Release下),如果在Debug下设置为DWARF with dSYM file那么在崩溃时将无法看到堆栈信息。
b.Build Settings -> Strip Debug Symbols During Copy,Debug下设置为NO,Release下设置为YES
文件拷贝编译阶段是否进行strip,设置为YES之后,会把拷贝进项目包的三方库、资源或者Extension的Debug Symbol去除。同样,如果Deployment Postprocessing不打开,该选项没有作用
c.Build Settings -> Symbols Hidden by Default,Debug模式下设置为NO,Release下设置为YES
Symbols Hidden by Default会把所有符号都定义成"private extern",移除符号信息。
5.Build Settings -> Make Strings Read-Only设置为YES
官方解释:Make Strings Read-Only (GCC_REUSE_STRINGS), Reuse string literals. 就是复用字符串字面量,提到复用,顾名思义就是减少生成不必要的,也是优化的一种形式。
6.Build Settings -> Dead Code Stripping设置为YES
消除无效代码,C/C++/Swift 等静态语言编译器会在 link 的时候移除未使用的代码,对于OC等动态语言是无效的。
7.Asset Catalog Compiler编译设置优化,Build Settings -> Asset Catalog Compiler - Options 中Optimization改为space。
这个选项可以改变actool(compile asset catalog这个过程使用的工具,内置在Xcode里的)在构建Assets.car时会按照一定策略选取编码算法,对其中的 png 图片重新编码, 从而减少包大小。Assets.xcassets 压缩格式对最终ipa包下assets.car文件大小的影响还是比较大的。但需要注意的是,cocoapods管理库中的asset catalog的编译过程在cocoapods生成的[CP] Copy Pods Resources这个脚本中,故上面的设置对Pod库组件是无效的。压缩策略有如下几种:
8.Build Settings -> Optimization Level改为-Oz
Optimization Level默认为-Os,-Oz是Xcode 11之后才出现的编译优化选项,核心原理是对重复的连续机器指令外联成函数进行复用,因此开启Oz,能减少二进制的大小,但同时会带来执行效率但额外消耗。可参考What's New in Clang and LLVM
在What's New in Clang and LLVM的Presentation Slides中,苹果给出了Optimization Level各参数优化的选择对比,如下图,对于性能要求高的,建议选择-O2和-O3,对于包大小敏感的,可选择-Os和-Oz,默认-Os是性能和大小平衡比较好的。最终选择什么,需要开发者根据自己实际项目而定。
Optimization Level各参数对比:
Xcode编译设置优化总结如下:
资源文件优化
资源文件的优化,通常来说是比较简单的,但是资源文件的优化是需要持续进行的,前面介绍的Xcode编译设置优化,配置好了之后,后续开发过程中只要不修改配置,都无需重复关注。但资源文件不同,随着项目的迭代,会不断引入新的资源文件,不断有废弃资源的产生,所以资源文件的优化是要持续进行的。
资源文件的优化分为两部分,即:无用资源的删除、已用资源的压缩。在这里建议分先后顺序,即先做删除再做压缩,因为如果先压缩了,结果发现是无用资源,就白白浪费了力气。
无用资源的删除:
· 已定义未使用的代码文件
· 已废弃业务,代码还在
· 已引用的图片但未使用
· 某些重复资源导入
已用资源的压缩:
·项目中引入图片、网页、json、音频等文件的压缩
无用资源的删除
随着项目的迭代,每个项目都会或多或少存在冗余。可能是开发了的功能未上线但产品让保留,保留着保留着就忘记了;可能是已下线的业务,没人通知到开发,于是代码逻辑一直都在;可能是删除某些业务代码时,对应的图片资源未删除;又或者是每个开发,导入各自熟悉的第三方库使用。
已定义未使用的代码
可使用AppCode进行分析,打开AppCode,待索引完成后,选择顶部菜单中的Code->Inspect Code,然后选择范围,whole Project点击OK,等待AppCode静态分析即可。静态分析完以后,可以在Unused code里看到所有的无用代码
AppCode中无用代码静态分析的类型有以下几种:
AppCode静态分析结果出来之后,删除前要经过确认,因为静态分析的结果可能会有误差,比如针对performSelector调用的方法就会被检测为没有调用。
当然也可以不利用AppCode,比如通过技术手段分析Mach-O文件 :
Mach-O文件中有__DATA.__objc_classrefs和__DATA.__objc_selrefs段,分别近似于“被使用的类的集合”和“被使用的方法的集合”。通过取差集的方式可以筛选出未被使用的类和方法。
1. 排查无用类
使用otool命令可查看__DATA.__objc_classrefs段和__DATA.__objc_classlist段,两者的差集可以认为是定义了但未使用的类。
不过__DATA.__objc_classrefs段和__DATA.__objc_classlist段中都只提供了类在二进制文件中的位置地址,而没有提供类名等可读信息。所以在获取到差集后,还需要结合下面这个命令的输出,将地址转换成可读的类名。
使用脚本筛选出差集对应的类后,还需要进行一遍人工梳理,因为动态使用的类、从nib或storyboard初始化的类以及在同一个文件中定义的多个类会被误判为未使用的类。
2. 排查无用方法
所有已经被实现的方法可以通过linkmap来获取,对linkmap做grep操作即可获得结果:
而所有已经被使用的方法可以通过对二进制文件逆向获得。使用otool工具逆向二进制文件的__DATA.__objc_selrefs 段,提取可执行文件里引用到的方法名:
使用这种方法取到的差集,还需要排除掉系统API中的protocol,accessor方法等。
已废弃业务,代码还在
需要梳理业务流程,结合线上业务数据点击量,同产品和业务确认对应功能是否下线,从而决定是否移除对应的业务模块代码。
已引入未使用图片
推荐使用工具LSUnusedResources,原理大致是遍历资源目录下后缀 ["imageset", "jpg", "png"...] 的文件,然后在源文件 ["m", "swift", "xib", "storyboard"...] 中字符串匹配,无匹配则是无用的资源文件。
使用时注意勾选Ignore similar name,然后点击右上角的Browse选中要扫描的项目地址,点击右下角的search,就会开始扫描,结果会在底部Unused Results中展示出来,然后CMD+A全选,export,导出到一个文本文件中。也可以在对应单条Item上面双击,会打开对应的文件夹。建议删除前在项目中搜索确认,是否确实没有使用(类似字符串中间替换的可能会被扫描出来,所以删除前需要确认)
某些重复资源的导入
重复资源的导入,分为两个方面,一方面是针对第三方SDK,另一方面是项目文件。
1.针对第三方SDK
项目中功能类似的SDK建议保留一个,建议分析相同功能的类库,结合实际情况,保留一个即可;另外,有些第三方类库导入时,可只导入实际使用的部分,不需全量导入,比如百度地图,定位和地图分开的,开发者根据需求来选择需要导入哪些。
2.针对项目文件
使用 fdupes 工具进行重复文件扫描,原理是:通过校验所有资源的 MD5,筛选出项目中的重复资源。这里还要说下cocoapods带来的图片重复合并问题,也属于这个范畴。现如今好多大的App都实行组件化,重度使用cocoapods进行库管理,越来越多的代码被封装成了pod库,以库的形式集成进工程中。在检查安装包内容时,看下.app文件的最外层的零散资源文件和pod库的asset catalog里的资源文件重复了。因为如果pod库在编写podspec的时候,可能用了这样的语句指定资源文件:
podspec中这样书写,会导致asset catalog中的图片,既作为asset catalog被合并到主工程的asset.car中,也会作为png被拷贝到安装包中。导致其中一套图片白白占用了安装包空间。应该以白名单的形式明确指定哪些资源文件是pod库中有效的资源文件。
已用资源的压缩
项目中引入图片、网页、json、音频等文件的压缩,网页的压缩指的是,放入App资源中的js文件,最好是经过H5端压缩后的。json文件的压缩,如果不是打开App时马上要用到的数据,可采取把对应资源放到服务端,下载后使用。音频文件的压缩,则是在可接受的范围之内,选择系统可支持的压缩比率高的格式。而最需要注意的是图片的压缩,图片的压缩,分为几个部分
1.Compress PNG Files 打包的时候自动对图片进行无损压缩,注意:这个选项对asset catalog中的资源是无效的,因为这个选项仅适用于零散资源文件。
2. Remove Text Medadata From PNG Files 移除 PNG 资源的文本字符
3.针对普通图片,可以调用tinyPNG API进行压缩
4.放入xcassets里的2x和3x图片,在上传时,会根据具体设备分开对应分辨率的图片,不会同时包含。而放入Bundle中的都会包含。所以要尽量把图片放入xcassets中。使用频率高且小的图片放到Asset.car中,Asset.car能保证加载和渲染速度最优。但是大的图片(大于100K)就不要放入Asset.car中了。大的图片可以考虑将图片转成WebP。
5. 将PNG 格式转 WebP,WebP 压缩率高,而且肉眼看不出差异,同时支持有损和无损两种压缩模式。有损压缩模式下可减少 64% 大小,无损压缩模式下可减少 19% 大小,可使用iSparta进行批量转换。注意:WebP在 CPU 消耗和解码时间上会比 PNG 高两倍,所以适合小图。
6.Pod库中的资源文件,推荐使用resource_bundles配合xcassets的方式来集成各个插件中的资源文件,即xcassets需要添加到podspec的resource_bundles中,Pod库中的代码在读取图片资源时,使用imageNamed:inBundle:compatibleWithTraitCollection:读取,而不是imageNamed:读取。但如果pod中有资源文件没有用xcassets,那这些资源文件必须放入resource_bundles中,禁止放入resource中;(resource_bundle中的资源在构建期能经过Xcode的优化,而resource中的资源不能)
这样做的优点有:
(1)各个pod管理各自的资源文件,不会有命名冲突的问题
(2)能利用苹果的app slicing功能 ,下面我会讲这个
资源文件优化总结如下:
延伸一个知识点,苹果官方的app slicing ,在以上一系列优化过程中,我们仅仅关注了内部平台构建出的安装包的包大小,而忽视了apple已经为我们提供了官方的解决方案。app slicing是iOS9增加的功能。当用户从app store上下载app时,可以只下载适用于其设备的app架构版本和所需资源(如2x和3x设备),从而减少app所占的空间。如果开发者想要使用app slicing,只需要将资源文件用Asset Catalog管理,不需要做额外的任何事情。所以一顿操作发现,苹果已经为开发者寻求到了一个较优的解决方案。
写在最后
瘦身完成之后,如何保证包大小不会再次迅速增大?就需要依赖适当的监控机制和合理的流程规范来控制。
监控机制保证实时发现问题,每次打包完成后比较包大小差异
流程规范是用于保证每个项目开发者知晓开发中注意什么,养成好的开发习惯,避免造成包大小的突然变大。如:
1.引入新的三方库时,要考虑是否已有同类型的库,是否可以自己实现,是否会造成体积增大。尽量避免Objective-C和Swift混编,优先引用相同语言类型的库;
2.新增的图片资源,关注大小,是否能代码实现,注意放入项目的位置,如果体积太大,压缩后使用;
3.废弃模块或者业务代码不要保留,及时清理。毕竟git有记录;
4. 及时关注包大小变化.
引用
1.深入探索 iOS 包体积优化
2.今日头条 iOS 安装包大小优化—— 新阶段、新实践