本文作者:大鹏
云音乐iOS客户端是自2013年开始的老我的项目,经验近十年的业务滚动倒退,从单体音乐APP倒退至今,多种业务加持,俨然曾经成为相似于平台级的巨型APP,并且包体积也随着业务的倒退越来越臃肿,影响用户的理论体验,甚至是品牌的口碑,在笔者开始优化之前云音乐在AppStore显示的包体积曾经达到了420MB之多,在这种状况下,团队开启了包体积优化的专项。
包体积优化是客户端开发的老命题了,基本上作为iOS开发同学多多少少都理解大体该怎么做,但随着苹果的倒退,一些原来可行的措施在新版本曾经不在实用,所以本篇文章则侧重于优化过程中的一些最新的实践经验,以及在大我的项目中是如何落地的,那么话不多说,上面就开始。
口径
在开始做优化之前,咱们首先须要摸清楚包体积的各种口径以及它们之间的关系,因为后续的一些优化措施会导致不同口径此消彼长的状况,所以首先要确定最终目标口径是什么。
首先,咱们能够在苹果后盾看到本人APP具体的装置大小和下载大小的具体情况,还蕴含了不同的机型版本。
那么苹果后盾的下载大小和装置大小是如何生成的呢,请看下图,在上传后,苹果官网对对咱们上传的IPA包解包后,对二进制进行了DRM加密(此项也会导致包体积的增长)和AppThinning,AppThinning会依据不同的机型对原始包的资源和代码进行不同水平的裁剪,从而生成适配具体机型的版本。此外苹果还会生成一个蕴含选集的通用版本,但并没有啥理论用途。对于DRM和AppThinning此处不开展,文章开端有链接。
如上图所示,在上传前后咱们有三个指标:
- APP原始包体积: 上传前IPA解包后,理论APP的大小
- 下载体积: AppStore中流量下载时提示框的大小
- 装置体积: AppStore中APP详情中显示的大小
在摸清了各指标的关系后,咱们最终抉择了用户感知最强的装置体积作为外围指标,以其作为最终目标进行优化。
剖析
尽管曾经确定了指标,但在优化之前,还须要对现状进行剖析,找到最大的劣化点,从而有针对性的进行优化,获取最大的收益比,那么下图就是云音乐iOS包的根本状况,能够看到红色的资源局部占到了一半以上的体积,而二进制则次之也占到了四分之一多,那么后续优化的侧重点能够放到资源和二进制源码。
资源
对于资源的解决其实形式就是惯例那么几种:资源清理、资源整顿、资源压缩、资源云端迁徙、资源合并等等,总之就是想尽一切办法去升高资源所占用的本地空间,上面简略介绍下咱们在云音乐所做的工作。
资源清理
在开始做整体的资源优化之前,第一步是须要清理曾经不在应用的资源,包含图片、配置文件、音视频等等,检测无用资源的次要思路就是通过动态检测判断资源是否有被援用,例如应用ImageName来判断图片是否被应用,当然线上检测的形式是更精确了,但在资源这里没有必要,不过云音乐作为老业务,应用图片的姿态也各式各样,例如援用的文件名不标准、未使AssetCatelog、手动拼接图片名称2x3x等问题,这就须要略微定制化的形式进行查找,排除异常情况,其余APP依据本身理论状况调整即可,思路都一样,网上也有现成工具。
云音乐通过几轮清理之后,先后清理图片等类型文件1200+,取得收益12+MB左右的原始包体积降落,还是比拟可观的。
资源整顿
资源整顿其实就是把适合的资源用适合的形式治理,这里次要指的就是Asset.car文件,家喻户晓,苹果自iOS7之后推出了AssetCatelog文件,帮忙开发者治理资源,其中最次要的就是图片资源,在编译之后会生成Asset.car文件并打入IPA包中,前文也说过,云音乐是老工程,所以还有局部资源图片是非Asset治理的形式,而应用Asset会给包体积带来收益,所以就须要对现有资源进行迁徙,应用Asset进行治理;但这里有个问题,为什么应用Asset会带来包体积收益呢?
要答复下面的问题,首先要从Asset的原理说起,在AssetCatelog的编译过程中,以ImageSet类型为例,首先会对Asset中的ImageSet类型图片进行无损压缩,并且会把多张ImageSet图片合成一张大图,故在编译后,是无奈通过bundle path的形式读取图片的,必须应用苹果的ImageName的API,因为它是通过坐标等形式,从合成后的大图中获取具体的图片信息的;那么这样做的益处就是,在压缩和合成的过程中会有图片体积的收益;然而通过咱们钻研,发现并不是所有图片都会有此收益,一些大体积的图片在通过无损压缩和合成后,产生的体积更大,咱们猜想这可能和合成大图无关,越小的图片收益的可能性越高。
另外就是动图最好不要应用ImageSet的类型,因为在压缩和合成的过程中动图会呈现问题,导致通过ImageName读取进去的数据不对,会产生无奈播放等问题;然而能够应用DataSet的类型,DataSet是不参加合成和压缩的,所以不会影响,对于其余的资源类型,个别也都能够应用DataSet的形式,而读取的时候应用NSDataAsset即可。那如何晓得Asset解决过的资源的状况呢,能够应用上面命令解析编译好的Asset.car,获取其中资源编译后的信息。
xcrun --sdk iphoneos assetutil --info Assets.car
从上图能够看到,对于Data类型的资源,是没有压缩的;而对于Image类型,是有标注出具体的压缩算法的,以及一些图片信息。另外在过程中咱们也发现,对于不同的图片苹果应用的压缩算法都是不同的,并且会被压缩成多份,这也是为什么咱们在把一部分资源从bundle中移入AssetCatalog中后,IPA体积还变大了的起因,但没关系,装置体积是会降落的,是因为应用Asset的最大的收益其实是来源于前文提到过的苹果的AppThinning,苹果的瘦身机制会把Asset.car依据不同的机型进行散发,例如1x2x3x都有不同应答的设施机型,所以尽管会被压缩成多份,但每台机器理论应用的只有一份,这也是为什么即使是IPA变大了,但其实装置体积会变小。
最初要说的是,因为没方法一个一个图片去进行Asset编译比照编译前后的大小,从概率来讲,更举荐小图(5k以内)以及有多版本(2x3x)的图片放入AssetCatalog治理,其余资源其实独自存储更自在,而非应用DataSet,因为独自存储更方便使用各种压缩伎俩而不放心会被苹果的解决而影响到,这个点在后续资源压缩会具体说到。
通过这项优化,云音乐iOS客户端迁徙各种尺寸的图片资源2400+,实现装置体积收益22+MB。
资源压缩
资源压缩很好了解,顾名思义就是对资源进行各种形式的压缩,在云音乐中最次要的资源就是图片,其余类型占比很小,常见的图片资源格局次要是png、apng、webp等,云音乐包里绝大部分图片也是以上几种格局;因为通过上一步的工作,简直所有图片都在AssetCatalog中治理,而上文也提到了苹果会对AssetCatalog的图片资源进行无损压缩,所以如果咱们自身对图片资源所施加的无损压缩是没有成果的,因为苹果会再压一遍,最终后果是以他为准。所以要在压缩这里拿到优化后果,就要实质性的升高图片的大小,那么就得做有损压缩。对于惯例图片格式,咱们应用了pngquant、tinypng等算法及工具进行压缩,在应用pngquant时,通过先后大数据样本测试,最终抉择80%的有损比率,因为此时是比率是收益曲线最高同时绝对图片品质影响较小的时候,但对于不同的工程这个曲线兴许是不一样的,因为每个工程的理论资源状况是有区别的,所以要自行去获取工程的数据,具体的做法是能够过脚本去尝试不同的压缩率并记录压缩后果从而造成一张曲线图。另外在咱们包里还有很多遗留的体积较大的webp动图,个别的形式都无奈进行压缩,通过肯定的调研最终发现谷歌官网提供了Webpmux能够对webp动图进行拆解和逐帧压缩以及合成,基于此咱们编写了一个能够压缩webp动图的脚本,实现了对webp动图的压缩。最终咱们把所有常见格局的图片压缩能力集成在一个大脚本中,对包内所有的图片资源进行压缩,此脚本对于后续防劣化也有用途。
通过此项,整体压缩各尺寸png图片5000+,apng动图100+,webp动图100+,总体收益42+MB(原始包体积)。
资源云端迁徙
在通过清理、整顿、压缩后,资源局部还是有不少包体积的占用,所以咱们启动了大资源云端迁徙专项,之所以是大资源是因为大资源带来的收益比最高,通过探讨,联合云音乐的理论状况,最终定下了50kb的基线,大于50kb则会被界定为大资源。咱们不是没有思考资源对立迁徙对立下载的计划,但从云音乐的体验以及老本层面思考,最终还是抉择以传统形式解决ROI高的局部。通过筛选后云音乐包内有150+的case合乎大资源的状况,其中85%以上是能够迁徙至云端的。对于资源是否要放在本地还是云端,咱们和设计同学独特制订了相干资源图片\动画的应用标准,纯技术资源则由技术同学判断。
在迁徙专项做完后,总体迁徙了100+的大资源,收益约在31+MB(原始包体积)。
资源合并
资源合并其实次要是二点,一个是单个类似图片的去重,咱们花了肯定功夫应用类似图的剖析算法对云音乐所有的资源图片进行了检测,后果和咱们预期并不相符,实际上并没有太多类似的图片、包含icon,此局部并无收益。另外一个是AssetCatalog合并,联合云音乐的理论状况,此项也并无收益,次要是云音乐的资源目前是集中化治理。
二进制
每个APP程序最终都会被编译出一个主体二进制文件,所有的动态库依赖都会被链接进来,此局部的大小次要由代码量以及编译参数影响,下文的优化思路也是集中于缩小代码量以及优化编译参数。
无用代码检测
想要升高代码量,首先想到的就是清理无用代码,那么哪些代码又是无用的呢?这就有了无用代码检测,个别检测的形式分为线上动静检测和线下动态检测,动静检测的准确率要远高于动态检测,并且动态代码编译器曾经反对了一些裁剪形式,例如DeadCode优化;那么基于此咱们采纳了更精确的线上大数据动静检测,惟一的毛病就是获取数据的周期较长,须要上线运行。
最后咱们的想法是通过hook类初始化办法+initialize来判断某个类是否被应用,但这种计划有几个问题:第一是启动机会的问题,因为咱们应用了AB采样,那么必须在AB初始化后某个工夫点开启,那么AB初始化之前的类就没法记录,除非所有用户都记录,只是在上传的时候采样,但这样会影响未被灰度的用户;第二是+initialize自身调用机会的问题,并不是所有类的+initialize都会被调用。之后咱们采纳了另外一种计划,在OBJC中,每个类都有本人的元数据,在元数据中的一个标记位存储着本人是否被初始化,这个标记位不受任何因素影响,只有有被初始化就会打标记,在objc的源码中获取标记位的形式如下:
struct objc_class : objc_object { bool isInitialized() { return getMeta()->data()->flags & RW_INITIALIZED; }}
但这个办法APP是无奈间接调用的,它是objc的办法;然而并不代表RW_INITIALIZED这个标记位的数据不存在,数据还是在的,所以咱们能够通过已有的接口以及能够浏览的源码信息来模仿上述代码,从而取得标记位数据确定某个类是否是初始化的,代码如下:
#define FAST_DATA_MASK 0x00007ffffffffff8UL#define RW_INITIALIZED (1<<29)- (BOOL)isUsedClass:(NSString *)cls { Class metaCls = objc_getMetaClass(cls.UTF8String); if (metaCls) { uint64_t *bits = (__bridge void *)metaCls + 32; uint32_t *data = (uint32_t *)(*bits & FAST_DATA_MASK); if ((*data & RW_INITIALIZED) > 0) { return YES; } } return NO;}
通过下面的模仿代码,我能够获取某个类是否是被应用的,进而上报信息后,基于大数据分析出哪些类是曾经能够清理的,通过此种形式,咱们检测出了数千个未被应用的类,但这些并不代表理论是可能清理的,比方有的在做AB,有的是预埋业务等等,所以数据后果还须要业务侧进行一遍过滤,最终咱们解决了1200+个类,胜利清理了300+,收益在2+MB(二进制章节所有口径均为原始包口径)左右,残余未解决的仍在解决中,作为长线进行优化。
二三方库下线
基于下面的未被应用的类数据,能够通过聚类分析,失去曾经不在应用的业务组件或者二三方库,在优化过程中咱们辨认出了数个能够下线的二三方库,收益在4+MB。
动静库依赖裁剪
除了业务代码的解决,自身云音乐也依赖了一些动静库,并且这些动静库因为历时起因,有些动态依赖是反复的,具体如下图所示:
这是比拟极其的一个Case,在主程序中、动静库A中、动静库B中别离有一份OpenSSL的符号,那么这种就造成了反复,占用二进制体积;那么这种问题最好的解决方案就是动转静,把动静库转化为动态库,都链接在主程序中,解除原来的依赖,都应用主二进制中的Symbol,这样还能够肯定水平的晋升启动速度,因为缩小了动静库的数量。通过对相似这种问题的解决,总体收益是3+mb。
编译优化
在通过各种形式优化裁剪代码之后,就要开始优化另外一个影响二进制体积的因素了,就是编译参数,编译参数有很多,能够分为编译期参数以及链接期参数,接下来我将整顿基本上所有会影响二进制体积的参数供读者参考应用
Asset Catalog Compiler Optimization
Asset编译优化能够升高Asset.car产物体积,此项云音乐之前只开启了主工程,未开启组件的编译参数,通过优化后收益未2.1MB
EXPORTED_SYMBOLS_FILE
对于APP来讲能够看做是一个大的“动静库”,用户在点击开启APP的时候零碎就开始加载这个动静库,那么动静库总会有向外裸露的符号也就是Exported Symbols,然而对于APP而言个别不会在iOS零碎里还有别的中央调用,更多的是APP调用零碎的服务,所以咱们能够把Exported Symbols给Trim掉,还好编译器提供了EXPORTED_SYMBOLS_FILE能够让咱们限度输入的符号,从而升高二进制的体积;具体的形式是新建一个txt文件,放入工程目录中(仅工程目录,无需退出到xcode工程中,会成为资源影响包体积),把EXPORTED_SYMBOLS_FILE指向这个文件,那么如果是空文件则所有的exported符号段都会被裁剪掉,能够通过在txt文件里指明具体要留下的符号,编译器就会裁剪掉未声明的局部。
下图为开启后被裁减掉符号段
值得注意的是,如果APP应用了Firebase,则不能全副裁剪掉,会导致Firebase启动不胜利,进而无奈获取Crash信息,起因是Firebase依赖上图Export Info中的__mh_execute_header这部分符号,所以能够在上文提到的txt文件中退出__mh_execute_header,则编译器在裁剪时会保留__mh_execute_header的局部。
此项为链接期优化,只需主工程开启即可,云音乐在开启后,此项收益是2.4MB。
Link-Time Optimization
LTO的优化次要体现在跨文件的废除代码裁剪优化、永远不会执行的空逻辑优化、内联优化,意思是间接复制函数,缩小内联层级,晋升函数栈的执行效率和空间利用率。详情请查看LLVM的官网文档,此处不在赘述。
另外通过测试验证了LTO只对动态语言失效,OC是动静语言,所有函数办法有可能在运行时被动静调用,所以是不可能裁剪的,这就是为什么在链接动态库时,如果是C库,那么看起来原来二进制很大,实际上被理论链接进来的只有实在应用的小局部,然而如果是OC库则基本上全副会链接。所以如果你的APP源码中C或者C++代码较多的话在此项上收益可能会大一些。
尽管LTO名称看起来是链接期优化,但实际上是编译期也须要参加的,否则会没有成果,这和跨文件的优化无关,在编译期就要产出局部信息,提供链接期优化应用。
通过LTO的优化,云音乐取得的收益是1MB。
GCC_OPTIMIZATION_LEVEL
此项意通过更激进的GCC编译优化,进而产生更低的二进制产物,Xcode默认是Debug设置O0,Release设置为Os,但其实还能够应用Oz模式,从而达到更小的体积。
其实Oz的原理和下面的在内联(inline)还是外联(outline)上的思路LTO刚好相同,Oz是想通过更多的外联来升高函数的内联层级,但这样就会是函数的调用栈变得很深,进而会升高函数的执行效率,如上图所示会变得比拟“慢”,其实实质上也是工夫和空间的博弈;另外如果要想开启此项可参考抖音的文章,他们有遇到一些objc_retainAutoreleaseReturnValue的问题,但截至目前,咱们在理论实际的过程中临时并未发现,不过基于稳定性的思考,此项目前还未在云音乐上线,只是在debug环境开启进行测试,还在继续察看中。
如果开启此项,通过测试预估的收益在10+MB左右。
其余编译优化项
- Enable C++ Exceptions以及Enable Objective-C Exceptions,敞开掉此项能够带来二进制体积上的收益,然而会影响TryCatch,酌情应用,云音乐未开启
- Architectures,架构指令集,此局部须要留神一些二三方的Framework是否蕴含不须要的指令集
Strip Symbols,裁剪符号相干,此处不开展,下方为相干设置
- Strip Linked Product = YES
- Strip Style = All Symbols,注:在Strip Linked Product未开启时,此项设置不失效
- Deployment Postprocessing 注: 此项在打包是无论怎么设置,苹果会默认设置为YES
- Symbols Hidden by Default = YES,设置符号可见性
- Make Strings Read-Only = YES
- Dead Code Stripping = YES,编译期检测断定未应用代码进行裁剪
- Optimization Level,个别debug设置为None,Release设置为Os
二进制小结
除了以上各种优化二进制的措施外,其实在业界还有不少其余措施,但云音乐因各种起因并未采纳,例如通过重命名_Text代码段,进而绕过苹果的DRM加密,来升高二进制大小,但此项在iOS13之后苹果曾经意识到这个问题,并肯定水平上解决了,所以这个优化办法基本上曾经生效了;还有二进制段压缩,从危险和收益的角度考量,也是暂未应用;还有属性动态化,次要是针对有大量属性的模型属性进行动静优化,动静增加get/set办法,从而取得省略这部分办法的收益,此项收益估算也很小,也就并没有应用。其实总结来说优化办法是很多的,但对于具体的APP依据理论状况抉择最合适的措施即可,并不一定非要如何如,毕竟要有ROI的考量。
防劣化
在优化的过程中,咱们发现工程的理论劣化速度也很快,甚至达到了每个迭代优化量的40%~50%,也就是说,咱们假设一个迭代优化了10MB,然而这个迭代的劣化达到了4~5MB,所以咱们不得不在治理的同时就开启防劣化的工作,咱们制订了一些防劣化措施,其中一部分曾经上线,剩下的还在开发中,目前曾经获得了很好的成果,体积的劣化状况曾经失去了比拟无效的遏制,也保住了优化的成绩,具体措施如下:
- 大资源卡口:在代码合入时进行资源检测,并强制卡口
- 二方库三方库卡口:在代码合入时进行二方库三方库的检测,蕴含新增和降级
- 主动压缩:对于资源合入进行主动压缩,但首推还是放在远端,十分必要的状况下再放本地
- 定期资源状况检测:定期自动化进行全APP的资源摸查,问题追溯
- 定期代码检测:定期自动化的进行全APP的代码摸查,无用代码下线
- 和UED独特推出图片动画动效资源应用标准,规定哪些能够在本地,哪些必须远端,以及动效的优化计划
后果
在通过一段时间的各种优化后,云音乐的装置体积降落87MB,从原先的420MB+升高到当初的330MB+,整体感官上还是有区别的,下载体积降落65MB,冲破了200MB的苹果OTA限度,达到了160+MB。
相干材料
- What is app thinning?
- Asset Catalog Format Reference
- pngquant
- webpmux
- Code Size Performance Guidelines
- 从 Exported Symbols 利用于包大小优化说到符号绑定
- LLVM Link Time Optimization
- Reducing Code Size Using Outlining
- Interprocedural MIR-level outlining pass
本文公布自网易云音乐技术团队,文章未经受权禁止任何模式的转载。咱们长年招收各类技术岗位,如果你筹备换工作,又恰好喜爱云音乐,那就退出咱们 grp.music-fe(at)corp.netease.com!