扣丁书屋

正经分析iOS包大小优化

背景

包大小优化是项目开发中不可避免会遇到的,网上关于包大小优化的文章很多,每篇文章说的都不尽相同,笔者曾经根据网上的文章做过包大小优化,但效果不尽人意;因此笔者想根据已有的文章、知识结合自己的理解、实践,做一份总结梳理,整理自己的包大小优化逻辑,不光要知道怎么做可以让包大小变化,还要知道为什么这么做能产生效果,所以就有了这篇文章。

分析

想要优化安装包大小,首先需要弄清楚影响安装包大小的因素有哪些?之前笔者优化包大小直接闷着头就去瘦身,瘦来瘦去也没瘦出个名堂,而且还跟别人说不清楚自己做了什么,为什么这么做?

后来总结出来,做事之前要先思考、分析、最后再去做,要思考的是影响这件事的因素有哪些,一一列举出来,查漏补缺;然后针对这些因素进行分析,分析哪些因素是人为可控的,哪些因素是不能改变的,针对可控的部分要怎么优化,不可控的部分是否能避免,最好可以使用思维导图工具,一一记录;然后按照思维导图的整理出来的数据,按步骤去执行。

回过头来,针对安装包大小,首先分析影响安装包大小的因素,有:Xcode的设置、资源、代码三个方面。那针对这几个方面要怎么优化?以及如何查看每一步优化的结果?

首先是怎么优化的问题:

Xcode的编译设置优化,Xcode设置影响的是生成包的大小,通过Xcode编译选项优化的设置,让生成的ipa包变小,比如不含断点调试、去掉异常支持等等。

资源文件的优化,资源不光有图片资源,也包含代码资源和其它导入的资源,可以通过分析安装包构成,看里面哪些部分比较大、不合理,从而进行优化。

代码的优化,通过Link Map生成Link Map File,分析Link Map File各文件占用,结合Mach-O文件进行分析优化。

然后是怎么查看每一步优化的结果的问题:

查看每一步的优化结果,可以通过分析打包出来的ipa的大小,以及ipa的组成,与初始的ipa包大小比较,即可直观得到优化的结果。但可以更进一步,分析ipa的构成,对比优化后的构成,看每一步的操作具体影响的是包的哪一块儿,从而导致包的大小发生了变化。所以先来看一下一个ipa的包包含哪些内容,然后每一步优化之后,对应ipa的哪一部分发生了变化?

安装包的构成

iOS打包出来的ipa,本质上是一个压缩包,所以可以将.ipa的后缀改为.zip,然后进行解压缩,之后会得到一个Payload文件夹,里面又一个xxx.app的文件,这个xxx.app就是包含所有文件的包了,选中xxx.app,右键显示包内容,即可看到里面具体包含的东西了,大致如下:

安装包:

  • _CodeSignature:ipa包签名文件
  • .lproj: 语言文件
  • Frameworks: 第三库、SwiftSupport库
  • Plugins: App创建的扩展,比如:Widget、Push、分享
  • Assets.car: 由Assets.xcassets生成的资源文件,里面包含各种分辨率的图片
  • embedded.mobileprovision:证书配置文件
  • Info.plist: 项目配置
  • exec格式的xxx: 可执行包
  • 其它资源文件
  • .mp3格式的文件
  • .html的文件
  • .json的文件
  • .png或者.jpg的文件

示例如下,Ps:不得不说,笔者这个ipa包含的内容真的是很全面了,各种的资源都有,哈哈哈

笔者初始ipa包大小为22.9M,解压缩之后.app的大小为57.1M,其中各部分大小明细如下

内容 大小
_CodeSignature 93 KB
.lproj 4 KB
Frameworks 37.5 MB
Plugins 181 KB
Assets.car 4.9M
embedded.mobileprovision 8KB
Info.plist 6 KB
exec格式的xxx 13 MB
其它资源文件 1.4 MB

了解了iPa包的组成之后,我们再回过头来,按照Xcode的编译优化、资源文件优化、代码优化的步骤来一步步分析。

Xcode编译设置

