包体积优化
1
超过 200 MB 会默认请求用户下载许可
iOS 8 之前的版本主二进制 __TEXT 段的大小不能超过 60MB
移除无用图片资源
不适配 iPhone4 以下的机型,1x 的图片都可以移除
推荐使用工具 [LSUnusedResources]:
可以根据项目实际情况定义查找文件的正则表达式。另外建议勾选 Ignore
similar name ,避免扫描出图片组
压缩图片等资源文件
无损压缩工具: [ImageOptim]
有损压缩: 使用 [Webp] 格式的图片
webp 只有 jpeg 格式的 1/3, 可以使用 [cwebp]进行格式压缩转换
删除重复文件
通过校验所有资源的 MD5,筛选出项目中的重复资源,推荐使用 [fdupes]工具
进行重复文件扫描,fdupes 是 Linux 平台的一个开源工具,由 C 语言编写 ,文
件比较顺序是`大小对比 > 部分 MD5 签名对比 > 完整 MD5 签名对比 > 逐字
节对比`。
图片资源放入.xcassets
尽量将图片资源放入 Images.xcassets 中
最终运行设备进行 2x 和 3x 分发
高频图缓存更有效
Pod 库中的资源文件
在 Pod 库里面的 Resources 目录下新建 Asset Catalog 文件,命名为
Images.xcassets,移入所有图片文件,接着手动修改该 SDK 的 `podspec` 文
件指定使用该 Images.xcassets
s.resource_bundles = {
'xxsdk' => ['PAX/Assets/*.xcassets']
}
Pod 资源文件的引用方式分为 `resource_bundles` 和 `resources`,这里我
们使用的是 `resource_bundles`,会为为指定的资源打一个 `.bundle`,
`.bundle`包含一个 `Assets.car`,获取图片的时候要严格指定 `bundle` 的位
置,能很好的隔离各个库资源包
大资源文件url下载
通过URL下载,并显示占位图
删除未使用代码
LinkMap 结合 Mach-O:
LinkMap 的 Symbols 中会列出所有方法、类、block及它们的大小,通过获取
LinkMap 即可以获得方法和类的全集;再通过 MachOView 获得使用过的方法
和类,两者的差值就是我们要找寻的未使用代码
删除未使用类
https://github.com/yan998/FindClassUnRefs
也可以通过 Runtime 提供的 `isInitialized` 方法在运行时检测一个类是否有被
使用过
精简重复代码
多人开发的项目可能存在大量复制粘贴代码,可以通过 [PMD]扫描重复的代码片
段 :
pmd cpd --files 扫描文件目录 --minimum-tokens 70 --language
objectivec --encoding UTF-8 --format xml > repeat.xml
编译选项-修改(生产包)
Valid Architectures
不支持32位以及 iOS8 ,可去掉 armv7及之前的架构
:
Strip Link Product = YES
Deployment Postprocessing = YES
ipa会去除掉symbol符号
Generate Debug Symbols = NO
去掉断点信息和符号化的调试信息
Enable C++ Exceptions 和
Enable Objective-C Exceptions = NO
不捕获 C++ 和 OC 的异常
编译选项-保持不变
- Generate Debug Symbols
默认为 Yes,用生成dSYM文件,有助于解析崩溃信息。
- Make Strings Read-Only
默认为 Yes,复用字符串字面量。
- Dead Code Stripping
默认为 Yes,去除冗余代码。
- Optimization Level
Release 下默认为 Fastest, Smalllest[-Os],自动优化代码。
- Symbols Hidden by Default
Release 下默认为 Yes,会移除符号信息,把所有符号都定义成 private
extern。
- Strip Swift Symbols
默认为 Yes,移除 Swift 相关的符号表,运行时再从 SWIFT 标准库中获取符
号,从而减少应用体积
App Thinning
上传 App 时,配置 App Thinning 即可开启。分为三个部分: Slicing、
Bitcode、On-Demand Resources
Slicing
2x 和 3x 图会分别放入到不同的变体中,用户从 App Store 下载时只下载特定
的变体
Bitcode 开启
需要保证 App 中所有的静态库和动态库都支持,否则会编译失败
App 的编译是在 Apple 服务器中进行
Organizer 中下载对应的 dYSM文件
On-Demand Resources
可将部分资源单独下载,而不随着 App 一同下载,减少初始 App 的大小
Framework 瘦身
未开启Bitcode 时,App 内的 Framework 会包含多个指令集,我们可以手动移
除不需要的指令集
Framework 也是 Mach-O 格式文件
正常只需要 armv7s 和 arm64 指令集就足够了
移除 Framework 的未使用代码
App Extension 用动态库替代静态库
用动态库会减小二进制文件的体积,但会增加启动时间,增加了运行时载入的过
程,并且依赖外部环境,需多方面考虑。
慎重引入第三方库
不要用引入功能重复的第三库
H5本地资源优化(只保留主页面)
Resources:检查无用资源文件
图片资源:Assets.car/bundle/png/jpg 等
视频 / 音频资源:mp4/mp3 等
静态网页资源:html/css/js 等
视图资源:xib/storyboard 等
国际化资源:xxx.lproj
其他:文本 / 字体 / 证书 等
使用的大资源可以在线下载
图标优化
使用 tint color 精简单色图标;
使用图标字体(IconFont)替换单色图标;
将部分相似图标进行整合
Build Setting - Excluded Architectures
项设置排除的架构
理论上只保留 arm64 架构其实就够用了,可以去除 armv6 、 armv7 、
armv7s 三种架构
链接时优化 LTO(Link-Time Optimization)
LTO 会降低编译链接的速度,所以建议在打正式包时开启
参数 LLVM_LTO=YES_THIN
包体积-二
完整总结
App Store
安装包大小
路径是App Store Connect->TestFlight->交付版本->构建版本元数据->App
Store文件大小
包大小
构建ipa
上传的ipa包
多个架构的产物,多套尺寸的图片资源
包含:
静态库二进制文件(arm64、armv7)、动态库(arm64、armv7)、
asset.car(@2x、@3x)、其他资源文件
下载的ipa包
上传ipa包进行裁剪和二次分发,拆分架构,包变小
包含:
静态库二进制文件(单架构)、动态库(单架构)、asset.car(单尺寸)、其他资
源文件
拆分单架构
使用lipo工具拆分单架构
lipo "originalExecutable" -thin arm64 -output "arm64Executable"
拆分asset
使用assetutil工具拆分asset
xcrun --sdk iphoneos assetutil --scale 3 --output "$targetFolder/
Assets3.car" "$sourceFolder/Assets.car"
ipa组成
iOS工程结构
iOS工程由壳工程和Pod模块组成
壳工程
主Target、Apple插件Target
Pod模块
源代码文件(OC、C、C++的.h和.m)、nib、bunlde、xcassets、多语言文
件、各种配置文件(plist、json)
ipa包
iOS上传到App Store是IPA包,IPA包解压后是一个文件夹,内部由各种类型的文
件构成
主要包括:
MachO可执行文件、.framework(动态库)、Assets.car、.appex(Apple插
件)、.strings(多语言)、.bundle、nib、json、png, 音频、视频 ...。
根据这些类别,分别分析其是否有优化空间
构建ipa包
主要是:
编译和文件拷贝
1.静态库里的源代码会被编译为MachO可执行文件,
2.xcassets文件夹会被转化为Assets.car,
3.其他都可以简单理解为文件拷贝。
分析Pod模块大小
静态库
1. 先解析linkmap数据,计算出Pod模块代码大小,
2. 再解析Pods-targetName-resource.sh的资源拷贝代码,计算出拷贝到Pod
模块的资源大小
动态库
1. 先使用lipo拆分动态库的二进制文件,计算出单架构的代码大小,
2. 然后再计算动态库framework内的资源文件,得到动态库的资源文件大小。
检测
无用代码检测
原理:
计算运行时Objc类覆盖率(系统自带覆盖率检测)
删除:
验证过后,可以下线掉无用的模块或代码文件
覆盖率:
APP运行时,某1个Pod模块被加载的类数量除以所有类数量,可以称为这个模块
的Objc类覆盖率
核心技术是判断一个类是否被加载过:
类第一次被使用时会调用:
+initialize方法
类被加载过后:
cls->isInitialized会返回True
源码:
// objc-class.mm
Class class_initialize(Class cls, id inst) {
if (!cls->isInitialized()) {
initializeNonMetaClass (_class_getNonMetaClass(cls, inst));
}
return cls;
}
// isInitialized方法读取了metaClass的data变量里的flags,如果flags里的第
29位为1,则返回True
// objc-runtime.h
#define RW_INITIALIZED (1<<29)
bool isInitialized() {
return getMeta()->data()->flags & RW_INITIALIZED;
}
未使用图片资源
技术原理:
1.先解析出所有拷贝到构建产物的资源文件,
2.再解析出代码中实际引用到的资源文件,
3.两者的差集就是无用资源
获取全量资源
在Cocoapos工程中,“Pods-targetName-resource.sh”脚本负责拷贝Pod里的
文件资源到构建产物,包括所有文件类型bundle、xcassets、json、png。
解析上面脚本可以得到每个Pod模块都拷贝了哪些图片资源:
// Pods-targetName-resource.sh
install_resource "${PODS_ROOT}/APodName/APodName.framework/
APodName.bundle"
install_resource "${PODS_ROOT}/BPodName/BPodName.framework/
BPodName.xcassets"
install_resource "${PODS_ROOT}/BPodName/BPodName.framework/
xxx.png"
实际引用资源
位置:
OC代码中引用资源文件都是以【字符串】字面量的形式声明,
构建后存放在Mach-O文件"__cstring" section。
计算:
1.利用strings解析framework的二进制文件就可以得到代码中所有的字符串声
明,
2.然后基于代码的各种引用方式匹
配“xxx”,"xxx@2x.png","xxx.png","xxx.bundle/xxx.png"
导出:
strings executable | grep 'xxx' > cstrings.txt
未引用类-
编译时
️ :不能检测动态创建的类
原理:
iOS编译的产物是Mach-o格式的,分析.o文件检测。
1. 文件里__DATA __objc_classlist段记录了所有类的地址
2.文件里 __DATA __objc_classrefs 段记录了所有引用过的类的地址,
3.两者Differ可以得到未被引用类的地址。
4.然后将地址符号化,就可以得到未被引用类信息。
otool命令
otool -v -s __DATA __objc_classrefs xxxMainClient #读取__DATA
Segment中section为__objc_classrefs的符号
otool -v -s __DATA __objc_classlist xxxMainClient #读取__DATA
Segment中section为__objc_classlist的符号
nm -nm xxxMainClient
扫描未使用类的开源工具:
https://github.com/xuezhulian/classunref
瘦身模块
ROI大全
瘦身 ROI 【投资回报率】高低:由上至下,左至右
总结:
组件治理:
0代码覆盖率
无用组件
重复功能组件
资源治理:
大资源
有损压缩
无用资源
重复图标
iconfont
多语言文案
编译优化:
精简编译产物oz
C++导出必要符号
删除未引用的c/c++/Swift 代码
【无OC】
Asset Catalog Compiler
Symbols Hidden by Default
LTO:优化跨模块调用代码
剥离符号表 Strip Linked Product
代码治理:
运行时未加载类
编译时无调用类
业务重构
运行时下载:
资源ODR
多语言文案
动态库(iOS暂不支持)
瘦身收益
优化编译逻辑
非侵入(也存在设置编译选项导致适配问题)
风险和成本比较低
收益 中等
无用模块
无用业务
确认是下线的模块功能
风险和成本比较低
收益 高
无用代码
无用方法
无用类
风险&成本 高
无用类工具检测
收益 低
无用资源
风险 低
成本 中,使用检测工具
收益 高
下面详细描述细分收益内容:
1.组件瘦身
组件瘦身的ROI:类加载率0%的组件 > 无用功能组件 > 重复功能组件
类加载率0%的组件:
完全未使用的模块,下架前需要和产品确认业务,避免因低频或者AB测原因导致
统计错误
无用功能组件
功能代码有引用,但实际上并无业务使用,
或者是AB测已经下线的逻辑
或者是已经重构了的业务,不再使用了
重复功能组件
不同业务重复导入相同文件,比如图片选择器、缓存库、UI组件
2.资源瘦身
资源瘦身ROI:
大资源>有损压缩>重复资源>iconfont>多语言文案瘦身>无用资源
大资源
音视频资源压缩,收益高
可以的话,本地资源可以使用http下载缓存
有损压缩
图片等压缩
无用资源
检测无用资源并清理
重复图标
很多图片是相同的,只是名称不一样,这个可以使用hash值统计
类似的图片可以进行合并
ODR
On-Demand Resource 表示按需下载资源
分三种:
Initial install tags.
此种标签的资源,会随着App从App Store下载而下载,但是会影响App的ipa大
小,也就是说此种资源会包含在ipa内。
Prefetch tag order.
此种标签会在App下载后,开始下载相应的资源,下载是存在顺序的,后面会说
明。此种资源并不会影响ipa的大小,也就是说此种资源并不包含在ipa内。
Dowloaded only on demand.
此种标签下的资源,会在必要的时候,主动触发下载,这是我们开发者自己控制
下载时机的。
iconfont
iconfont支持缩放、修改颜色,它size小,适合用于箭头、占位图等图标场景
减少包大小也能提高开发视觉体验的统一性
多语言文案
如果App支持20个语种,每个语种4000多条文案,文案总大小6MB,优化空间
大
3.编译优化
编译优化ROI:
Optimization Level > 动态库复用主二进制静态库> 链接器产物压缩
精简编译产物Oz
Optimization Level多个级别,-Oz比-O3的编译产物体积小10%左右。设置-
Oz以后,XCode会优化连续的汇编指令,从而减少二进制大小,但副作用是执行
速度会变慢。C++工程建议都开启
LTO(OC/C++)
LTO 是在LLVM链接时,优化跨模块调用代码
编译时间可能增加几倍,甚至几十倍,建议只在Release包开启
优点:
●将一些函数內联化
●去除了一些无用代码
●对程序有全局的优化作用
缺点:
●降低编译链接速度,只建议在打正式包时开启
●降低 link map 可读性(出现XX-lto.thin的类)
动态库复用
二进制静态库
C++动态库经常用到一些基础库比如openssl、libyuv、libcurl,他们一般是静
态库。如果动态库引用了静态库,它编译时默认会内嵌静态库的所有符号。
一些基础静态库 甚至被多个动态库引用,导致重复引入
最佳解决方案是共享符号表,让动态库可以调用主二进制的基础库符号,从而可
以去掉内置的静态库。只要修改XCode的Link配置,无需额外的代码开发
动态库工程设置:
1、设置当遇到未定义的函数时,动态查找APP主二进制符号表。
2、关闭bitcode
3导出动态库需要调用的外部符号,写到一个文件exported_symbols内
链接器产物压缩
iOS工程构建产物是MachO文件,MachO文件中的TEXT段存放了各种只读的数
据段,__cstring段存放了普通的C String,__objc_methtype和
__objc_methname存放了Objc的方法签名和方法名。比入Objc代码中声明的
@"Hello world",底层会产生一个CFString,构建后存放在__cstring中。这些数
据很占空间,一般工程至少会有10MB以上,压缩的收益很可观。我们上线后,
App Store安装包大小从191MB优化到174MB,减少了16MB
技术原理:
链接时将TEXT段数据移到__DATA段并压缩,运行时先执行解压代码,解压
TEXT段数据存到自定义段中,将代码中对字符串的引用的地址修正为解压后的自
定义段。
剥离符号表:
Strip Linked Product
Debug环境不要设置YES,否则调试时看不到符号
主工程Release
Deployment Postprocessing :YES
Strip Linked Product :YES
Strip Style :All Symbols(剥离所有符号表和重定向信息)
Framework工程
Deployment Postprocessing :YES
Strip Linked Product :YES
Strip Style :Non-Global Symbols(剥离包括调试信息等非全局的符号,保留外
部符号)
说明:
1、静态库不能将Strip Style 设置为All Symbols,因为剥离了所有符号的静态库
是无法被正常链接的
2、去除符号不影响 dSYM 文件中的符号信息,查看崩溃日志时,可以从 dSYM
文件中找对应符号
Symbols Hidden by Default
主工程Release
Symbols Hidden by Default :Yes
Framework工程 静态库/动态库
Symbols Hidden by Default :NO
库设置为NO,否则会有链接错误
剔除未引用的C/C++/Swift代码:
Dead Code Stripping
此设置对 对动态语言OC无效。
主工程开启设置:
Dead Code Stripping :Yes
Asset Catalog Compiler
主工程设置:
Asset Catalog Compiler->Optimization设置为space
Optimization有三个选项,空、time和Space,选择Space可以优化包大小
C++只导出必要符号:
Symbol Visibility
C++的静态库和动态库都只导出必须的符号,默认设置为隐藏所有符号,然后用
Visibility Attributes单独控制需要导出的符号
默认隐藏所有C++符号
Other C++ Flags->添加-fvisibility=hidden
设置需要导出的符号
__attribute__((visibility("default"))) void MyFunction1() {}
__attribute__((visibility("default"))) void MyFunction2() {}
....
4.代码下线
根据Objc类覆盖率统计的结果,可以逐步下线掉未被使用的类。代码文件的量级
比较小,下线代码需要仔细确认,避免引起功能问题或crash,ROI比较低。
5.Flutter专项
Flutter是单独构建的,引入的内容包括Flutter引擎产物、Flutter业务代码产物。
APP如果引入Flutter,需要对Flutter进行专项瘦身。
精简编译产物Oz:
Optimization Level
-Oz选项相比Os,收益预估11%,但首屏性能1%~9%的损耗
Dart符号剔除和混淆
可以通过--dwarf-stack-trace选项去除Dart标准的调试符号。通过--
obfuscate 选项,可以将较长的符号替换为短符号,副作用是符号会被混淆
Flutter icon摇树优化
--tree-shake-icons
去除NOTICES文件
防劣化机制
review 代码需要一套机制确保包不要出现过度增长,过大对资源需要审核必要性