本文作者:大鹏

  云音乐iOS客户端是自2013年开端的老项目,经历近十年的事务翻滚开展,从单体音乐APP开展至今,多种事务加持,俨然现已成为类似于渠道级的巨型APP,而且包体积也跟着事务的开展越来越臃肿,影响用户的实践体会,甚至是品牌的口碑,在笔者开端优化之前云音乐在AppStore显现的包体积现已到达了420MB之多,在这种状况下,团队敞开了包体积优化的专项。

  包体积优化是客户端开发的老命题了,基本上作为iOS开发同学多多少少都了解大体该怎样做,但跟着苹果的开展,一些本来可行的办法在新版别现已不在适用,所以本篇文章则侧重于优化过程中的一些最新的实践经验,以及在大项目中是怎样落地的,那么话不多说,下面就开端。

口径

  在开端做优化之前,咱们首要需求摸清楚包体积的各种口径以及它们之间的联系,由于后续的一些优化办法会导致不同口径此消彼长的状况,所以首要要确认终究方针口径是什么。 首要,咱们能够在苹果后台看到自己APP详细的装置巨细和下载巨细的详细状况,还包含了不同的机型版别。

如何让云音乐iOS包体积减少87MB

  那么苹果后台的下载巨细和装置巨细是怎样生成的呢,请看下图,在上传后,苹果官方对对咱们上传的IPA包解包后,对二进制进行了DRM加密(此项也会导致包体积的增长)和AppThinning,AppThinning会依据不同的机型对原始包的资源和代码进行不同程度的裁剪,然后生成适配详细机型的版别。此外苹果还会生成一个包含全集的通用版别,但并没有啥实践用处。关于DRM和AppThinning此处不打开,文章结尾有链接。

如何让云音乐iOS包体积减少87MB

如上图所示,在上传前后咱们有三个方针:

  • APP原始包体积: 上传前IPA解包后,实践APP的巨细
  • 下载体积: AppStore中流量下载时提示框的巨细
  • 装置体积: AppStore中APP概况中显现的巨细

在摸清了各方针的联系后,咱们终究挑选了用户感知最强的装置体积作为核心方针,以其作为终究方针进行优化。

剖析

  尽管现已确认了方针,但在优化之前,还需求对现状进行剖析,找到最大的劣化点,然后有针对性的进行优化,获取最大的收益比,那么下图便是云音乐iOS包的基本状况,能够看到赤色的资源部分占到了一半以上的体积,而二进制则次之也占到了四分之一多,那么后续优化的侧重点能够放到资源和二进制源码

如何让云音乐iOS包体积减少87MB

资源

  关于资源的处理其实办法便是常规那么几种:资源整理、资源整理、资源紧缩、资源云端搬迁、资源合并等等,总之便是想尽一切办法去下降资源所占用的本地空间,下面简单介绍下咱们在云音乐所做的作业。

资源整理

  在开端做整体的资源优化之前,第一步是需求整理现已不在运用的资源,包括图片、配置文件、音视频等等,检测无用资源的首要思路便是通过静态检测判别资源是否有被引证,例如运用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

如何让云音乐iOS包体积减少87MB

如何让云音乐iOS包体积减少87MB

  从上图能够看到,关于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。

动态库依靠裁剪

  除了事务代码的处理,自身云音乐也依靠了一些动态库,而且这些动态库由于历时原因,有些静态依靠是重复的,详细如下图所示:

如何让云音乐iOS包体积减少87MB

这是比较极点的一个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文件里指明详细要留下的符号,编译器就会裁剪掉未声明的部分。

如何让云音乐iOS包体积减少87MB
如何让云音乐iOS包体积减少87MB

下图为敞开后被裁减掉符号段

如何让云音乐iOS包体积减少87MB

值得注意的是,假如APP运用了Firebase,则不能悉数裁剪掉,会导致Firebase发动不成功,然后无法获取Crash信息,原因是Firebase依靠上图Export Info中的__mh_execute_header这部分符号,所以能够在上文提到的txt文件中参加__mh_execute_header,则编译器在裁剪时会保留__mh_execute_header的部分。

  此项为链接期优化,只需主工程敞开即可,云音乐在敞开后,此项收益是2.4MB。

Link-Time Optimization

  LTO的优化首要体现在跨文件的废弃代码裁剪优化、永久不会履行的空逻辑优化、内联优化,意思是直接复制函数,削减内联层级,提高函数栈的履行功率和空间利用率。概况请查看LLVM的官方文档,此处不在赘述。

如何让云音乐iOS包体积减少87MB

  其他通过测验验证了LTO只对静态言语收效,OC是动态言语,一切函数办法有可能在运转时被动态调用,所以是不可能裁剪的,这便是为什么在链接静态库时,假如是C库,那么看起来本来二进制很大,实践上被实践链接进来的只有真实运用的小部分,可是假如是OC库则基本上悉数会链接。所以假如你的APP源码中C或许C++代码较多的话在此项上收益可能会大一些。

  尽管LTO称号看起来是链接期优化,但实践上是编译期也需求参加的,否则会没有作用,这和跨文件的优化有关,在编译期就要产出部分信息,供给链接期优化运用。

  通过LTO的优化,云音乐取得的收益是1MB。

GCC_OPTIMIZATION_LEVEL

  此项意通过更急进的GCC编译优化,然后发生更低的二进制产物,Xcode默许是Debug设置O0,Release设置为Os,但其实还能够运用Oz模式,然后到达更小的体积。

如何让云音乐iOS包体积减少87MB

  其实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,可是这个迭代的劣化到达了45MB,所以咱们不得不在管理的同时就敞开防劣化的作业,咱们拟定了一些防劣化办法,其间一部分现已上线,剩余的还在开发中,现在现已取得了很好的作用,体积的劣化状况现已得到了比较有效的遏制,也保住了优化的成果,详细办法如下:

  • 大资源卡口:在代码合入时进行资源检测,并强制卡口
  • 二方库三方库卡口:在代码合入时进行二方库三方库的检测,包含新增和晋级
  • 自动紧缩:关于资源合入进行自动紧缩,但首推仍是放在远端,非常必要的状况下再放本地
  • 定期资源状况检测:定期自动化进行全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!