一般这一步容易被人忽略,因为提到优化最先能想到的就是资源优化,比如图片压缩、无用代码删除等等,而对于Xcode自身的编译优化提及的反而不多。而且由于网上提供的参考针对每个项目可能结果都不一样,有些编译选项的设置是需要针与实际项目结合起来才可以,所以笔者这里整理和总结了一下:

Xcode编译优化相关:

  1. Build Settings中去掉异常支持,Enable C++ Exceptions和Enable Objective-C Exceptions设置为NO,Other C Flags添加-fno-exceptions;

注意:Enable C++ Excptions和Enable Objective-C Exceptions是指项目支持对错误的异常处理,比如try catch、throw之类的;所以如果项目中使用的有类似的异常处理的,这个关闭了之后会报错(Cannot use '@try' with Objective-C exceptions disabled)。包括宏定义中使用的有try{}、@finally{}之类的,比如@strongify等,如果关闭了最后打包的时候也会报错。

-fno-exceptions的意思是禁用异常机制,参考gcc,同样,当项目中有try thorw的时候,就不要设置这个选项为NO

❝ Before detailing the library support for -fno-exceptions, first a passing note on the things lost when this flag is used: it will break exceptions trying to pass through code compiled with -fno-exceptions whether or not that code has any try or catch constructs. If you might have some code that throws, you shouldn't use -fno-exceptions. If you have some code that uses try or catch, you shouldn't use -fno-exceptions. 2. Build Settings -> Architectures,Release下设置为arm64

Architectures指定工程被编译成可支持哪些指令集类型,支持的指令集越多,就会编译出多个指令集代码的数据包,ipa包就会变大。默认的standard architectures(armv7,arm64) 参数,打的包里面有32位、64位两份指令集。如果不需要32位的,可以在other中更改支持的指令集,从而使ipa包变小。

❝armv6: iPhone, iPhone 3G, iPod 1G/2G armv7: iPhone 3GS, iPhone 4, iPhone 4S, iPod 3G/4G/5G, iPad, iPad 2, iPad 3, iPad Mini armv7s: iPhone 5, iPhone 5c, iPad 4 arm64: iPhone X,iPhone 8(Plus),iPhone 7(Plus),iPhone 6(Plus),iPhone 6s(Plus), iPhone 5s, iPad Air(2), Retina iPad Mini(2,3) arm64e: XS/XS Max/XR/ iPhone 11, iPhone 11 pro x86_64: 模拟器64位处理器 i386: 模拟器32位处理器

注意:Xcode 12之后,没有了Valid Architectures选项的设置。另:如果把Architectures从standard architectures(armv7,arm64)改为arm64,则会发现target无法选择模拟器运行了,所以建议Debug模式下修改为arm64、x86_64或者不修改,Release下设置为arm64。

更新:经朋友指点,这个地方Architectures的设置,还有另外一种设置方法,Architectures不修改,Excluded Architectures中设置Release模式下 Any iOS SDK -> armv7,也可以实现同样的效果。设置了之后,就是Release下把armv7的指令集排除在外。选中target会发现默认设置了 Any iOS Simulator SDK -> arm64,意思是模拟器的时候排除arm64指令集。

如下:

  1. Build Settings -> Generate Debug Symbols设置为NO

Generate Debug Symbols的意思是生成调试符号,当这个选项设置为YES时,每个源文件在编译成.o文件时,编译参数多了-g和-gmodule,意思是generate complete debug info,所以产生的.o文件会大,从而最终生成的可执行文件也就会变大。

❝Generate Debug Symbols (GCC_GENERATE_DEBUGGING_SYMBOLS), Enables or disables generation of debug symbols. When debug symbols are enabled, the level of detail can be controlled by the Debug Information Format (DEBUG_INFORMATION_FORMAT) setting.

注意Generate Debug Symbols设置为NO时,在Xcode中设置的断点不会中断,即不能断点调试。且最后不能生成DSYM文件,即使Debug Information Format设置了,也不能生成,因为首先要有调试信息然后才能生成DSYM文件,而设置为NO,意味着不产生调试信息,所以也就没办法生成DSYM文件。所以建议不要设置。

  1. Build Settings -> Deployment Postprocessing,Debug模式下设置NO,Release下设为YES

