我们平台就包体积问题已经做过多次优化,累计优化达到42MB+,截止到2.0.22版本,包体积是92.1MB,在行业同类APP中处于领先。包体积优化话题老生常谈,但随着苹果系统的不断迭代,优化方式也在变化,本文是基于当前的一些实践总结,下面就从统计口径、治理思路、具体的实践来讲述包体积工作如何开展。
iOS工程从打包到生成ipa-》下载包-》安装包的流程如下:
综上,我们选择了安装大小作为优化统计的口径。此外,我们可以在AppStoreConnect的TestFlight->构建版本->构建版本元数据->APP文件大小中查看经过APPThinning后的各个变体版本大小。
1.APP文件大小的限制
2.AppStoreOTA下载大小限制
Appstore对使用蜂窝网络下载有限制,若下载大小超过限制,iOS13之前无法使用蜂窝网络下载App,需通过Wi-Fi网络下载;iOS13之后默认会弹框让用户选择是否下载。如下为苹果历年来对App蜂窝网络下载限制的变化:
我们部门的APP是2021年起步,拉新是重要的工作之一。2017年GooglePlay的一个研究结果是包体大小每上升6MB,应用下载转化率就会下降1%,因此我们的目标是跟行业头部的APP中对比维持较低的包体积。
由于包体积大小随着功能迭代中增加删除代码、图片、资源文件而发生变化的,所以包体积治理也是一个动态的过程。我们的治理思路是,对包体积情况分析分多期治理达到目标,并且在功能迭代中持续监控避免劣化,小幅的波动待累计后周期性做一次优化。
在进行优化前,弄清楚ipa安装包里面有哪些内容很重要。首先对ipa包进行解压,解压后对文件进行归类统计,然后针对各个类型的文件发现和思考优化空间。如下是某个版本的各部分文件占比情况:
由上,结合包体积优化的一些资料,我们大体确定了以下几个思路:
包体积优化的方式有很多,以下是我们综合考虑ROI、风险、用户体验等因素的一些实践,从Mach-O优化、资源文件优化、编译设置优化3个方面讲述。
安装包中包含的Mach-O文件包括主工程可执行文件、动态库Mach-O文件。Mach-O文件是根据代码生成的,所以从以下几个方向优化。
#1.Podfile中可以配置库只在debug下集成,#在release下打包时不会嵌入到安装包中,避免包体积增大target'User'dopod'UICompare','0.1.5',:configurations=>['Debug']pod'FLEX','5.22.10',:configurations=>['Debug']pod'SwiftLint','~>0.43.1',:configurations=>['Debug']...end#2.如果有的库里面某个文件夹的内容没用到,也可以使用hook来删除pre_installdo|installer|#包体积优化去掉AFNetworking中没用到的UIKit+AFNetworking文件夹中内容system("rm-rf./Pods/AFNetworking/UIKit+AFNetworking")end2.重复代码整合比如Aspects库hook功能,有的库拷贝了一份,又因为要避免可能发生的符号冲突,将类名和文件名改写。
这种情况不容易查找,依赖开发者在需求开发过程中发现后先标记,在后续的包体积优化需求中考虑将文件下沉。
Swift工程中多个动态库依赖了相同的功能,各自把那些文件拷贝了一份在其内部。
注意:如果是OC文件,生成Pod动态库时也不会发生符号冲突;如果是Swift文件,有命名空间不会发生符号冲突。
这种情况下,如果是Swift文件,需要手动排查;如果是相同的OC符号在不同的Mach-O中(即主工程可执行文件和动态库的Mach-O文件),我们可以在调试运行时在Xcode控制台打印看到。推荐改法也是下沉到基础库中。
我们可以在ipa包中的Frameworks文件夹中查看所有的动态库,动态库相比静态库会占用更大空间;另一方面过多的动态库数量也会影响启动速度,苹果官方的推荐是不超过6个,所以我们将一些库改为静态库。
"网易云音乐包体积优化"的一个case:在主程序中、动态库A中、动态库B中分别有一份OpenSSL的符号,这就造成了重复,占用二进制体积。这种问题的解决方案就是动转静,把动态库转化为静态库,都链接在主程序中,解除原来的依赖,都使用主二进制中的Symbol。
使用hopper打开动态库我们可以看到AFNetworking和SDWebImage动态库里面有关一些基础的使用方法存在重复Name,却不同的Address,比如这个dispatch_once。
APP工程是组件化开发的,有100多个Pod库,经过分析后采用了两种修改的方式:
有的公司在全面改为XCFramework之后,看源码不方便,改代码后又需要生成新的XCFramework库版本,导致开发效率低,继而全面去除.....
综合考虑,我们这边的做法是:
使用Cocoapods管理集成三方库时,如果Pod库的resources集成方式不对,会带来的图片重复合并问题。
#经过排查,发现是PodSpec文件中资源的写法导致的#1、产生问题的写法s.resource_bundles={'Home'=>['Home/Assets/**/*']}#2、改为写法就ok了s.resource_bundles={'Home'=>['Home/{Assets,Classes}/**/*.{xcassets,png,xib}']}3.####图片压缩
使用了ImageOptim工具来进行70%的微量有损压缩。
有的图标可能会放在文件夹或者bundle中,但推荐把图标放到AssetCatalog管理,这样能从苹果AppStore瘦身机制受益。注意,打包生成的ipa可能会变大,需要在AppStoreconnect中查看变体版本的安装包大小才能看到实际收益。
编译设置优化时,建议在Podfile的post_install函数中设置主工程和Pod的编译设置,这样能让各个Pod库也生效,也能做到仅仅设置打包环境是生效不影响编译调试耗时!
通过GCC编译优化,产生体积更小的二进制产物,对OC、C、C++都有效果。
编译优化配置路径为:BuildSettings->AppleClang-CodeGeneration->OptimizeLevel
我们使用-Oz,即release下设置GCC_OPTIMIZATION_LEVEL=z;后可以执行包体积上的优化。
-Oz进一步说明:在Xcode11之后提供的编译优化参数,它通过识别单个编译单元中跨函数的相同代码序列来减少代码大小。这些序列在单个编译器生成的函数中被封装(Outlined)。每个原始代码序列都被替换为调用该Outlined函数。会减小相同代码存在多份问题,但是也会使得的函数调用存在更深的调用栈,对客户端性能较小。
由上,将工程和pod库的编译设置改为-Osize。
配合使用的还有CompliationMode设置,可以在release设置为wholemodule
Link-TimeOptimization(LTO)是一种编译优化技术,它在链接阶段对整个程序进行全局优化,而不仅仅是单个源文件。这意味着编译器能够在链接时看到整个程序的结构,从而进行更全面的优化。
具体来说,LTO执行以下主要任务:
提供以下几个选项:
我们在release下选择Incremental选项。开启这个优化后,一方面减少了汇编代码的体积,一方面提高了代码的运行效率。
一般情况下,iOS应用程序的主要逻辑和功能都是在主二进制文件中实现的。为了确保应用程序的安全性和完整性,主二进制文件中的符号信息应该是内部的,不应该直接暴露给外部。这样可以防止恶意用户或攻击者直接访问和调用应用程序中的敏感函数或变量。
Xcode中的SymbolsHiddenbyDefault控制是否将符号默认隐藏。设置为Yes,则Xcode会在构建时使用-fvisibility=hidden选项来隐藏所有符号。这意味着主二进制文件中的符号会被限制在当前编译单元内部可见,不会被其他模块引用。这是一种常见的做法,以确保应用程序的符号信息不会被外部使用。
但实际查看主二进制文件可以看到主二进制还是暴露了一些符号,所以需要我们需要通过设置EXPORTED_SYMBOLS_FILE为一个空的文件来解决。
在设置为"SymbolsHiddenbyDefault"为"Yes"后,可以使用命令行工具nm(namelist)来查看主二进制文件中的符号是否被限制在当前编译单元内部可见。
请按照以下步骤进行操作:
在输出结果中,你会看到不同类型的符号,例如函数、全局变量和局部变量。对于被隐藏的符号,可能会显示为U或u,这意味着它们是未定义的或局部的符号。这些符号在当前二进制文件以外是不可见的。
相反,如果符号显示为T(函数)或D(数据),则表示它们是可见的全局函数或数据。这些符号可能是由于设置为"No"或其他配置,而允许在外部模块中访问的符号。
需要注意的是,这只能提供关于符号是否被隐藏的信息,而无法提供详细的函数或变量名称。如果需要查看更详细的符号信息,可以使用专门的反汇编工具(如HopperDisassembler)来分析和查看二进制文件的内容。
总而言之,使用nm命令可以查看被隐藏的符号,并判断其是否被限制在当前编译单元内部可见。
StripLinkedProduct设置为YES可以去除不需要的符号信息。需要注意StripLinkedProduct选项在DeploymentPostprocessing设置为YES的时候才生效,而DeploymentPostprocessing在Archive时不受手动设置的影响,会被强制设置成YES。
结论:将DeploymentPostprocessing设置为NO,将StripLinkedProduct设置为YES。