记录一次安装包大小优化的实践。
维持安装包体积是一个持续的过程,建立预警机制,监控每个版本的体积大小。
资源文件优化
图片资源优化
- 推荐通过FengNiao清理无用的图片资源 onevcat/FengNiao
- swift开发,支持帧动画图片匹配,如image_%d
- 命令行工具,可以给 Xcode 添加 Run Script,在每次构建的时候自动检测/清理未使用的资源
FengNiao 的基本原理是查找出项目中所有使用到的字符串和项目中所有的资源文件,两者进行匹配(完全匹配和模式匹配,模式匹配支持带数字资源的前缀/中缀/后缀匹配),计算差集就为未使用的资源。
- 推荐通过ImageOptim进行图片压缩 ImageOptim
- 压缩项目中的JPG格式图片能明显减少安装包大小
- 压缩项目中Assets内的PNG格式图片无明显作用,对于大图甚至产生负优化
Xcode会自动压缩Assets内的PNG格式图片,但是Bundle内的图片资源仍需手动压缩。
对于三方库内的Bundle内的图片建议放置于Assets中。
- 大图优化
- 切分大图,将图片背景、文案改为代码实现
- 动效资源图片采用雪碧图的方式存储
- 部分无透明度且无法拆分的背景大图,采用JPG格式并有损压缩至设计师可以接受的范围内
- 图标优化
- 通过修改tintcolor复用单色重复图标
- 通过旋转复用图标
- 推荐使用On-Demand Resources On-Demand Resources Guide
将 App 中的无需立即用到资源放在 App Store 云端上,然后你需要把资源标记为不同的Tag,需要的时候才去下载相应Tag的图片。如游戏中的不同关卡资源。
- 建议将webp作为项目图片格式
- 压缩率高。支持有损和无损2种方式,比如将 Gif 图可以转换为 Animated WebP,有损模式下可以减小 64%,无损模式下可以减小 19%
- 支持 Alpha 透明和 24-bit 颜色数,不会像 PNG8 那样因为色彩不够出现毛边
- CUP 消耗和解码时间上会比 PNG 高2倍
视频/音频资源远端化
建议将视频与音频文件放置在服务器,客户端按需下载或者使用流播放。
HTML5远端化
建议将HTML5资源放置在服务器,客户端可以使用离线缓存的方式来缓存网页资源到本地。
代码优化
- 扫描未使用代码
- 基于 Clang 扫描 如何使用 Clang Plugin 找到项目中的无用代码
基本思路是基于 clang AST,追溯到函数的调用层级,记录所有定义的方法/类和所有调用的方法/类,再取差集。
- 基于可执行文件扫描 iOS微信安装包瘦身
基本思路是Mach-O 文件中的 (__DATA,__objc_classlist) 段表示所有定义的类, (__DATA.__objc_classrefs) 段表示所有引用的类(继承关系是在 __DATA.__objc_superrefs 中);使用的方法和引用的方法也是类似原理。因此我们使用 otool 等命令逆向可执行文件中引用到的类/方法和所有定义的类/方法,然后计算差集。
- 基于源码扫描 fui
基本思路是对源码文件进行字符串匹配。例如将 A *a、[A xxx]、NSStringFromClass("A")、objc_getClass("A") 等归类为使用的类,@interface A : B 归类为定义的类,然后计算差集。
- 通过 AppCode 查找无用代码 AppCode
查找出 AppCode 中无用的类、无用的方法甚至是无用的 import ,但是无法扫描通过字符串拼接方式来创建的类和调用的方法。
- Cocoapods 中的优化选项配置
Cocoapods 的 project 文件在每次 pod install 或者 pod update 会重置,所以需要 hook pod install 来设置 Pods 中每个 Target 的编译选项:
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['ENABLE_BITCODE'] = 'YES'
config.build_settings['STRIP_INSTALLED_PRODUCT'] = 'YES'
config.build_settings['SWIFT_COMPILATION_MODE'] = 'wholemodule'
config.build_settings['SWIFT_VERSION'] = '5.0'
if config.name == 'Debug'
config.build_settings['SWIFT_OPTIMIZATION_LEVEL'] = '-Onone'
config.build_settings['GCC_OPTIMIZATION_LEVEL'] = '0'
config.build_settings['COPY_PHASE_STRIP'] = 'NO'
config.build_settings['DEPLOYMENT_POSTPROCESSING'] = 'NO'
config.build_settings['GCC_GENERATE_DEBUGGING_SYMBOLS'] = 'YES'
else
config.build_settings['SWIFT_OPTIMIZATION_LEVEL'] = '-Osize'
config.build_settings['GCC_OPTIMIZATION_LEVEL'] = 's'
config.build_settings['COPY_PHASE_STRIP'] = 'YES'
config.build_settings['DEPLOYMENT_POSTPROCESSING'] = 'YES'
config.build_settings['GCC_GENERATE_DEBUGGING_SYMBOLS'] = 'NO'
end
end
end
end
- 静态库瘦身
发布版本中删除 i386、x86_64 是模拟器的指令集,只保留 armv7 和 arm64
- 静态库指令集信息查看:lipo -info libname.a(或者libname.framework/libname)
- 静态库拆分:lipo 静态库文件路径 -thin CPU架构 -output 拆分后的静态库文件路径
- 静态库合并:lipo -create 静态库1文件路径 静态库2文件路径... 静态库n文件路径 -output 合并后的静态库文件径
4.iOS的指令集
- 如果项目可以仅支持iphone5s及以上设备,去除armv7/armv7s支持
arm64:iPhone6s | iphone6s plus|iPhone6| iPhone6 plus|iPhone5S | iPad Air| iPad mini2(iPad mini with Retina Display)
armv7s:iPhone5|iPhone5C|iPad4(iPad with Retina Display)
armv7:iPhone4|iPhone4S|iPad|iPad2|iPad3(The New iPad)|iPad mini|iPod Touch 3G|iPod Touch4
i386是针对intel通用微处理器32位处理器
x86_64是针对x86架构的64位处理器
模拟器32位处理器测试需要i386架构,
模拟器64位处理器测试需要x86_64架构,
真机32位处理器需要armv7,或者armv7s架构,
真机64位处理器需要arm64架构。
编译优化
- Clang/LLVM 编译器优化选项
Xcode ➡️ Build Setting ➡️Apple Clang - Code Generation ➡️ Optimization Level 设置为Fastest Smallest[-Os]
- None[-O0]
Debug 默认级别,不进行任何优化,直接将源代码编译到执行文件中,结果不进行任何重排,编译时比较长。主要用于调试程序,可以进行设置断点、改变变量 、计算表达式等调试工作。 - Fast[-O,O1]
最常用的优化级别,不考虑速度和文件大小权衡问题。与-O0级别相比,它生成的文件更小,可执行的速度更快,编译时间更少。 - Faster[-O2]
在-O1级别基础上再进行优化,增加指令调度的优化。与-O1级别相,它生成的文件大小没有变大,编译时间变长了,编译期间占用的内存更多了,但程序的运行速度有所提高。 - Fastest[-O3]
在-O2和-O1级别上进行优化,该级别可能会提高程序的运行速度,但是也会增加文件的大小。 - Fastest Smallest[-Os]
Release 默认级别。这种级别用于在有限的内存和磁盘空间下生成尽可能小的文件。由于使用了很好的缓存技术,它在某些情况下也会有很快的运行速度。 - Fastest, Aggressive Optimization[-Ofast]
它是一种更为激进的编译参数, 它以点浮点数的精度为代价。
- Swift Compiler - Code Generation
Xcode -> Build Setting ->Swift Compiler - Code Generation -> Optimization Level 设置为Optimize for Size[-Osize]
- No optimization[-Onone]:不进行优化,能保证较快的编译速度。
- Optimize for Speed[-O]:编译器将会对代码的执行效率进行优化,一定程度上会增加包大小。
- Optimize for Size[-Osize]:编译器会尽可能减少包的大小并且最小限度影响代码的执行效率。
- Deployment
Xcode ➡️ Build Setting ➡️ Deployment
- Strip Linked Product & Deployment Postprocessing
Strip Linked Product设置为YES,Deployment Postprocessing设置为NO
在 Archive 的时候 Xcode 总是会把 Deployment Postprocessing 设置为 YES 。所以我们可以打开 Strip Linked Product 并且把 Deployment Postprocessing 设置为 NO,而不用担心调试的时候会影响断点和符号化,同时打包的时候又会自动去除符号信息。
- Strip Linked Product/Strip Debug Symbols During
仅在Release环境设置为YES
与 Strip Linked Product 类似,但是这个是将那些拷贝进项目包的三方库、资源或者 Extension 的 Debug Symbol 去除掉,同样也是使用的 strip 命令。这个选项没有前置条件,所以我们只需要在 Release 模式下开启,不然就不能对三方库进行断点调试和符号化了
去掉不必要的符号信息,可以减少可执行文件大小,但去除了符号信息之后我们就只能使用 dSYM 来进行符号化。
- Link-Time Optimization
Xcode ➡️ Build Setting ➡️Apple Clang - Code Generation ➡️Link-Time Optimization设置为Incremental
Link-Time Optimization 是 LLVM 编译器的一个特性,用于在 link 中间代码时,对全局代码进行优化。这个优化是自动完成的,因此不需要修改现有的代码;这个优化也是高效的,因为可以在全局视角下优化代码。
开启这个优化后,一方面减少了汇编代码的体积,一方面提高了代码的运行效率.
- 多余代码去除(Dead code elimination):如果一段代码分布在多个文件中,但是从来没有被使用,普通的 -O3 优化方法不能发现跨中间代码文件的多余代码,因此是一个“局部优化”。但是Link-Time Optimization 技术可以在 link 时发现跨中间代码文件的多余代码。
- 跨过程优化(Interprocedural analysis and optimization):这是一个相对广泛的概念。举个例子来说,如果一个 if 方法的某个分支永不可能执行,那么在最后生成的二进制文件中就不应该有这个分支的代码。
- 内联优化(Inlining optimization):内联优化形象来说,就是在汇编中不使用 “call func_name” 语句,直接将外部方法内的语句“复制”到调用者的代码段内。这样做的好处是不用进行调用函数前的压栈、调用函数后的出栈操作,提高运行效率与栈空间利用率
- Asset Catalog Compiler
Xcode ➡️ Build Setting ➡️Asset Catalog Compiler ➡️Optimization设置为space