Deployment Postprocessing是Strip配置的总开关

官方解释:

❝Deployment Postprocessing (DEPLOYMENT_POSTPROCESSING), If enabled, indicates that binaries should be stripped and file mode, owner, and group information should be set to standard values.

注意:Deployment Postprocessing是Strip配置的总开关,只有这个设置为YES之后,下面的Strip Linked Product、Strip Debug Symbols During Copy的设置才会生效。

a. Build Settings -> Strip Linked Product,Debug下设置为NO,Release下设置为YES

对最后生成的二进制文件进行strip,去除不必要的符号信息,Release下可以为YES。

官方解释:

❝Strip Linked Product (STRIP_INSTALLED_PRODUCT), If enabled, the linked product of the build will be stripped of symbols when performing deployment postprocessing.

注意:如果Deployment Postprocessing不打开,该选项没有作用。去除了符号信息之后需要使用dSYM来进行符号化,所以需要将 Debug Information Format 修改为DWARF with dSYM file(Release下),如果在Debug下设置为DWARF with dSYM file那么在崩溃时将无法看到堆栈信息。

b. Build Settings -> Strip Debug Symbols During Copy,Debug下设置为NO,Release下设置为YES

文件拷贝编译阶段是否进行strip,设置为YES之后,会把拷贝进项目包的三方库、资源或者Extension的Debug Symbol去除。

官方解释:

❝Strip Debug Symbols During Copy (COPY_PHASE_STRIP), Specifies whether binary files that are copied during the build, such as in a Copy Bundle Resources or Copy Files build phase, should be stripped of debugging symbols. It does not cause the linked product of a target to be stripped—use Strip Linked Product (STRIP_INSTALLED_PRODUCT) for that.

注意:如果Deployment Postprocessing不打开,该选项没有作用

c . Build Settings -> Symbols Hidden by Default,Debug模式下设置为NO,Release下设置为YES

Symbols Hidden by Default会把所有符号都定义成"private extern",移除符号信息

官方解释:

❝Symbols Hidden by Default (GCC_SYMBOLS_PRIVATE_EXTERN), When enabled, all symbols are declared private extern unless explicitly marked to be exported using attribute((visibility("default"))) in code. If not enabled, all symbols are exported unless explicitly marked as private extern. See Controlling Symbol Visibility in C++ Runtime Environment Programming Guide.

5 . Build Settings -> Make Strings Read-Only设置为YES

复用字符串字面量

官方解释:

❝Make Strings Read-Only (GCC_REUSE_STRINGS), Reuse string literals.

6 . Build Settings -> Dead Code Stripping设置为YES

消除无效代码,C/C++/Swift 等静态语言编译器会在 link 的时候移除未使用的代码,对于OC等动态语言是无效的

官方解释:

❝Dead Code Stripping (DEAD_CODE_STRIPPING), Activating this setting causes the -dead_strip flag to be passed to ld(1) via cc(1) to turn on dead code stripping.

7 . Pod优化,如果项目是OC的,但CocoaPod中有使用了Swift库,打开了use_frameworks!,可以优化为针对单个Swift库使用use_frameworks!而不是全部第三方库都使用。代码如下:

  # Podfile
# 下面的这行代码还是打开,不需要注释掉
  use_frameworks!

# 之前的其它代码


# 然后添加下面的代码,xxx是要打包成framework的库
dynamic_frameworks = ['xxx']
pre_install do |installer|
    installer.pod_targets.each do |pod|
        if !dynamic_frameworks.include?(pod.name)
            def pod.static_framework?;
             true
            end
            def pod.build_type;
                Pod::BuildType.static_library
            end
        end
    end
end

在OC的项目中,Podfile如果引用了Swift的第三方库,一般都会直接打开use_frameworks!,对应的Pod中所有的库都会打包成动态库,以及Swift和OC库的依赖问题会导致依赖库增加,会造成包体积增大。

修改方法有两种:

