随着移动应用的业务场景越来越丰富,功能日趋复杂,App的安装包体积也在逐渐增大。根据谷歌对安装包大小对下载转化率的分析,一个包大小每增长6MB,下载转化率就有1%的降低。除了下载转化率,下载成功率也跟包大小成反比。因此对包进行瘦身很有必要。
此外,在苹果的AppStore,安装超过200M的应用,需要用户授权才能使用蜂窝网络下载,所以臃肿的包体积无形中也为App的下载转化带来了困难。
网易严选也始终从持续优化用户体验的角度出发,坚持不懈的对App客户端瘦身进行探索和实践,同时借鉴了网易集团内部各兄弟产品,如网易云音乐、网易大神等产品客户端优秀的瘦身经验,持续的对网易严选App客户端进行瘦身。
在网易严选6周年庆来临前夕,我们又一次对严选iOS客户端进行了专项瘦身,这次瘦身成功将严选iOS App的包体积从210M瘦身到了144M,使得整个包体积低于AppStore设置的200M红线,从而用户可以无需额外开启蜂窝网络授权的情况下直接下载。
要对App安装包进行瘦身,首先需要清楚的认识App安装包是有什么组成的,哪些东西为其大小做了贡献;然后分析能够通过什么手段、策略来安全的减少安装包的体积,在减少体积的前提下,他们有各自会带来什么问题;最后,在App瘦身之后,应该如何使其长治久安。本文主要通过以上三个方面来介绍iOS App瘦身的一些实用指南和最佳实践。
在进入App瘦身的主题之前,让我们先来了解一下iOS App包的组成结构,到底那些东西为App的体积增长做了“贡献”。
上传AppStore的包是一个ipa文件,解压后得到.app文件,可以通过查看包内容的方式看到,一个应用程序安装包主要包括以下几个部分。分别是:应用程序二进制文件本身、应用程序扩展、嵌入的各种Framework,以及大量、各种类型的资源文件。一个完整的安装包,涵盖的内容,大体上可以如下图所示。
所以App瘦身,是一个系统工程,针对不同的组成部分,有不同的方式和工具来瘦身,对应的,不同部分的瘦身难度和收效也不尽相同。
针对可运行的二进制文件(包括主程序和各种扩展程序),主要是采用增加编译优化选项、删除无用类、减少第三方库的依赖等方式,对二进制产物大小进行控制。第三方库依赖的减少、无用类的删除同样对提升应用程序的启动速度大有裨益。二进制文件大小控制的好坏,更多取决于团队的日常维护和开发习惯,比如是否定期维护废弃代码、裁剪第三方库以及代码结构设计的是否合理等。但总的来说,在较好的控制开发质量的前提下,通过对二进制文件的瘦身,并不会对包的大小带来本质的改变。
随着应用的迭代,越来越丰富功能和资源管理腐化,可能会带来越多越多的内置资源,这些都是导致安装包体积增长的重要因素。需要我们通过严格控制无效资源、平衡资源质量和效果,以及采用on-Demand Resources、Slicing等方式减少应用资源的体积。
接下来具体分项介绍一下App安装包瘦身的一些实践思路。
在App Slicing技术推出之前,不同硬件属性(CPU指令集、分辨率、硬件资源)设备上安装的App包都是一样的,所以为了达到能够在所有设备上都运行这个App的目的,App安装包必须涵盖能够支持所有类型设备的所有资源,导致App安装包变得逐渐臃肿。这种模式如下图所示。
随着CPU、GPU等的硬件的加速迭代和苹果设备的推陈出新、品类扩充,这一现象变得更加突出,包的增长已经出现了不可控的趋势。于是苹果推出了App Slicing技术。
这一技术的基本原理如下图所示。
在开发阶段,通过使用Assets来管理图片等资源,而不是直接放在bundle里。
由此,当打包出来ipa上传到App Store Connect之后,App Store Connect会根据所支持的不同设备生成多种“变体”(variant),每个变体中只包含其所支持设备类型需要的资源,如针对高分屏的是3x图片,而抛弃其他的2x图片。基本流程如上图所示。具体生成的安装包示意图如下:
Bitcode是基于LLVM编译体系,在编译过程中,前端部分生成中间代码(Intermediate Representation,简称IR),而不是特定平台的(如arm64、x86)的二进制机器码,最终在针对不同的运行平台(如arm64、x86)生成特定指令集的二进制机器码。
在Bitcode引入之前,同一个应用程序安装包,需要包含其所支持的所有平台的二进制可执行文件,无形之间增大了安装包的体积,并且这种代价是毫无意义的。引入Bitcode之后,Xcode提交应用市场的安装包将不再是最终的二进制可执行文件,而是中间代码形式的产物,App Store Connect针对不同的架构进行编译,从而为每种特定的类型生成专有的二进制可执行文件,从而降低安装包体积。
在我们的工程中,如何使用Bitcode这一功能呢?自Xcode 7开始,Bitcode处于默认开启状态,具体开启或关闭路径为:Build Settings --> Enable Bitcode --> 设置为YES/NO
减少安装包大小。最终安装到具体架构的App是当前架构下的二进制包,没有冗余的其他平台文件,能够减小App安装包的大小。
编译结果平台无关性。Bitcode带来的最大好处是提交产物具有平台无关性,可以兼容未来新架构平台的出现,只需要App Store Connect针对新的架构进行编译,即可轻松支持,不需要开发者付出时间去适配。
Bitcode在带来好处的同时,也引入了一些额外的工作。
第三方库依赖。由Bitcode的特性决定的,如果项目要使用Bitcode特性,就要求工程所依赖的所有第三方动态库、静态库都支持Bitcode,否则在App Store Connect将无法生成对应平台的二进制产物。这一限制,也为Bitcode的引入和普及带来了不小的阻力,一些陈旧的第三方库已经不再更新,需要开发者自己从源码层面重新编译、引用来解决这一问题,这无形中对团队的技术能力提出了更高的要求。
dYSMs文件生成。由于最终的可执行文件是在App Store Connect生成的,所有dYSMs也无法在本地打包时生成,需要在App Store Connect下载针对不同平台的dYSMs文件,这对已经存在上报dYSMs文件的CI流程是一种破坏,需要开发者付出额外的工作来做适配。
简单来说,On-Demand Resources是一种按需加载资源的策略,通过将非常用的资源留在App Store服务器上,在App需要的时候再加载到本地的策略,来实现二进制文件及核心必要资源与非必须及不常用资源的分离的方式,来实现大幅度减少App安装包的目标。On-Demand Resources支持的资源类型和集成方式如下所示:
On-Demand Resources的基本原理如下所示。
On-Demand Resources的生命周期如下图所示,用户在安装App的时候,只需要从App Store下载运行必须的二进制可执行文件以及必要资源(如icon等),确保应用程序可以运行即可。在使用过程中按需下载资源,下载后的资源缓存在本地。
On-Demand Resources为用户带来了以下一些明显的好处:
更小的安装包体积
资源延迟加载
资源远程存储
应用内购买资源按需加载
使用On-Demand Resources,需要在Build Settings中设置开启Enable On Demand Resources标志,如下图所示。
On-Demand Resources是通过标签来管理资源的,它支持三种标签,分别是Initial install,Prefetch tag order,Dowloaded only on demand。
Initial install 标签的资源,会随着App本身在安装的时候从App Store下载到用户设备上。
Prefetch tag order 标签的资源,会在App下载之后,按照资源设置的顺序,逐个在后台下载的用户设备上。
Dowloaded only on demand 标签的资源,会在需要的时候触发下载,下载时机由开发者控制。
On-Demand Resources在实际应用中为App包的体积减少可以带来非常可观的收益,只是对资源标签类型和是否需要按需加载的设计与配置,需要精心考虑,避免影响用户体验。
在完成苹果官方给出的优化方案之后,接下来就可以对我们的应用程序本身进行瘦身优化了。首先从可执行的二进制文件开始,Apple的可执行文件是Mach-O格式的二进制文件,可以使用iSee工具分析查看可执行文件的组成情况。如下图所示,在iSee工具的分析结果中,可以查看各个库的空间占用情况,也为我们优化提供了思路和方向。同时这款工具也会分析未使用的类和未使用的方法,这些都为我们后续清理废弃代码提供过了一定的帮助。
要使用iSee工具进行分析,需要使用到Xcode编译期间生成的linkMap文件,再结合可执行二进制文件才能进行分析,默认情况下linkMap文件的生成是关闭的,开启需要按如下设置。
工程编译Optimization Level选项的配置,对最终生成的包体积是有影响的。
这里的Optimization Level设置有一下几种选项:
O0: 不进行优化,Debug 下默认开启。
O1: 小幅度优化包体积和执行效率。关闭 Strict Aliasing,块重新排序,块间调度。
O2: 开启所有不会引起空间换时间的选项。循环展开,函数内联等选项会关闭。
O3: 开启 O2 所有选项,并且开启函数内联,寄存器重命名。
Os: 开启 O2 中所有不会明显增加包体积的选项,执行速度和包体积都有保障。
Ofast: 开启 O3 中所有选项,并且可能会开启一些违反语言标准的选项来保证最快的执行速度。
通常实践中,在发布的安装包中,在包体积与运行效率的考量之下,选择-Os的配置比较理想。Debug模式下,通常需要选择-O0配置。
No Optimization[-Onone]: 不进行优化,最快的编译速度
Optimization for speed[-O]: 对代码执行速度进行优化,会增加包的大小
Optimization for Size[-Osize]: 对包大小进行优化,最小限度减少执行效率
Single File: 单文件优化,增量编译,多核 CPU。交叉引用。
Whole Module: 模块优化,最大限度优化整个模块,不存在交叉引用的问题。
在实践中,-Osize 配合 Whole Module 发挥效果最好,能减少 5%~30% 可执行文件的大小,影响的执行速度在 5% 左右。包大小和执行速度之间,需要根据具体应用的场景进行权衡取舍。
开启该选项,对C/C++/Swift等静态语言的实现包大小有较大帮助,该标志位可以控制在编译时将运行中不可达的模块排除在编译结果中,从而降低包的体积。但是该选项对Objective-C无效,因为OC是动态语言,器模块可以动态调用,哪些模块会被调用,可以在运行时再决定。
在日常开发过程中,为了避免重复造轮子的资源浪费,往往需要引入大量的第三方库实现一些基础或者特殊功能,随着项目的不断深入,第三方库的管理可能会陷入混乱,导致包的体积逐渐膨胀。主要有以下几种原因可能导致安装包体积变大。首先是不再使用的第三方库没有得到及时清理,一直驻留项目中。其次是为一个很小的功能引入一个庞大的库,导致大量的空间浪费。第三是不做合理的技术选型,盲目的引入大量的第三方库。
解决三方库导致的包体积膨胀,更多的是靠良好的编程设计习惯和团队规范的技术标准,以及定期的依赖巡查。
首先,在引入第三方库是需要做出合理评价,有些功能,只是需要付出一点代价就可以靠自研解决,避免引入第三方库的同时也保证里实现的可控性和扩展性。
其次,目前很多成熟的第三方库,都会用sub module的形式来管理,在引入时可以考虑只引入自身需要的sub module,避免不必要的空间浪费。
第三,如果是自身团队或者兄弟团队产出的Framework,在实现可控的情况下,可以推进使用XCAsset来管理Framework中的资源,享受App Thinning Slicing带来的益处。
第四,规范团队第三方库管理流程,定期检查清理无用库或者更新实现,降低包体积。
第五,对于一些可以拆包的第三封库,可以考虑通过技术手段拆分后,取用对项目有用的部分,从而达到库裁剪的效果。
最终的可执行二进制文件,都是有代码编译而成的,所以从源头上减少代码量,也就会为最终产物瘦身打好基础,除了程序员良好的程序设计和编码习惯外,腐化治理也是减少无意义代码量的重要途径。
随着项目的不断演进,会沉积下大量废弃的类和方法,这些类在业务场景下虽然不会再被用到,但是却会被编译打包进安装包中占用其体积。无论是出于减少包体积的需要还是防止代码腐化,都需要对无用代码进行清理。
但是,OC是动态语言,是可以通过运行时动态创建类和调用方法的,所以很难通过静态调用关系来确定那些类和方法没有被使用。
扫描复用代码主要有以下几种思路:
基于 Clang AST 扫描
基于 Mach-O segment/section 查找
基于源码扫描: fui
使用 AppCode 扫描无用代码:无用方法/无用类/无用 import,缺点是无法扫描动态调用
举例来讲,可以通过一款fui工具,对代码进行静态扫描,发现一些未被引用的模块,从而以此来处理无用代码,但是一定要注意,OC是动态语言,静态扫描的结果只能作为指引和参考,每一个文件都需要 人为排查。
工程中的重复代码也是导致包体积增大的一个因素(虽然不至于增加太多),由于不好的开发习惯,或不理想的模块划分,可能会导致程序员大量的拷贝代码在不同的模块中实现同样的功能,日积月累,可能会导致大量的重复代码。
日常开发中,我们可以手工使用或者在持续集成流程中使用PMD,经常性的监测我们的代码重复率,并重点监控新增代码的重复情况,从而实现长期的低代码重复率。
像陈旧代码一样,长期没有维护的资源图片会出现很多无用资源,随着业务的迭代,大量的资源图片被废弃但是没在工程中移除,这些资源都会被带到安装包中贡献其体积。日常中可以通过一些好用的工具,如LSUnusedResources来定期查看检索无用资源,并其进行清理。
这里也需要注意,比如在严选的工程中,前期设计的资源使用框架,在引用图片资源时,并未直接使用资源名称,从而导致工具会误判,针对这种特殊情况,可以通过修改工具源码适配自己框架的特性,也可以在无用资源量不大的情况下人工排查。
FengNiao是一款还在持续更新的iOS工程无用图片资源探测工具,是一款命令行工具,在我们的日常开发中,建议针对FengNiao进行二次开发,配合团队自身的图片命名规范进行修改、定制,将其放入到编译的Run Script流程中,显示的提示Warnings,从而做到无用资源出现的实时反馈。
另一方面,为了达到最佳的用户体验,设计师往往会给到最佳质量的资源。但是,这些资源往往码率极高,已经完全超过了移动设备所需要的极限质量,不仅拖慢了应用的运行速度,同样也影响了App安装包的体积。所以在实际工作中,我们需要对资源进行合理的压缩,在保证最佳的用户体验基础上,尽量减少资源的体积。
针对PNG图片,我们可以使用pngcrush、TinyPNG等工具对PNG图片进行压缩后在使用,在严选的iOS客户端项目中,我们引入了自研监测工具,对图片资源大小进行监测,在使用TinyPNG工具对大型的图片做合理的压缩,从而减少包的体积。
资源优化对比如下:
仅仅是通过资源压缩,就节省出了接近20M的空间。
针对JPG文件,可以使用ImageOptim文件进行适当的压缩。
在图片格式的选择上,随着严选iOS 客户端对webp格式的全面支持(静态、动态图),目前严选在使用的所有远程图片资源都已经使用了webp格式,接下来,我们会考虑将本地资源图片也全部替换成webp格式,从而更进一步降低图片资源的体积。
此外,如果应用是使用到了视频资源,一定更要对其进行恰当的管理。首先需要控制视频的长短、码率,从而降低视频的体积,对于无法有效降低体积的视频资源,需要考虑使用On-Demand Resource等策略实现预期、懒加载等技术方案来解决。
出于下载转化率和用户体验的考虑,严选App从项目之初就很关注包体积问题,相关的编译器优化一直开启在最大化平衡包体积和运行效率的配置组合上。
近两年随着业务迭代和技术演进,严选App在包体积控制上也一直在不断的跟进。
在腐化治理方面,及时处理废弃代码及相关库。随着技术转型,在容器化方面废弃weex,裁剪掉weex相关SDK及功能逻辑实现,共计为包体积减少41M+。
在资源优化方面,通过对App的UI层所需资源进行裁剪压缩、使用XCAssets方案,共计为包体积减少了19M+。清理废弃品宣占位图等资源减少5M+,非必须切实时需求的大资源,全部通过技术手段实现线上化。H5等相关资源通过Prefetch等预加载能力(类似On-demand Resources策略)减少打包内置资源2M左右。
综合各项包体积优化的实践,近年来严选App包体积共计优化了67M+,优化比例在31%左右。严选App后续还会在Bitcode、On-demand Resource策略、腐化治理等方面持续的深耕包体积优化,确保持续的提升用户体验。
包体就优化前后的对比如下:
在掌握了App安装包大小的构成,以及体积优化的技术和方向之后,更重要的是如何保持安装包体积在合理范围内,以下是严选项目中的一些实践和思考,以及未来要更进一步深化的方向。
在严选的客户端研发流程中,我们已经实现了完整的、闭环CI/CD流程,这套流程确保了我们团队日常研发中的规范、质量保证和持续集成。
在实际开发中,任何MR都会经过严格的CI流程,在这一套流程中,针对包体积的管理,我们增加了无用代码监测、重复代码报警、无用资源监测、包体积变化阈值报警等机制,做到了实时监测影响包体积的一些重要因素,从而确保包体积瘦身的长治久安。
在未来,我们会考虑充分利用App Thinning技术,最大化安装包体积的优化。
在我们的工程中,已经大量使用XCAssets来管理资源,享受到了一部分App Thinning带来的益处。但是,基于一些历史原因,严选iOS客户端暂时还无法完全利用其Bitcode带来的好处,这也是我们未来努力的重要方向。
此外,基于电商场景,各种资源(比如定期的大促等场景)可以考虑使用On-demand Resources技术,实现动态的利用资源同时降低安装包体积的目的。
OS X ABI Mach-O File Format Reference
(https://github.com/aidansteele/osx-abi-macho-file-format-reference)
Shrinking APKs, growing installs
(https://medium.com/googleplaydev/shrinking-apks-growing-installs-5d3fcba23ce2)
What is app thinning? (iOS, tvOS, watchOS)
(https://help.apple.com/xcode/mac/current/#/devbbdc5ce4f)
App Thinning in Xcode
(https://developer.apple.com/videos/play/wwdc2015/404/)
On-Demand Resources Essentials
(https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/On_Demand_Resources_Guide/)
Doing Basic Optimization to Reduce Your App’s Size
(https://developer.apple.com/documentation/xcode/doing-basic-optimization-to-reduce-your-app-s-size/)
Create asset catalogs and sets
(https://help.apple.com/xcode/mac/current/index.html?localePath=en.lproj#/dev10510b1f7)
无用资源检测
(https://github.com/tinymind/LSUnusedResources)
clang - the Clang C, C++, and Objective-C compiler
(https://clang.llvm.org/docs/CommandGuide/clang.html)