Alibaba.com瘦包40MB——业界最全的iOS包大小技术总结
前言
包大小是衡量APP性能的一项重要指标,它直接影响用户的下载点击率(包太大不想下)、下载安装成功率(下载慢不用了)、APP卸载率(太占空间先删掉)。包大小的计算逻辑很简单,它是各种类型的文件占用磁盘大小相加。APP瘦身的技术却很复杂,代码文件的复杂度和编译器策略决定了可执行文件的大小,业务功能和工程架构决定了代码文件的复杂度。iOS APP瘦身,需要掌握的技能有XCode构建技术、LLVM编译器技术、CocoaPods构建技术、图片压缩技术、持续集成技术。本文总结提炼了Alibaba.com App的瘦身的技术和策略,系统化地介绍APP瘦身的业务价值、分析技术、瘦身技术、防劣化机制,让读者可以系统化地了解APP瘦身的技术体系。本文还基于实践经验,介绍各种瘦身技术的ROI,让读者可以避免踩雷,将资源浪费在效果不佳的技术上。希望对你有所帮助。
业务价值
1. 蜂窝网络环境下,用户不愿意支付流量费用。包大小超过200MB时,App Store会弹框提醒用户下载可能会产生流量费用。
2. 下载时间太长,用户不愿意等就取消了
1. 他们不再使用该应用程序
2. 有限的存储空间
详细材料: clevertap:Why Users Uninstall Apps
分析技术
APP瘦身最终目标是减少App Store的安装包大小和下载包大小,但研发阶段对比XCode构建包大小会更方便,需要理清楚他们之间的口径差异。
结果指标:App Store安装包大小和下载包大小
lipo "originalExecutable" -thin arm64 -output "arm64Executable"
xcrun --sdk iphoneos assetutil --scale 3 --output "$targetFolder/Assets3.car" "$sourceFolder/Assets.car"
// objc-class.mmClass class_initialize(Class cls, id inst) {if (!cls->isInitialized()) {initializeNonMetaClass (_class_getNonMetaClass(cls, inst));}return cls;}// objc-runtime.h#define RW_INITIALIZED (1<<29)bool isInitialized() {return getMeta()->data()->flags & RW_INITIALIZED;}
// Pods-targetName-resource.shinstall_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"
strings executable | grep 'xxx' > cstrings.txt
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
瘦身技术
包大小瘦身可以从纯技术视角瘦身,也可以从逻辑视角瘦身。 从纯技术视角有两种思路,第一种思路是优化编译逻辑, 第二种思路是删减各种其他类型(非编译产物)的文件。编译优化对业务逻辑无侵入式,风险和成本比较低,但收益通常也不高。删减文件则比较复杂,删减资源文件收益高但成本不小,逐个删减源码文件风险高且收益小。
类加载率0%的组件
无用功能组件
“无用功能组件”是逻辑上已经没用的组件,它可能代码还有耦合,但逻辑已经没用,通过重构就可以下线。比如AB实验没效果的业务代码、往年大促的业务代码、业务改版后遗留的老业务、已经过时的三方库。重复功能组件
对于大规模团队,不同业务线可能会引入“重复功能组件”,比如图片选择器、缓存库、UI组件。重复的功能组件会带来不必要的包大小压力,同时也会带来维护成本。大资源
对大资源进行单点优化收益很大,优先分析100KB以上的资源。比如音频文件,我们工程中音频铃声900KB,优化后去掉了700KB。有损压缩
# 判断是否APNG格式,APNG格式不压缩function isApng() {ret=`grep "acTL" "$1"`lastWord="${ret##* }"if [[ $lastWord == "matches" ]]; thenecho "YES"elseecho "NO"fi}# 使用各种工具压缩png图片# pngquantpngquant 256 -s5 --quality 70 --output "${tmpFileName}" -- "${tmpOrigName}"# pngcrushpngcrush -brute -rem alla -nofilecheck -bail -blacken -reduce -cc -- "$tmpOrigName" "$tmpFileName"# optipngoptipng -o6 -i0 -out "$tmpFileName" -- "$tmpOrigName"# advpngcp "$tmpOrigName" "$tmpFileName"advpng -4 -z -- "$tmpFileName"# 逐个比较压缩效果,保留效果最好的压缩图片sizeBefore=`stat -f%z "${tmpOrigName}"`sizeBefore=`expr $sizeBefore`sizeNow=`stat -f%z "${tmpFileName}"`sizeNow=`expr $sizeNow`if [ "$sizeNow" -lt "$sizeBefore" ]; then# 大小变小了echo "[advpng] $tmpOrigName"retImg="$tmpFileName"fi# 记录hash值img_sum=`/usr/bin/shasum -a 256 "$retImg" | cut -d " " -f1`cache_file="$cacheFolder/$img_sum.png"
无用资源
业务长期迭代会积累许多无用的资源。通过代码静态扫描,可以分析出没有被引用的图片资源。我们有工程无用资源有12MB,其中有20个模块无用资源超过100KB。重复图标
不同业务需求会引入重复资源,长期积累也会造成很大浪费。解决方案是在构建时计算资源的哈希值,去重相同哈希资源,并保留源文件名和哈希值的映射表。运行时Hook 资源加载的”imageNamed“方法,根据映射表替换资源名称。ODR
iOS不能像Android一样,运行时更新Framework。iOS的ODR技术(On Demand Resource)提供了运行时动态下载的能力。如果启动用不到的资源文件,可以通过ODR处理。iconfont
iconfont支持缩放、修改颜色,它size小,适合用于箭头、占位图等图标场景,使用iconfont可以减少包大小也能提高开发视觉体验的统一性。首先,基于设计规范出一套完整的iconfont,封装iconfont组件,提供易用的API。然后禁止新业务场景增加图片资源,团队形成开发习惯后再逐步替换存量的图片。多语言文案
对于国际化App,多语言文案也是大头。我们App支持20个语种,每个语种4000多条文案,文案总大小6MB,瘦身后减少了2MB。| 主工程 | OC模块 62880行代码 | C++模块 | |
| LTO | <10KB | 开启前 modulesize:2020KB, MainProjectSize:6463KB 开启后 modulesize:1582KB MainProjectSize:6888KB 收益:13KB | / |
| 剥离符号表:Strip Linked Product | <10KB | / | / |
| 精简编译产物Oz:Optimization Level | 100-200KB | Os:2261KB Oz:2020KB 收益:241KB | / |
| Symbols Hidden by Default | <10KB | 优化前:2020KB 优化后:2020KB 收益:0KB | / |
| 剔除未引用的C/C++/Swift代码:Dead Code Stripping | <10KB | / | / |
| Asset Catalog Compiler | <10KB | ||
| 动态库共享APP基础静态库 | / | / | 动态库A依赖了公共的静态库库B,通过设置只导出必要符号 收益:1367KB |
| C++全局静态数组改成动态分配内存 | / | 收益:200KB | |
| C++去掉RTTI支持 | / | 收益:1MB |
主工程ReleaseOptimization Level :-OzFramework工程Optimization Level :-Oz
● 将一些函数內联化
● 去除了一些无用代码
● 降低编译链接速度,只建议在打正式包时开启
主工程ReleaseLink-Time Optimization 设置为Incrementalframework工程ReleaseLink-Time Optimization 设置为Incremental
动态库复用主二进制静态库
Other Linker Flags -> -undefined dynamic_lookupEnable Bitcode -> No
APP工程: 1、配置需要导出exported_symbols文件内的所有符号,避免编译时动态库需要用到的符号被strip掉。 2、关闭bitcode。nm -u xxx.framework/xxx > exported_symbols.txt
// exported_symbols.txt是需要被导出的符号文件路径EXPORTED_SYMBOLS_FILE -> exported_symbols.txtEnable Bitcode -> No
链接器产物压缩(黑科技)
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
Strip Linked Product设置会剥离特定的符号,Debug环境不要设置YES,否则调试时看不到符号。主工程ReleaseDeployment Postprocessing :YESStrip Linked Product :YESStrip Style :All Symbols(剥离所有符号表和重定向信息)Framework工程Deployment Postprocessing :YESStrip Linked Product :YESStrip Style :Non-Global Symbols(剥离包括调试信息等非全局的符号,保留外部符号)
Symbols Hidden by Default
Symbols Hidden by Default用于设置符号默认可见性,如果设置为YES,XCode会把所有符号都定义为”private extern”,包大小会略有减少。动态库设置为NO,否则会有链接错误。主工程ReleaseSymbols Hidden by Default :YesFramework工程 静态库/动态库Symbols Hidden by Default :NO
主工程Dead Code Stripping :Yes
Asset Catalog Compiler
Optimization有三个选项,空、time和Space,选择Space可以优化包大小主工程Asset Catalog Compiler->Optimization设置为space
设置需要导出的符号Other C++ Flags->添加-fvisibility=hidden
__attribute__((visibility("default"))) void MyFunction1() {}__attribute__((visibility("default"))) void MyFunction2() {}....
精简编译产物Oz:Optimization Level
-Oz选项相比Os,收益预估11%,但首屏性能1%~9%的损耗Dart符号剔除和混淆
根据官方文档,可以通过--dwarf-stack-trace选项去除Dart标准的调试符号。通过--obfuscate 选项,可以将较长的符号替换为短符号,副作用是符号会被混淆。实践效果:Release环境下,--dwarf-stack-trace和--obfuscate选项开启后减少14%的大小Flutter icon摇树优化
--tree-shake-icons 实践效果:Alibaba.com App开启后减少了300KB左右大小去除NOTICES文件
实践效果:压缩前700KB,压缩后80KB防劣化机制
1. 基线数据: 基于特定版本,通过上文提到的”计算模块大小“的方法,计算出每个模块的基线数据
2. 存量模块: 模块增量超过基准数据100KB时禁止集成。 设置补偿机制,如果能对存量模块瘦身,可以抵消新增的模块大小。 对于特殊情况可以走特殊审批,加入审批流程是启发大家反思。 增加那么多是否有价值? 有没有带入不必要的资源?
总结
总结一下包大瘦身的实施路径。第一步,制定目标,跟踪APP下载转化率(App Store Connect Analytics)、APP安装包大小、XCode构建包大小等结果指标。第二步,建设分析体系,包括Pod模块大小分析、Objc类覆盖率分析、无用图片资源分析等。第三步,根据ROI使用各项瘦身技术,组件瘦身>资源瘦身>编译优化>代码下线.第四步,建设防劣化机制,包括增量标准、集成卡口能力、健康度分析。
http://www.baijingapp.com/article/24808
扫描未使用类的开源工具
https://github.com/xuezhulian/classunref googleplaydev:Shrinking APKs, growing installs https://medium.com/googleplaydev/shrinking-apks-growing-installs-5d3fcba23ce2
clevertap:Why Users Uninstall Apps https://clevertap.com/blog/uninstall-apps/
Pngcrush https://pmt.sourceforge.io/pngcrush/
pngquant https://pngquant.org/
OptiPNG http://optipng.sourceforge.net/
advpngLTO介绍
https://zhuanlan.zhihu.com/p/384160632推荐阅读
在当今应用生态环境下,跨平台解决方案一直备受关注,业界有越来越多公司尝试Flutter。阿里巴巴实践Flutter的同时,一直在思考如何从经济体技术战略的层面拉通AliFlutter的体系建设。通过阅读此书,读者可以深入了解阿里巴巴Flutter技术及业务应用的实践,同时运用以实战。
点击阅读原文查看详情。