这两种修改方法都可以显著减小包的体积。通过替代库可避免导入Swift相关的依赖基础库,且对应的use_frameworks!可以注释掉,相对来说,包会更小,但是改动比较大,需要把之前的库替换掉。而通过hook的方式,针对个别的库打包成动态库,其余的打包成静态库,项目修改比较小,包体积也能减小。具体采用哪种视具体项目情况而定。

注意:Podfile按照上面的修改了之后,需要重新使用Pod install命令更新,然后编译时可能会有报错。

  • 去除依赖的Swift第三方库,找对应的OC库替代;
  • 通过Podfile里hook的方式,改动态库为静态库

a . 如果报的错误是,'RuntimeError - [Xcodeproj] Consistency issue: build setting ARCHS has multiple values: {"Debug"=>"$(ARCHS_STANDARD)", "Release"=>["arm64"]}',可以先把Architecture中的值改为一样的,Pod install之后再改为不同指令集的。 b . 因为之前第三方库打包成framework的问题,如果遇到xxx framework not found的错误,在Build Setting中Other Linker Flags中进行修改,把对应的已经不是framework的库从其中移除即可。示例如下:

8 . Asset Catalog Compiler编译设置优化,Build Settings -> Asset Catalog Compiler - Options 中Optimization改为space

这个选项可以改变actool在构建Assets.car时选取的编码压缩算法,减少包大小。可以使用下面的命令检查Assets.car中图片的编码压缩算法。

  // 可以把对应信息生成.json文件,用于对比不同
 xcrun --sdk iphoneos assetutil --info Assets.car > Assets.json

比如笔者的设置了之后,对比查看信息如下,可发现压缩的算法不同,占用空间的大小也不同

9 . Build Settins -> Optimization Level改为-Oz

Optimization Level默认为-Os,-Oz是Xcode 11之后才出现的编译优化选项,核心原理是对重复的连续机器指令外联成函数进行复用,因此开启Oz,能减少二进制的大小,但同时会带来执行效率但额外消耗。可参考What's New in Clang and LLVM

在What's New in Clang and LLVM的Presentation Slides中,苹果给出了Optimization Level各参数优化的选择对比,如下图,对于性能要求高的,建议选择-O2和-O3,对于包大小敏感的,可选择-Os和-Oz,默认-Os是性能和大小平衡比较好的。最终选择什么,需要读者根据自己实际项目而定。 Optimization Level各参数对比:

官方解释:

❝Smallest, Aggressive Size Optimizations: This setting enables additional size savings by isolating repetitive code patterns into a compiler generated function. -Oz

Xcode编译设置优化总结如下:

优化结果

笔者最初项目大小最初为22.9M,设置了Pod优化之后大小缩减为21M,设置了Asset Catalog Compiler - Options 中Optimization为space之后,项目大小变为20.7M,再设置了上面其它Xcode编译优化之后,项目变为13.2M(笔者把Architecture设为arm64)

设置Pod优化之后各部分对比

内容 大小
_CodeSignature 67 KB
.lproj 4 KB
Frameworks 21.4 MB
Plugins 181 KB
Assets.car 4.9M
embedded.mobileprovision 8KB
Info.plist 6 KB
exec格式的xxx 19.2 MB
其它资源文件 1.4 MB

简单对比大小之后可以发现,Frameworks从35M减少到了21.4M,而exec文件从13M升到了19.2M,但总的ipa包大小变为了21M,比之前但22.9M降低了。

为什么会这样呢?还记得Frameworks文件夹里放的是什么内容吗?Framework中放的Pod中设置的第三方的动态库、以及Swift Support库。仔细观察Frameworks文件夹中的内容,可以发现,之前在这里面的第三方的.framework,除了指定的打包成动态库的第三方xxx、和Swift Support库还在,其他的都不见了。

还记得笔者改的是什么吗?笔者把Pod中第三方库从都使用动态库改为了个别使用动态库、其它使用静态库。因为动态库和静态库链接的方式的不同,动态库链接时不复制,在程序启动后用动态加载,所以是单独放在Framework文件夹下;而静态库是链接时会被完整的复制到可执行文件中。所以,于是就出现了这样的结果,Frameworks文件大小减少,而可执行文件大小增加。

