1、前言
包巨细是衡量APP功用的一项重要方针,它直接影响用户的下载点击率(包太大不想下)、下载安装成功率(下载慢不用了)、APP卸载率(太占空间先删掉)。包巨细的核算逻辑很简略,它是各种类型的文件占用磁盘巨细相加。APP减肥的技能却很复杂,代码文件的复杂度和编译器策略决议了可履行文件的巨细,事务功用和工程架构决议了代码文件的复杂度。iOS APP减肥,需求掌握的技能有XCode构建技能、LLVM编译器技能、CocoaPods构建技能、图片紧缩技能、持续集成技能。本文总结提炼了Alibaba.com App的减肥的技能和策略,系统化地介绍APP减肥的事务价值、剖析技能、减肥技能、防劣化机制,让读者能够系统化地了解APP减肥的技能体系。本文还根据实践经历,介绍各种减肥技能的ROI,让读者能够防止踩雷,将资源糟蹋在作用不佳的技能上。希望对你有所协助。
2、事务价值
2.1、包体巨细每上升6MB,运用下载转化率就会下降1%
在2019谷歌开发者大会上,谷歌给出了一个很具体的数据,包体巨细每上升6MB,运用下载转化率就会下降1%。不同区域转化率略有差异,APK包体巨细每削减10MB ,全球平均下载转化率会提升1.75%,新兴国家代表印度和巴西下载转化率提升2.0%以上,高端市场代表美国和德国下载转化率提升1.5%。
上图标题:APK削减10MB,在不同国家转化率增长
数据来源:google play 内部数据
上述数据调研剖析报告是2019年曾经的,现已有所滞后,仅供参阅
包巨细影响下载转化率可能有3个原因:
-
蜂窝网络环境下,用户不愿意付出流量费用。包巨细超越200MB时,App Store会弹框提醒用户下载可能会产生流量费用。
-
下载时间太长,用户不愿意等就取消了
-
下载过程中呈现网络连接问题
虽然Google Play没有给出不同APP类目的数据,但是从以上三个原因揣度,不同类目包巨细对下载转化率的影响估量差不多。App Store的用户人群比较高端,能够参阅美国和德国的数据。
2.2、20%的人由于存储空间有限而卸载运用程序
clevertap在2021年做了一项查询,他们查询了2000多个移动运用程序用户,询问了他们卸载移动运用程序的首要原因,其中有20%的人由于存储空间有限而卸载运用程序。
最首要的3个原因是:
- 他们不再运用该运用程序
- 有限的存储空间
- 太多的广告。
2.3、App Store 发布和下载约束
兼容iOS8的App,主二进制文件的Text段不能超越60MB, 不然将无法提交App Store。App Store下载包超越200MB,无法运用蜂窝流量下载和更新。
3、剖析技能
APP减肥终究方针是削减App Store的安装包巨细和下载包巨细,但研发阶段比照XCode构建包巨细会更便利,需求理清楚他们之间的口径差异。
3.1、成果方针:App Store安装包巨细和下载包巨细
检查途径是App Store Connect->TestFlight->交付版本->构建版本元数据->App Store文件巨细
3.2、过程方针:XCode构建包巨细
XCode构建产品ipa包的巨细和App Store的安装巨细口径差异很大,经过模拟Apple处理的流程能够得到它们的联系。开发者上传的ipa包里有多个架构的产品,多套尺寸的图片资源,苹果会进行裁剪和二次分发,转化为App Store下载的ipa包。
构建产品ipad包巨细:静态库二进制文件(arm64、armv7)、动态库(arm64、armv7)、asset.car(@2x、@3x)、其他资源文件
App Store的安装巨细:静态库二进制文件(单架构)、动态库(单架构)、asset.car(单尺寸)、其他资源文件
运用lipo东西拆分单架构
lipo "originalExecutable" -thin arm64 -output "arm64Executable"
运用assetutil东西拆分asset
xcrun --sdk iphoneos assetutil --scale 3 --output "$targetFolder/Assets3.car" "$sourceFolder/Assets.car"
3.3、构建包组成
学习怎么看病前,得先了解人体的构成。同样的道理,咱们首现要了解iOS工程结构和IPA包结构。
iOS工程结构:iOS工程由壳工程和Pod模块组成,模块有静态库和动态库两种类型。壳工程的构成有主Target、Apple插件Target。模块内部的构成有源代码文件(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…。从iOS工程到IPA包: iOS工程构建为IPA包,核心的改变是编译和文件复制,静态库里的源代码会被编译为MachO可履行文件,xcassets文件夹会被转化为Assets.car,其他都能够简略理解为文件复制。
经过剖析构建包的组成,能够判别有哪些优化空间。假如动态库占比太高,就可能有很多可裁剪代码。主张依据资源巨细敏感程度划分,比方:MachO executeable(包含一切静态库)、动态库、Apple Extension、assets.car、bundle、nib、音频、视频。
3.4、Pod模块巨细
APP减肥会涉及很多事务模块,离不开事务团队的参与。因而,咱们需求剖分出每个Pod模块的巨细,然后能够横向比较各个事务团队的包巨细占比。静态库和动态库核算的原理不同。关于静态库,先解析linkmap数据,核算出Pod模块代码巨细,在解析Pods-targetName-resource.sh的资源复制代码,核算出复制到Pod模块的资源巨细。关于动态库,先运用lipo拆分动态库的二进制文件,核算出单架构的代码巨细,然后再核算动态库framework内的资源文件,得到动态库的资源文件巨细。
3.5、运行时Objc类覆盖率
假如能知道App运行时有哪些类被运用过,就能够下线掉无用的模块或代码文件,Objc类覆盖率方针能够帮到咱们。APP运行时,某1个Pod模块被加载的类数量除以一切类数量,能够称为这个模块的Objc类覆盖率。核心技能是判别一个类是否被加载过,下面介绍一个经过线上验证的轻量级方案。ObjC的类第一次被运用时会调用+initialize办法,类被加载往后cls->isInitialized会返回True。isInitialized办法读取了metaClass的data变量里的flags,假如flags里的第29位为1,则返回True。
// objc-class.mm
Class 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;
}
3.6、未被运用的图片资源
技能原理是先解分出一切复制到构建产品的资源文件,再解分出代码中实际引证到的资源文件,两者的差集就是无用资源。
第一步获取全量资源文件。在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。运用strings解析framework的二进制文件就能够得到代码中一切的字符串声明,然后根据代码的各种引证方式匹配“xxx”,”xxx@2x.png”,”xxx.png”,”xxx.bundle/xxx.png”
strings executable | grep 'xxx' > cstrings.txt
3.7、编译时未被引证的类
iOS编译的产品是Mach-o格局的,文件里 __DATA __objc_classrefs 段记载了一切引证过的类的地址,__DATA __objc_classlist段记载了一切类的地址,两者Differ能够得到未被引证类的地址。然后将地址符号化,就能够得到未被引证类信息。由于Objc是动态言语,假如运用runtime动态调用某个class,这种情况扫描不出来。(比方Target Action和 JS Core)
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
扫描未运用类的开源东西
4、减肥技能
包巨细减肥能够从纯技能视角减肥,也能够从逻辑视角减肥。
从纯技能视角有两种思路,第一种思路是优化编译逻辑, 第二种思路是删减各种其他类型(非编译产品)的文件。编译优化对事务逻辑无侵入式,危险和本钱比较低,但收益通常也不高。删减文件则比较复杂,删减资源文件收益高但本钱不小,逐一删减源码文件危险高且收益小。
相比而言,从逻辑的视角减肥作用会更显着。站在逻辑的视角,工程是由许多功用模块组成的,首要能够分为事务功用模块(主页、查找、收银台、订单)、根底功用模块(网络库、图片库、中间件、各种三方库…)。大型工程通常几百乃至上千个模块组成,小模块的有几十KB,大的模块有1MB~10+MB。功用模块内聚性强,当某个功用模块能够废弃或被代替时,整体下线的危险和本钱比较低,ROI很高。
4.1、减肥技能大图
依据项目经历,我整理出各种优化办法的ROI,下面依次介绍。
4.2、组件减肥
组件减肥的ROI:类加载率0%的组件 > 无用功用组件 > 重复功用组件
“类加载率0%的组件”是一个完好的Pod模块,模块的代码和资源能够被整体删除,ROI最高。“无用功用组件”通常是模块内某个文件目录,需求处理一些耦合,整理资源的归属,ROI其次。“重复功用组件”需求适当重构,搬迁本钱和危险最高,ROI相对较低。
4.2.1、类加载率0%的组件
“类加载率0%的组件”是指运行时没有任何一个类被加载过的组件,它在运行过程中完全没有用到,能够整体下线。核算类加载率时一般会进行采样,有些低频事务组件可能会误报,下线前需求和事务Owner二次承认。
4.2.2、无用功用组件
“无用功用组件”是逻辑上现已没用的组件,它可能代码还有耦合,但逻辑现已没用,经过重构就能够下线。比方AB试验没作用的事务代码、往年大促的事务代码、事务改版后遗留的老事务、现已过期的三方库。
4.2.3、重复功用组件
关于大规模团队,不同事务线可能会引进“重复功用组件”,比方图片挑选器、缓存库、UI组件。重复的功用组件会带来不必要的包巨细压力,一起也会带来保护本钱。
4.3、资源减肥
资源减肥ROI:大资源>有损紧缩>重复资源>iconfont>多言语案牍减肥>无用资源
4.3.1、大资源
对大资源进行单点优化收益很大,优先剖析100KB以上的资源。比方音频文件,咱们工程中音频铃声900KB,优化后去掉了700KB。
4.3.2、有损紧缩
XCode构建时会做“compile asset catalog”,会从头对图片进行无损紧缩。因而运用imageoptim等东西进行无损紧缩作用不显着,其中紧缩png图片没有用果,紧缩jpg图片有一定作用。依据实践经历,icon做有损紧缩并不影响视觉体验,紧缩率能够到达70%~80%。比方运用tinypng算法紧缩一张425.9 KB的png图片,紧缩后79.8 KB,紧缩率到达81%。业界有不少png紧缩东西,咱们运用到的有tinypng、pngquant、pngcrush、optipng(无损)、advpng。实测发现不同的图片,紧缩作用最好的东西是不一样,所以紧缩图片时能够用每个东西测验,然后取作用最好的。需求留意的是,紧缩APNG图片可能会变成非动图,尽量防止对APNG图片进行紧缩,紧缩前先辨认是否APNG图片。
# 判别是否APNG格局,APNG格局不紧缩
function isApng() {
ret=`grep "acTL" "$1"`
lastWord="${ret##* }"
if [[ $lastWord == "matches" ]]; then
echo "YES"
else
echo "NO"
fi
}
# 运用各种东西紧缩png图片
# pngquant
pngquant 256 -s5 --quality 70 --output "${tmpFileName}" -- "${tmpOrigName}"
# pngcrush
pngcrush -brute -rem alla -nofilecheck -bail -blacken -reduce -cc -- "$tmpOrigName" "$tmpFileName"
# optipng
optipng -o6 -i0 -out "$tmpFileName" -- "$tmpOrigName"
# advpng
cp "$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"
4.3.3、无用资源
事务长时间迭代会堆集许多无用的资源。经过代码静态扫描,能够剖分出没有被引证的图片资源。咱们有工程无用资源有12MB,其中有20个模块无用资源超越100KB。
4.3.4、重复图标
不同事务需求会引进重复资源,长时间堆集也会造成很大糟蹋。解决方案是在构建时核算资源的哈希值,去重相同哈希资源,并保存源文件名和哈希值的映射表。运行时Hook 资源加载的”imageNamed“办法,依据映射表替换资源名称。
4.3.5、ODR
iOS不能像Android一样,运行时更新Framework。iOS的ODR技能(On Demand Resource)供给了运行时动态下载的才干。假如发动用不到的资源文件,能够经过ODR处理。
4.3.6、iconfont
iconfont支撑缩放、修正颜色,它size小,适合用于箭头、占位图等图标场景,运用iconfont能够削减包巨细也能进步开发视觉体验的统一性。首先,根据设计规范出一套完好的iconfont,封装iconfont组件,供给易用的API。然后制止新事务场景添加图片资源,团队构成开发习气后再逐渐替换存量的图片。
4.3.7、多言语案牍
关于国际化App,多言语案牍也是大头。咱们App支撑20个语种,每个语种4000多条案牍,案牍总巨细6MB,减肥后削减了2MB。
4.4、编译优化:
编译优化ROI:Optimization Level > 动态库复用主二进制静态库> 链接器产品紧缩
XCode编译优化选项中,Optimization Level作用最显着,主张一切模块构建都敞开Oz选项。假如APP有动态库,并且依靠了openssl等根底静态库,主张和APP工程共享,并经过EXPORTED_SYMBOLS_FILE的选型,保证动态库中需求用到的符号都在编译过程保存。假如团队技能储备强,能够自研链接器产品紧缩技能,作用也很大。具体作用如下。
4.4.1、精简编译产品Oz:Optimization Level
Optimization Level多个等级,-Oz比-O3的编译产品体积小10%左右。设置-Oz以后,XCode会优化接连的汇编指令,然后削减二进制巨细,但副作用是履行速度会变慢。C++工程主张都敞开。
主工程Release
Optimization Level :-Oz
Framework工程
Optimization Level :-Oz
4.4.2、LTO(OC/C++)
LTO 是在LLVM链接时,优化跨模块调用代码。依据咱们分享的经历,它不同APP包巨细优化作用差异较大,需求具体测验。另外它也有一些副作用,主张只在Release包敞开。LTO介绍
优点:
- 将一些函数內联化
- 去除了一些无用代码
- 对程序有全局的优化作用
缺陷:
- 降低编译链接速度,只主张在打正式包时敞开
- 降低 link map 可读性(呈现XX-lto.thin的类)
主工程设置
主工程Release
Link-Time Optimization 设置为Incremental
framework工程Release
Link-Time Optimization 设置为Incremental
4.4.3、动态库复用主二进制静态库
C++动态库常常用到一些根底库比方openssl、libyuv、libcurl,他们一般是静态库。假如动态库引证了静态库,它编译时默许会内嵌静态库的一切符号。虽然咱们能够在动态库中设置只导出需求用到的静态库符号,但是有可能多个动态库都用到了同一个根底库,这样仍是会造成根底库的冗余。比方openssl巨细1MB,假如A、B两个动态库依靠了openssl,APP也引证了openssl,终究ipa包实际有3个openssl,有2MB巨细是冗余的。
这种场景下,最佳解决方案是共享符号表,让动态库能够调用主二进制的根底库符号,然后能够去掉内置的静态库。只要修正XCode的Link配置,无需额外的代码开发。
动态库工程:
1设置当遇到未界说的函数时,动态查找APP主二进制符号表。
2 封闭bitcode
Other Linker Flags -> -undefined dynamic_lookup
Enable Bitcode -> No
3导出动态库需求调用的外部符号,写到一个文件exported_symbols内
nm -u xxx.framework/xxx > exported_symbols.txt
APP工程:
1配置需求导出exported_symbols文件内的一切符号,防止编译时动态库需求用到的符号被strip掉。
2封闭bitcode。
// exported_symbols.txt是需求被导出的符号文件途径
EXPORTED_SYMBOLS_FILE -> exported_symbols.txt
Enable Bitcode -> No
假如C++动态库里依靠了外部静态库,该静态库的符号默许会被悉数导出。实际上动态库只用了其中的部分符号,能够设置导出符号的白名单,然后削减导出没有用到的静态库符号。
留意事项:1APP和framwork工程都要封闭bitcode,不然编译不过。2假如多个动态库都运用同一个根底库,导出的符号表需求取并集。3动态库晋级要及时更新符号表,根底库晋级要测验兼容性。
4.4.4、链接器产品紧缩(黑科技)
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段数据存到自界说段中,将代码中对字符串的引证的地址修正为解压后的自界说段。
4.4.5、剥离符号表:Strip Linked Product
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 文件中找对应符号
4.4.6、Symbols Hidden by Default
Symbols Hidden by Default用于设置符号默许可见性,假如设置为YES,XCode会把一切符号都界说为”private extern”,包巨细会略有削减。动态库设置为NO,不然会有链接错误。
主工程Release
Symbols Hidden by Default :Yes
Framework工程 静态库/动态库
Symbols Hidden by Default :NO
4.4.7、除掉未引证的C/C++/Swift代码:Dead Code Stripping
Dead Code Stripping敞开后会在链接时移除未运用的代码,它对静态言语C/C++/Swift有用,对动态言语OC无效。
主工程
Dead Code Stripping :Yes
4.4.8、Asset Catalog Compiler
Optimization有三个选项,空、time和Space,挑选Space能够优化包巨细
主工程
Asset Catalog Compiler->Optimization设置为space
4.4.9、C++只导出必要符号:Symbol Visibility
C++的静态库和动态库都只导出必须的符号,默许设置为隐藏一切符号,然后用Visibility Attributes独自操控需求导出的符号
默许隐藏一切C++符号
Other C++ Flags->添加-fvisibility=hidden
设置需求导出的符号
__attribute__((visibility("default"))) void MyFunction1() {}
__attribute__((visibility("default"))) void MyFunction2() {}
....
4.5、代码下线
依据Objc类覆盖率核算的成果,能够逐渐下线掉未被运用的类。代码文件的量级比较小,下线代码需求仔细承认,防止引起功用问题或crash,ROI比较低。
4.6、Flutter专项
Flutter是独自构建的,引进的内容包含Flutter引擎产品、Flutter事务代码产品。APP假如引进Flutter,需求对Flutter进行专项减肥。
4.6.1、精简编译产品Oz:Optimization Level
-Oz选项相比Os,收益预估11%,但首屏功用1%~9%的损耗
4.6.2、Dart符号除掉和混杂
依据官方文档,能够经过–dwarf-stack-trace选项去除Dart规范的调试符号。经过–obfuscate 选项,能够将较长的符号替换为短符号,副作用是符号会被混杂。实践作用:Release环境下,–dwarf-stack-trace和–obfuscate选项敞开后削减14%的巨细
4.6.3、Flutter icon摇树优化
–tree-shake-icons
实践作用:Alibaba.com App敞开后削减了300KB左右巨细
4.6.4、去除NOTICES文件
实践作用:紧缩前700KB,紧缩后80KB
5、防劣化机制
5.1、增量规范和集成卡口
包巨细减肥的技能就像各种减肥运动,跑步、跳舞、燃脂瑜伽。但想到达减肥的方针,只靠运动是不够的,还得操控卡路里的摄入,管住嘴的人最终才干减肥成功。因而,咱们还需求”新增卡路里的规范”(包巨细增量规范)和监工(集成卡口)。持续集成傍边参加一个卡口插件,剖析构建包的linkemap文件得到模块的巨细可,然后比照基线数据,假如违反了包巨细增量规范,则制止集成。
包巨细增量规范(参阅):
- 基线数据:根据特定版本,经过上文提到的”核算模块巨细“的办法,核算出每个模块的基线数据
- 存量模块:模块增量超越基准数据100KB时制止集成。设置补偿机制,假如能对存量模块减肥,能够抵消新增的模块巨细。关于特殊情况能够走特殊批阅,参加批阅流程是启发咱们反思。添加那么多是否有价值?有没有带入不必要的资源?
- 新模块:新事务功用需求走批阅流程,评估新事务价值和包巨细增量是否合理。
5.2、横向比照事务健康度
当包巨细从技能层面现已优化到极致时,想要进一步减肥,只能从事务价值的角度去挖掘。假如一个模块磁盘尺寸大,用户运用量却少,那能够以为它对事务的价值较小。因而咱们能够界说一个技能方针来衡量模块单位巨细的事务贡献度。根据容积率方针,咱们就能够横向比照各个事务,要求容积率低的事务做包巨细减肥,或下线不太重要的事务功用。
容积率 = Business PV / Business size
l
6、总结
总结一下包大减肥的施行途径。第一步,制定方针,跟踪APP下载转化率(App Store Connect Analytics)、APP安装包巨细、XCode构建包巨细等成果方针。第二步,建设剖析体系,包含Pod模块巨细剖析、Objc类覆盖率剖析、无用图片资源剖析等。第三步,依据ROI运用各项减肥技能,组件减肥>资源减肥>编译优化>代码下线.第四步,建设防劣化机制,包含增量规范、集成卡口才干、健康度剖析。
7、参阅
白鲸出海:2019谷歌开发者大会首日亮点:Google Play的新改变
www.baijingapp.com/article/248…
googleplaydev:Shrinking APKs, growing installs
medium.com/googleplayd…
clevertap:Why Users Uninstall Apps
clevertap.com/blog/uninst…
Pngcrush
pmt.sourceforge.io/pngcrush/
pngquant
pngquant.org/
OptiPNG
optipng.sourceforge.net/
advpng
www.advancemame.it/doc-advpng.…