OC

iOS包体积优化

Posted by sunzhongliang on August 11, 2022

背景

早些年的iPhone在非WiFi模式下下载APP限制为几十MB,超过这个大小就强制必须使用WiFi下载,虽然苹果官方这些年一直在提高这个大小限制,如今的iOS 13下载超过200MB的安装包时会默认弹框请求用户是否继续下载,如果APP的安装包体积更小,则可以提高整体更新率,减少用户等待时间,更快的触达用户,因此安装包瘦身是APP优化中的重要一环。

资源瘦身

大资源文件通过运行下载

对于一些非必要的大资源文件,例如字体库换肤资源静态的H5样式等等,可以在 APP 启动后通过异步下载到本地,而不用直接放在 ipa 包内。

图片资源放入xcassets

尽量将图片资源放入Images.xcassets中,包括 pod 库的图片。 Images.xcassets 中的图片加载后会有缓存,提升加载速度,并且在最终打包时会自动进行压缩(Compress PNG Files),再根据最终运行设备进行 2x 和 3x 分发。

对于内部 Pod 库中的资源文件,我们可以在 Pod 库里面的 Resources 目录下新建 Asset Catalog 文件,命名为 Images.xcassets,移入所有图片文件,接着手动修改该 SDK 的 podspec 文件指定使用该 Images.xcassets

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

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];

删除重复文件

通过校验所有资源的 MD5,筛选出项目中的重复资源,推荐使用 fdupes 工具进行重复文件扫描,fdupes 是 Linux 平台的一个开源工具,由 C 语言编写 ,文件比较顺序是大小对比 > 部分 MD5 签名对比 > 完整 MD5 签名对比 > 逐字节对比

通过 Homebrew 安装 fdupes:

brew install fdupes

查看目标文件夹下的重复文件:

fdupes -Sr 文件夹   // 查看文件夹下所有子目录中的重复文件及大小
fdupes -Sr 文件夹 > 输出地址.txt  // 将信息输出到txt文件中

4474 bytes each:
Test/Images.xcassets/TabBarImage/tabBar_2.imageset/tabBar_2@2x.png
Test/Resource/TabBarImage/tabBar_2@2x.png

3912 bytes each:
Test/Images.xcassets/TabBarImage/tabBar_3.imageset/tabBar_3@2x.png
Test/Resource/TabBarImage/tabBar_3@2x.png

资源文件压缩

图片等资源建议使用无损压缩,建议和公司的设计沟通已确保图片保持在一个合理的大小区间
还可以使用WebP格式的图片,Webp 是由 Google 推出的图片格式,有损压缩模式下图片体积只有 jpeg 格式的 1/3,无损压缩也能减小 1/4,目前SDWebImage的扩展都已支持对该格式图片的加载

移除无用资源

通过资源关键字进行全局匹配,筛选出未使用的资源。有些资源使用是通过拼接或者后台下发名称,需对筛选出的资源进行确认,以防止误删

代码瘦身

删除未使用的代码

删除未使用的类、方法可以有效的减少代码段的大小,从而减少包体积。但人为的去筛选未使用的方法太过于耗时耗力,可以通过LinkMapMach-O相互结合去排查未使用的代码。

LinkMap会列出所有的类、方法、block以及所占用的大小
MachOView可以查看Mach-O文件的内容

运行时Objc类覆盖率

如果能知道App运行时有哪些类被使用过,就可以下线掉无用的模块或代码文件,Objc类覆盖率指标可以帮到我们。
APP运行时,某个Pod模块被加载的类数量除以所有类数量,可以称为这个模块的Objc类覆盖率。核心技术是判断一个类是否被加载过,下面介绍一个经过线上验证的轻量级方案。ObjC的类第一次被使用时会调用+initialize方法,类被加载过后cls->isInitialized会返回True。isInitialized方法读取了metaClass的data变量里的flags,如果flags里的第29位为1,则返回True。

// objc-class.mm
Class class_initialize(Class cls, id inst) {
    if (!cls->isInitialized()) {
        initializeNonMetaClass (_class_getNonMetaClass(cls, inst));
    }
    return cls;
}


// objc-runtime.h
#define RW_INITIALIZED        (1<<29)
bool isInitialized() {
    return getMeta()->data()->flags & RW_INITIALIZED;
}

/*
这个方法我们是无法直接调用的,它是 OC 的方法。但是,要知道类的元数据结构是不会变的,所以我们可以通过自己模拟构建类的元数据结构来获取 RW_INITIALIZED 标记位数据,从而来确定某个类是否已经初始化,代码如下:
*/
- (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; 
 }

删除版本遗留代码

随着业务的迭代,有些时候可能会产生一些兼容性等遗留下来的代码,这些代码到了某个版本、时间点不再继续运行的时候,可以删除掉,从而节省大小

精简重复代码

多人开发协作时,可能会存在大量的功能性相同、或者是直接复制粘贴的代码,对于这种代码,可以从架构层次去解决,尽可能的封装为一个公共方法。

其他

慎用引入第三方库

  1. 不要引用重复的三方库,如JSONModel和MJExtension
  2. 要考虑引入三方库的必要性,不能够仅仅是只有一个地方用到了某个功能,从而引入一个三方库

编译选项

  • Valid Architectures 设置编译生成的 ipa 包所支持的架构,不支持32位以及 iOS8 ,可去掉 armv7及之前的架构

  • Strip Link Product 和 Deployment Postprocessing Strip Linked Product 默认为 Yes,Deployment Postprocessing 默认为 No,Strip Linked Product 在 Deployment Postprocessing 设置为 YES 的时候才生效。当Strip Linked Product设为YES的时候,ipa会去除掉symbol符号,运行 App 断点不会中断,在程序中打印[NSThread callStackSymbols]也无法看到类名和方法名。而在程序崩溃时,终端的函数调用栈中也无法看到类名和方法名。但是不会影响正常的崩溃日志生成和解析,依然可以通过符号表来解析崩溃日志,适合线上使用,建议在 release 下都设置为 Yes

  • Generate Debug Symbols 默认为 Yes,当设置为 Yes 时,编译生成的 .o 文件会更大,包含了断点信息和符号化的调试信息,方便开发阶段调试,建议在 release 下设置为 No,线上需要获取崩溃信息时搭配编译生成的 dSYM 文件解析符号。

  • Enable C++ Exceptions 和 Enable Objective-C Exceptions 默认都为 Yes,用于捕获 C++ 和 OC 的异常,如果项目中使用了 try catch, 可考虑去掉并在 release 下设置为 No,配合在 Other C Flags 添加 -fno-exceptions 和 -fno-rtt ,会有比较明显的体积减小

  • Generate Debug Symbols 默认为 Yes,用生成dSYM文件,有助于解析崩溃信息。

  • Make Strings Read-Only 默认为 Yes,复用字符串字面量。

  • Dead Code Stripping 默认为 Yes,去除冗余代码。

  • Optimization Level Release 下默认为 Fastest, Smalllest[-Os],自动优化代码。

  • Symbols Hidden by Default Release 下默认为 Yes,会移除符号信息,把所有符号都定义成 private extern。

  • Strip Swift Symbols 默认为 Yes,移除 Swift 相关的符号表,运行时再从 SWIFT 标准库中获取符号,从而减少应用体积。

本文首次发布于 孙忠良 Blog, 作者 [@sunzhongliang] , 转载请保留原文链接.