设置了Asset Catalog Compiler - Options 中Optimization为space后对比

内容 大小
_CodeSignature 67 KB
.lproj 4 KB
Frameworks 21.4 MB
Plugins 181 KB
Assets.car 4.7M
embedded.mobileprovision 8KB
Info.plist 6 KB
exec格式的xxx 19.2 MB
其它资源文件 1.4 MB

对比之后发现,Assets.car中大小从4.9M减少到了4.7M,总的打包出来的ipa大小减少了0.3M,说明asset压缩有效。

设置了Optimization Level为-Oz后对比

内容 大小
_CodeSignature 67 KB
.lproj 4 KB
Frameworks 21.4 MB
Plugins 181 KB
Assets.car 4.7M
embedded.mobileprovision 8KB
Info.plist 6 KB
exec格式的xxx 18.9 MB
其它资源文件 1.4 MB

对比之后发现,exec的可执行文件大小从19.2M减少到了18.9M,说明优化有效,但总的打包出来的ipa大小并没有改变(上传到fir后,包小了0.03M),故而最终这个选项笔者最终没有打开。

Xcode其它编译设置优化之后对比

内容 大小
_CodeSignature 67 KB
.lproj 4 KB
Frameworks 11.1 MB
Plugins 83 KB
Assets.car 4.7M
embedded.mobileprovision 8KB
Info.plist 6 KB
exec格式的xxx 10.3 MB
其它资源文件 1.4 MB

对比大小之后可以发现,Frameworks从21.4M减少到了11.1M,Plugins从181KB减少到了83KB,exec文件也从19.2M减少到了10.3M。

同样为什么会这样呢?笔者设置了strip移除了符号信息,设置了打包只生成arm64架构指令集的包,所以Framework和exec都变小了,那Plugin呢?Plugin中是App创建的扩展,笔者针对通知扩展,也设置了只生成arm64指令集的包,所以Plugin也变小了。

优化总结

也许有人觉得笔者目前项目包没有太大,做这个优化是否有意义?在笔者看来,包大小优化应该是习惯,不是因为包大了才去优化,而是因为觉得有优化空间,所以才优化。打包出来ipa的包小,说明历史负担不重,船小好调头,编译打包速度也快,而且试错成本也低,恰恰是正应该优化的时候。优化总结出来的经验教训,落入文档,后续开发时能时刻注意,对开发来说是更好的。

资源文件优化

资源文件的优化,通常来说是比较简单的,但是资源文件的优化是需要持续进行的,前面介绍的Xcode编译设置优化,配置好了之后,后续开发过程中只要不修改配置,都无需重复关注。但资源文件不同,随着项目的迭代,会不断引入新的资源文件,不断有废弃资源的产生,所以资源文件的优化是要持续进行的。

资源文件的优化分为两部分,即:无用资源的删除、已用资源的压缩。在这里建议分先后顺序,即先做删除再做压缩,因为如果先压缩了,结果发现是无用资源,就白白浪费了力气。

无用资源的删除:

  • 已定义未使用的代码文件
  • 已废弃业务,代码还在
  • 已引用的图片但未使用
  • 某些重复资源导入

已用资源的压缩:

  • 项目中引入图片、网页、json、音频等文件的压缩

下面一步步的来实践:

无用资源的删除

随着项目的迭代,每个项目都会或多或少存在冗余。可能是开发了的功能未上线但产品让保留,保留着保留着就忘记了;可能是已下线的业务,没人通知到开发,于是代码逻辑一直都在;可能是删除某些业务代码时,对应的图片资源未删除;又或者是每个开发,导入各自熟悉的第三方库使用。

这部分的优化在笔者看来,也分为两步,一是预防,一是治理。笔者个人感觉预防比治理更重要。因为预防是前置,预防到位了,治理就会很轻松。

预防

上面说的场景,读者可能都遇到过,那要怎么做才能避免出现上面的情况,或者尽量少出现上面的情况?

笔者个人想法是通过规范的流程来,有一套规范的流程,按流程执行,避免出现信息不对等的情况。

