1. 前⾔
随着需求的增加,App 的安装包的⼤⼩通常会不断上涨。之前我做的一个 App ipa 的⼤⼩达到了300 MB。解压后的包⼤⼩达到了400 MB。
其中,主 App 的可执⾏⽂件占 200 MB。第三⽅ framework 的可执⾏⽂件占 100 MB。App 本身的资源⽂件占 30 MB。第三方的资源⽂件占 20 MB。App 的 Extension 占 50 MB。
IPA | App Bundle | Main / Frameworks Executable | Assets.car | PlugIns |
---|---|---|---|---|
300 M | 400 M | 200 M / 100 M | 30 M + 20 M (三方 SDK) | 50M |
将编译后的 App 上传到 Test Flight 后,可以查看 app 在各个设备上的预估安装⼤⼩。⽤户在 AppStore 上看到的⼤⼩就是 Test Flight 上对应设备的 Install Size。
如果包太⼤,可能会让⼀些⼿机容量较⼩的⽤户对我们的App望⽽却步。所以减⼩包⼤⼩成为了刻不容缓的任务。
⾸先看⼀下App⾥主要包含了哪些内容:
Exectutable: 编译后的可执⾏⽂件
Resources:图⽚、⾳频、视频等资源⽂件
Framework:使⽤到的动态库
Pulgins:App的Extensions
Framework中主要是第三⽅的库,我们能做的事情不多。我们可以从 Exectutable、Resources、Pulgins 这三块⼊⼿,进⾏App的瘦身
2. 可执⾏⽂件优化
1. 优化编译选项
由于⼀些历史原因,之前的 App 出 release 包的时候,保留了symbols。这样在外部⽤户使⽤App过程中如果出现crash,我们是可以 直接通过代码输出符号化的crash的堆栈信息。
常⻅的获取堆栈信息有这三种⽅式:
通过NSThread的callStackSymbols⽅法
[NSThread callStackSymbols]
通过backtrace_symbols函数
void* callstack[128];
int frames = backtrace(callstack, 128);
char **strs = backtrace_symbols(callstack, frames);
借助第三⽅库BSBacktraceLogger
BSBacktraceLogger.bs_backtraceOfMainThread()
要剔除 symbol 也很简单:在 Project 的 Build Settings 中,修改编译选项。然后让每个 Target 继承 Project 的选项,就可以达到⽬的。先上结论:
选项 | 修改前 | 修改后 |
---|---|---|
Strip Linked Product | main app: NO extension: YES | main app: YES extension: YES |
Strip Style | main app: Debugging Symbols | main app: All Symbols |
下⾯介绍⼀下三个选项的含义。
1. deployment postprocessing
If enabled, indicates that binaries should be stripped and file mode, owner, and group information should be set to standard values.
这个选项是部署的总开关。默认是No,我们维持默认值即可。在debug阶段,Xcode会帮我们保留debug symbol等各种调试信息。当我们使⽤Archive打包的时候,Xcode会⾃动将deployment postprocessing设置为Yes。
2. Strip Linked Product
If enabled, the linked product of the build will be stripped of symbols when performing deployment postprocessing.
这个选项需要在deployment postprocessing为Yes时才会⽣效。它主要⽤来控制是否要把⾮必要的符号信息剔除掉。Xcode默认值是Yes,我们不⽤担⼼在debug时symbols会被剔除。因为在debug阶段deployment postprocessing默认是No,所以Strip Linked Product选项将被忽略。
在我之前 App 的版本中,Strip Linked Product值为No。也正是这样,导致了release包中包含的debug symbols。这让App增⼤了⼏⼗MB。
因此我们需要针对Appstore的包,将其改为Yes。
3. Strip Style
The level of symbol stripping to be performed on the linked product of the build. The default value is defined by the targetʼs product type.
Strip Style⽤来控制需要去除的符号的类型:
All Symbols: 剔除所有符号。
Non-Global Symbols: 剔除⾮全局的 Symbol,保留全局符号。建议在静态库、动态库、Extension中使⽤这个选项。
Debug Symbols: 剔除调试符号,将导致⽆法断点调试。
Xcode默认是All Symbols。在我之前App中,main app是Debug Symbols。我们需要将main App的选项改为All Symbols。
2. 堆栈符号化
有的同学可能会担⼼开启Strip Linked Product,并将Strip Style改为All Symbols后,外部⽤户如果出现crash怎么办。其实Xcode在打包的时候,会⽣成dSYM⽂件,这个⽂件⾥⾯包含了App的符号信息。所以我们依然可以通过dSYM来还原我们的堆栈符号信息。
但是如果你有在代码中,通过诸如 [NSThread callStackSymbols] 等⽅式获取堆栈信息,那拿到的将是不含符号信息的堆栈。需要我们通过atos⼯具做进⼀步的解析。
这⾥简单介绍⼀下如何⼿动解析:
- 获取dSYM⽂件
我们需要拿到dSYM⽂件,⽐如到jenkins下载对应的dSYM⽂件。
- 获取堆栈信息
从app中获取类似下⾯这种格式的堆栈信息
0 Demo 0x0000000104ff891c Demo + 18716
- 计算load address
堆栈信息中的0x0000000104ff891c是⼗六进制的真实内存地址。18716是⼗进制的偏移地址。这⾥我们需要额外计算load address。计算⽅式是:真实地址-偏移地址
0x0000000104ff891c - 0x491c(18716的⼗六进制) = 0x104FF4000.
所以这⾥我们得到的load address是0x104FF4000。
- 打开终端,使⽤atos解析
atos -arch <arch> -o <path_to_dsym> -l <load_address> <stack_address_in_crash_report>
在这个例⼦⾥,我们使⽤以下指令:
atos -arch arm64 -o Demo.app.dSYM/Contents/Resources/DWARF/Demo -l 0x104FF4000 0x0000000104ff891c
⼀顿操作后,我们拿到了最终符号化后的堆栈信息
ViewController.test() (in Demo) (ViewController.swift:30)
这⾥需要⼀提的是,⼿动计算load address可能出现不准确的情况。所以在我们App的⽇志⾥,已经加⼊了load address信息,类似这样:
Load address: 4330029056
3. 扫描并删除⽆⽤的代码
如果项⽬是使⽤Objective-C开发,可以使⽤AppCode IDE扫描没⽤的类。但是AppCode不⽀持扫描Swift的没⽤的类。感谢开源社区,我们还有另外⼀个开源的⼯具fus,可以很⽅便的扫描Swfit中没⽤的类。
安装fus:
gem install fus
查看帮助
fus help
查找当前⽬录下⽆⽤的类
fus find
最后根据输出⼿动清理代码即可。我在实际操作过程中,发现fus存在误报的情况。项⽬中明明有在⽤的类,fus也会认为是⽆⽤的。不过所幸误报的数量较少,我们清理代码前逐⼀确认⼀遍即可。
4. 分析LinkMap
⼀个⼤型的项⽬,只是代码段就有可能超过100M。这时候检查到底是哪个类、哪个第三⽅库占⽤了太多空间,就显得尤为重要。
我之前写过⼀个LinkMap的分析⼯具:https://github.com/huanxsd/LinkMap。⽤来分析项⽬的LinkMap⽂件,得出每个类或者库所占⽤的空间⼤⼩(代码段+数据段),⽅便定位需要优化的类或静态库。
在我们这个项⽬中,LinkMap解析出来的结果是c++的代码占了⼤部分空间。这样以后就可以针对性的对这部分代码进⾏优化。
[图片上传失败...(image-20bddf-1723110030447)]
经过以上⼏种⽅法处理后,可执⾏⽂件从200 MB减⼩到了 110 MB。
3. 资源⽂件优化
1. 删除⽆⽤的资源⽂件
这部分其实⽹上已经很多教程了,主要就是靠⼯具来查找所有使⽤到的字符串和所有的资源⽂件。两者进⾏匹配。流⾏的有这两个
⼯具:
分别⽤这两个⼯具进⾏查找,结果基本⼀样。需要注意的是,如果代码中的图⽚名称是通过动态拼接的,那有可能造成匹配失败,出现误报的情况。所以在使⽤⼯具搜索完,还是需要仔细的确认⼀遍是否真的没有使⽤,再清理图⽚。
2. 压缩图⽚⼤⼩
压缩图⽚分两种,⼀种是⽆损压缩,⼀种是有损压缩。在Jerold的提示下,我使⽤Pngyu先进⾏了有损压缩,再⽤ImageOptim进⾏⽆损压缩。
3. 删除Localizable.strings中没有使⽤的字符串
我们项⽬中的多语⾔⽂件是通过python脚本,通过扫描项⽬代码,提取出来的。但是之前的python脚本有可以优化的空间。⽐如⼀ 些代码⾥不再使⽤的多语⾔字符串⼀直保留在Localizable.strings中。我们可以通过优化python脚本,可以删除这些字符串。具体的 实现可以看项⽬中的cleanUpUnusedStrings.py⽂件。
经过上⾯三个⽅法处理后。App包中的资源⽂件⼤⼩从原来的30 MB降到了20MB。
4. 针对部分特别⼤的png图⽚,使⽤ jpg 或者 webp 格式代替
在调研过程中,发现⼀个问题,有时候我们⼯程中的图⽚很⼩,但是ipa中的图⽚却变⼤了⾮常多。⽐如有⼀张图⽚⼯程中是325KB。但是编译后,在ipa中变成了1.3MB。⾜⾜⽐原来⼤了4倍。这是因为 Xcode在打包的时候,会把png图⽚转换成更适合iphone的图⽚,提⾼iphone加载图⽚的速度。代价就是图⽚可能会增⼤。在这篇⽂章中有做说明Xcode's built-in (de)optimization
Xcode for iOS by default converts all PNG images to a non-standard iOS-specific PNG derivative. This format saves iOS devices a trivial conversion step during loading, because it uses premultiplied BGRA instead of RGBA color space. It doesn't affect rendering speed at all.
Xcode's conversion is applied even when it makes files bigger. It will undo ImageOptim's savings.
如果是⼀些不常⽤的图⽚,其实是没必要为了时间⽽牺牲空间的。所以针对那些图⽚很⼤,使⽤频率很低的图⽚,可以考虑转成 jpg 或者 webp格式。这样就可以避免图⽚打包后增⼤的问题。
4. Extension优化
1. 使⽤独⽴的Localizable.strings⽂件
在我之前 App 版本中,有3个Extension直接使⽤了main app的Localizable.strings。由于Extension的资源是各⾃独⽴的,所以会导致打包后,每个Extension都有⼀份完整的Localizable.strings。⼀份完整的多语⾔⽂件有12MB。多出3份多语⾔⽂件相当于App增⼤了3*12=36MB。
解决⽅案是Extension不再使⽤main app的Localizable.strings,每个Extension增加⾃⼰的Localizable.strings⽂件。然后编写python脚本。在导⼊多语⾔时,扫描Extension的代码,拷⻉每个Extension中真正⽤到的字符串。
2. 剔除多余的三⽅库
Extension的代码是完全独⽴的,不能和main app共享。所以Extension在引⼊第三⽅依赖的时候要格外注意。
在我之前App中,有⼀个Extension依赖了完整的ZoomSDK。但是实际上只需要使⽤ZoomSDK⾥的MobileRTCScreenShare.framework。
我们内部有对ZoomSDK做简单的封装,所以通过修改zoomSDK.podspec。增加两个 subspec:
MobileRTC: 供main app使⽤的framework
MobileRTCScreenShare:供Broadcast Extension使⽤的framework
通过以上两个⽅法,Extension从原来的60MB减⼩到了6MB。
5. 其他优化
1. 重新编译FFmpeg库
我们项⽬中有⽤到FFmpeg⾥的⼀个视频格式转换功能,把3gp⽂件转成mp4⽂件。在之前的版本,项⽬中引⼊了完整的ffmpeg库。a⽂件有154.8MB。由于ffmpeg是⼀个强⼤的库,有⾮常多功能。⽽我们仅仅只需要3gp⽂件转mp4⽂件功能,所以可以通过 对ffmpeg重新编译,剔除掉多余的功能,达到减⼩包⼤⼩的⽬的。
有⼀位⼤佬对ffmpeg做了封装:ffmpeg-kit。通过这个库,我们可以很⽅便的在iOS项⽬中使⽤ffmpeg命令⾏。
所以我们可以在ffmpeg-kit的基础上,重新编译FFmpeg库:
⾸先,修改ffmpeg的编译选项,禁⽤不需要的功能。打开ffmpeg-kit⾥的scripts/apple/ffmpeg.sh,在./configure的后⾯增加以下配置:
--disable-ffplay \
--disable-ffprobe \
--disable-network \
--disable-hwaccels \
--disable-bsfs \
--disable-indevs \
--disable-outdevs \
--disable-devices \
--disable-parsers \
--disable-protocols \
--enable-protocol=file \
--disable-encoders \
--enable-encoder=mpeg4,aac* \
--disable-muxers \
然后执⾏ios.sh脚本:
./ios.sh --disable-armv7 --disable-armv7s --disable-i386 --disable-x86-64-mac-catalyst --disable-arm64-mac-catalyst -- disable-arm64e -x
这⾥我们通过--disable-xxx禁⽤不需要的架构,-x 表示⽣成xcframework。最后在prebuild⽬录下,可以找到ffmpeg-kit-min-4.4- ios-xcframework,就是我们需要的库。
重新编译后的库⽂件从原来的154.8MB减⼩到90MB。App的Install Size减⼩了3MB。
结束
以上便是优化的全部⽅法。最终效果也⽐较明显。以iPhone 12为例,App的Install Size从390 MB,降到了现在230 MB。⼀共减⼩了160 MB。
Universal Download Size | Universal Install Size | iPhone12 Download Size | iPhone 12 Install Size | |
---|---|---|---|---|
优化前 | 260 MB | 430 MB | 140 MB | 390 MB |
优化后 | 200 MB | 260 MB | 90 MB | 230 MB |