产品和开发之间的信息不对等,会导致业务相关的冗余,产品知道具体业务的数据,而开发不清楚,所以可以通过定期同步给开发,让开发也能了解到对应业务是否活跃,从而及时对项目进行优化。

开发之间的信息不对等,会导致各自开发自己的,重复造轮子,所以可以通过建立公共文档,开发的流程规范、项目使用第三方库的规范、设计规范、代码规范都列举出来,每个人都能根据对应的文档了解到对应项目的信息,每个人开发都应该有一套统一的标准。这样就可以避免一人一套代码的问题。

具体的规范流程读者可以针对自己公司的实际情况来,可以思考一下,之前开发中是否出现了类似的问题,出现了之后是否有改变,怎么能避免再次出现同样的问题?

治理

针对无用资源的删除

  • 已定义未使用的代码

    可使用AppCode进行分析,打开AppCode,待索引完成后,选择顶部菜单中的Code->Inspect Code,然后选择范围,whole Project点击OK,等待AppCode静态分析即可。静态分析完以后,可以在Unused code里看到所有的无用代码。 AppCode中无用代码静态分析的类型有以下几种:

内容 大小
Unused class 无用类
Unused import statement 无用类引入声明
Unused property 无用属性
Unused method 无用方法
Unused parameter 无用参数
Unused instance variable 无用实例变量
Unused local variable 无用局部变量
Unused value 无用值声明
Unused macro 无用宏定义
Unused global declaration 无用全局证明
  • AppCode静态分析结果出来之后,删除前要经过确认,因为静态分析的结果可能会有误差,比如针对performSelector调用的方法就会被检测为没有调用。

  • 已废弃业务,代码还在

    需要梳理业务流程,结合线上业务数据点击量,同产品和业务确认对应功能是否下线,从而决定是否移除对应的业务模块代码。

  • 已引入未使用图片

    推荐使用工具LSUnusedResources,原理大致是遍历资源目录下后缀 ["imageset", "jpg", "png"...] 的文件,然后在源文件 ["m", "swift", "xib", "storyboard"...] 中字符串匹配,无匹配则是无用的资源文件。

    使用时注意勾选Ignore similar name,然后点击右上角的Browse选中要扫描的项目地址,点击右下角的search,就会开始扫描,结果会在底部Unused Results中展示出来,然后CMD+A全选,export,导出到一个文本文件中。也可以在对应单条Item上面双击,会打开对应的文件夹。建议删除前在项目中搜索确认,是否确实没有使用(类似字符串中间替换的可能会被扫描出来,所以删除前需要确认)

  • 某些重复资源的导入

    重复资源的导入,分为两个方面,一方面是针对第三方SDK,另一方面是项目文件。

  • 针对第三方SDK

    项目中功能类似的SDK建议保留一个,比如埋点统计的友盟、TalkingData等,线上日志分析的听云、Bugly等,又或者网络请求、UI布局的类库,建议分析相同功能的类库,结合实际情况,保留一个即可;另外,有些第三方类库导入时,可只导入实际使用的部分,不需全量导入,也是可以优化的地方。

  • 针对项目文件

    使用 fdupes 工具进行重复文件扫描,原理是:通过校验所有资源的 MD5,筛选出项目中的重复资源,文件比较顺序是大小对比 > 部分 MD5 签名对比 > 完整 MD5 签名对比 > 逐字节对比。来自:包体积大小:瘦身

    fdupes使用如下:

 // 1. 首先安装fdupes
brew install fdupes

// 2. 使用,其中xxx是要扫描的目录,yyy.txt是扫描结果输出的文件
fdupes -Sr /Users/.../xxx/ > /Users/.../yyy.txt 

输出结果类似于下面这样,通常相同文件可通过修改引用,仅保留一份源文件就可以。

WX20210505-103131@2x.png

已用资源的压缩

  • 项目中引入图片、网页、json、音频等文件的压缩

    网页的压缩指的是,放入APP资源中的js文件,最好是经过H5端压缩后的。

    json文件的压缩,如果不是打开APP时马上要用到的数据,可采取把对应资源放到服务端,下载后使用。

    音频文件的压缩,则是在可接受的范围之内,选择系统可支持的压缩比率高的格式。

    而最需要注意的是图片的压缩,图片的压缩,分为几个部分

  • Compress PNG Files 打包的时候自动对图片进行无损压缩

  • Remove Text Medadata From PNG Files 移除 PNG 资源的文本字符

  • resource_bundles:允许定义当前 Pod 库的资源包的名称和文件。用 hash 的形式来声明,key 是 bundle 的名称,value 是需要包括的文件的通配 patterns。CocoaPods 官方强烈推荐使用 resource_bundles,因为用 key-value 可以避免相同名称资源的名称冲突。同时建议 bundle 的名称至少应该包括 Pod 库的名称,可以尽量减少同名冲突。使用resource_bundles会为指定的资源打一个.bundle,所以获取图片时要注意指定bundle的位置:

    NSString *bundlePath = [[NSBundle bundleForClass:[self class]].resourcePath stringByAppendingPathComponent:@"/PAX.bundle"];
      NSBundle *resource_bundle = [NSBundle bundleWithPath:bundlePath];
      UIImage *image = [UIImage imageNamed:@"xxxx" inBundle:resource_bundle compatibleWithTraitCollection:nil];
    
  • resources:使用 resources 来指定资源,被指定的资源只会简单的被 copy 到目标工程中(主工程)。官方认为用 resources 是无法避免同名资源文件的冲突的,同时,Xcode 也不会对这些资源做优化。

  1. 单色图标、简单的功能图标,建议使用IconFont,矢量图标库的格式,既能统一规范格式,又能减少资源文件大小。圆角和阴影图片尽量代码实现。

IconFont的使用可参考笔者之前写的Demo——IconFont

  1. 针对普通图片,可以调用tinyPNG API进行压缩,这里可以使用笔者之前修改的脚本BatchProcessImage,调用的是tinyPNG的API可以一次性压缩500张图片,而且只需要指定项目目录,会自动压缩后替换原来的图片,不需要手动导入导出图片。使用可参考链接BatchProcessImage,需要注意的是注意python版本,python3和python,以及pip3和pip的选择,安装依赖库的时候使用的哪个python版本,最后调用的脚本命令的时候就要用对应的python版本。

另:如果项目图片超出500张,可以修改一下脚本文件,即:压缩过程中把压缩处理过的图片存储下来,然后第二次执行时,对于压缩过的不处理,就可以接着上次压缩到的继续压缩了。而如果需要对压缩过的再次压缩,只需要把存储下来的压缩过的图片名字清除即可。

  1. 放入xcassets里的2x和3x图片,在上传时,会根据具体设备分开对应分辨率的图片,不会同时包含。而放入Bundle中的都会包含。所以要尽量把图片放入xcassets中。但是,根据抖音品质建设 - iOS 安装包大小优化实践篇中介绍的,Assets.car编译过程中有时会选择一些图片,拼凑成一张大图来提高图片的加载效率。被放进这张大图的小图会变为通过偏移量的引用,建议使用频率高且小的图片放到Asset.car中,Asset.car能保证加载和渲染速度最优。但是大的图片(大于100K)就不要放入Asset.car中了。大的图片可以考虑将图片转成WebP。WebP是Google公司的一个开源项目,能够把图片压缩到很小,但是肉眼看不出来差别,目前iOS常用的图片显示类库都用支持该格式解析的拓展。可使用iSparta进行批量转换。

  2. 私有Pod库中的资源文件,建议在Pod库里面的Resource目录下新建Asset Catalog文件,命名为Images.xcassets,私有库使用的图片放入这里,然后手动修改该SDK的podspec,指定resource_bundles使用Images.xcassets。


    s.resource_bundles = {
        'xxsdk' => ['xxx/Assets/.../*.xcassets']
    }

Ps: Pod资源文件引用方式有 resource_bundles 和 resources,推荐使用 resource_bundles。参考深入探索 iOS 包体积优化

  1. 最后,是Xcode中关于图片压缩的设置,有时候压缩了图片之后,发现包大小并没有改变太多,可能是因为Xcode的Compress PNG Files选项的原因。建议如果自己对图片进行了压缩,就可以把Xcode的Compress PNG Files设置为NO。

资源文件优化总结如下:

优化结果

笔者项目的ipa大小从Xcode编译优化后的大小为13.2M,经过了资源文件优化之后大小缩减为10.3M。

资源文件优化之后各部分对比

内容 大小
_CodeSignature 67 KB
.lproj 4 KB
Frameworks 11.1 MB
Plugins 83 KB
Assets.car 2.4M
embedded.mobileprovision 8KB
Info.plist 6 KB
exec格式的xxx 10.3 MB
其它资源文件 952 KB

简单对比之后发现,Assets.car从4.7M减少到2.4M,其它资源文件从1.4M减少到952KB,说明资源压缩有效。

优化总结

见图,

监控机制?

瘦身完成之后,如何保证包大小不会再次迅速增大?就像减肥之后不会迅速反弹一样?就需要依赖适当的监控机制和合理的流程规范来控制。

监控机制保证实时发现问题,每次打包完成后,运行脚本比较包大小差异,如果有增大超过了设置的阈值,则邮件通知相关开发,开发关注排查是什么原因导致包大小变大;同时做好记录,每次打包的包大小变化及时注意记录,造成包大小变化的原因,落入文档。

流程规范是用于保证每个项目开发者知晓开发中注意什么,养成好的开发习惯,避免造成包大小的突然变大。

  • 引入新的三方库时,要考虑是否已有同类型的库,是否可以自己实现,是否会造成体积增大。尽量避免Objective-C和Swift混编,优先引用相同语言类型的库
  • 新增的图片资源,关注大小,考虑是否能用Iconfont,是否能代码实现,注意放入项目的位置,如果体积太大,压缩后使用
  • 废弃模块不要保留,及时清理
  • 及时关注包大小变化,对于包大小变化的原因及结果整理落入文档,反馈总结

总结

截止目前为止,笔者总体项目优化结果如下:

优化内容 ipa大小
原始 22.9M
Xcode编译优化-Pod优化后 21M
Xcode编译优化-Asset Catalog Compiler编译设置优化后 20.7M
Xcode编译优化-其它 13.2M
资源优化 10.3M

达到了预期瘦身的效果,虽然还有更进一步优化的空间,比如把项目中唯一引用的Swift的第三方库改为OC的,从而可以去除混编,能大幅缩减项目大小,但是由于需要改动业务代码,笔者暂时搁置了。

总的来说,笔者在业务代码没有改动的情况下,经过Xcode编译优化和资源文件压缩,把包大小从22.9M压缩到了10.3M,就结果来说是超出了预期。

但是作为优化包大小的实践来说还有待未完待续的地方,就是最后一步的代码优化,笔者打算单独抽一篇文章,来补充一下,使用代码优化的流程逻辑。


https://mp.weixin.qq.com/s/_Mvl0FGriKsvAyRheU2Z7w

最多阅读

快速配置 Sign In with Apple 2年以前  |  4554次阅读
使用 GPUImage 实现一个简单相机 2年以前  |  3267次阅读
APP适配iOS11 2年以前  |  3247次阅读
App Store 审核指南[2017年最新版本] 2年以前  |  3082次阅读
开篇 关于iOS越狱开发 2年以前  |  3034次阅读
所有iPhone设备尺寸汇总 2年以前  |  3000次阅读
在越狱的iPhone设置上使用lldb调试 2年以前  |  2982次阅读
给数组NSMutableArray排序 2年以前  |  2946次阅读
UITableViewCell高亮效果实现 2年以前  |  2726次阅读
使用ssh访问越狱iPhone的两种方式 2年以前  |  2716次阅读
关于Xcode不能打印崩溃日志 2年以前  |  2576次阅读
使用ssh 访问越狱iPhone的两种方式 2年以前  |  2466次阅读
iOS14 隐私适配及部分解决方案 1年以前  |  2217次阅读
使用最高权限操作iPhone手机 2年以前  |  2214次阅读
为对象添加一个释放时触发的block 2年以前  |  2211次阅读

手机扫码阅读