iOS App 瘦身减肥记

5,052 阅读17分钟

前言

有人说现在什么年代了, 还差那几十M的流量?比如下面这位道友

你king,你king~ 你擦!

而且我最近也发现了一个惊人的现象, 网上很多关于瘦身的文章开头上都是“最近公司项目不忙,正好利用清闲时间把项目瘦瘦身。” 要么就是 “最近老大说要优化项目包体积 ,正好有时间.....” balabala~。

其实App持续迭代,团队之间不断合入代码和资源,如果在开发过程中不多注意细节或优化,让包无节制的增长下去。后果不堪设想。当然快餐App除外。

什么?! 你觉得还是没必要?

你:“必要性??”

我:“呸,你儿子是个胖子”

你:“为什么骂人 ”

我:“呸,你儿子是个胖子”

笔者分享此文章的目的在于,让在座的开发者们在日常开发中有哪些值得注意的东西,平时多注意这些东西,未来可能不需要过多的去操心瘦身的问题。更让非iOS开发的同学了解瘦身我们到底做了什么,有个笼统的概念。


一、苹果食谱 - 三餐( Slicing、Bitcode、On-Demand Resources)

1. 什么是Slicing (iOS 9) ? [ˈslaɪsɪŋ]

当向 App Store Connect 上传 .ipa 时,App Store Connect 构建过程中,会自动分割当前App,创建特定的变体,以适配不同的设备。当用户从App Store下载当前App, App Store会根据当前用户的设备类型,分发针对当前设备的变体的过程, 叫做Slicing, 原理如下图:

大致分发原理

通常情况下 iOS系统App内部本地图片使用时 同一张图片通常有 @1x 、@2x、 @3x三种格式,我们知道这是为了适配不同的屏幕,但是有一个问题,每一张本地图片都有3张的话,当前设备只能使用一种格式,而另外两两种格式的图片不起任何作用,进而会增大app包的体积,Slicing的出现解决了此问题,不过还需要我们遵循Slicing的规则,即@1x 、@2x、 @3x格式的图片要同时存在,并且将图片资源存放在.xcassets去管理,不能直接放在Bundle。 然而就当前苹果设备来讲 @1x的设备基本不存在, 所以@2x、 @3x是必备的。

此功能可以简单的优化图片资源大小,在适配不同屏幕的前提下,进一步优化了当前App的包体积。

所以:小图片尽量放到Assets.xcassets里面去

注意的是:用Asset管理图片要比直接放在bundle里在加载速度上要快。因为Asset会在编译期做优化,让加载的时候更快,此外在Asset中加载图片是要比Bundle快的,因为'[UIImage imageNamed]'要遍历 Bundle 才能找到图。加载 Asset 中图的耗时主要在在第一次张图,因为要建立索引,可以通过把启动的图放到一个小的 Asset 里来减少这部分耗时。

引申:
  • 凡是图片都必须要有@1x 、@2x、 @3x放进.xcassets去管理过于繁琐,有一个一劳永逸的方法,即把当前@1x的图片转成PDF格式 放进xcassets里面,其他的全部删除,Xcode在打包的过程中会根据当前PDF图的大小,生成@1x 、@2x、 @3x图片,然而生成的图片的像素满足像素条件,如果未来出现@4x、@5x也不需要在添加其对应格式的图片, 仅当前一个PDF即可详情请参考
  • 不是所有的情况都能放进.xcassets去做Slicing, 例如图片国际化、体积较大的图片等

2. 什么是Bitcode?

Bitcode 是一种程序中间码。是纯苹果内部行为, 当前app包含Bitcode 配置的程序将会在 App Store Connect 上被重新编译和链接,进而对可执行文件做优化。这部分都是在服务端自动完成的。

如果以后Apple 新推出了新的 CPU 架构或者以后 LLVM 推出了一系列优化,我们就不需要重新为其发布新的安装包了。Apple Store 会为我们自动完成这步。

但是开启 Bitcode需要注意:

  • 全部都要支持。我们所依赖的静态库、动态库、Cocoapods 管理的第三方库,都需要开启 Bitcode。否则打包会编译失败。具体错误会在Xcode中指出。
  • crash 定位。开启 Bitcode 后最终生成的可执行文件是 Apple 自动生成的,同时会产生新的符号表文件,所以我们无法使用自己包生成的 dYSM 符号化文件来进行符号化。

Bitcode开启 Build Settings -> Enable Bitcode -> 设置为 YES

此功能为苹果内部优化行为,非开发则直接操纵,不过多赘述

3. on-Demand Resources

翻译为按需加载,顾名思义,就是需要的时候再去加载。指一部分资源(图片资源,压缩包资源等)可以被放置在苹果的服务器上(非开发者主动放置,只需放到项目中,做些处理即可),不随着 App 的下载而下载,直到用户真正进入到某个页面时或者出发下载事件时才下载这些资源文件。

按需加载支持iOS 9.0及以后的app,按需加载资源是默认开启的。你也可以在target的build settings中手动更改。

使用介绍:

3.1 在Xcode中,开启按需加载功能(默认开启)

开启关闭,默认开启

3.2 选中Resources Tag标签 点击+加号 并且命名自己想要的资源名称

2231609308975_.pic_hd.jpg 名词解释

  • Initial install tags. 此种标签的资源,会随着 app 从 App Store 下载而下载,但是会影响 app 的 ipa 大小,也就是说此种资源会包含在 ipa 内。和开发工具默认行为一致
  • Prefetch tag order.此种标签会在 app 下载后,开始下载相应的资源,此种资源并不会影响 ipa 的大小,也就是说此种资源并不包含在 ipa 内。此场景常用游戏场景,不过多赘述
  • Dowloaded only on demand. 此种标签下的资源,会在必要的时候,主动触发下载,这是我们开发者自己控制下载时机的。

3.3 将预下载的资源放进Xcode内部,点击下方+ 选中该资源 这样将该图片资源添加进按需加载的规则中

image.png

3.4 点击当前图片资源, 查看Resources Tag

WechatIMG225.png

3.5 如何通过按需加载进行coding

    // 加载资源
    // New 为按需加载命名的tags, 如上图所示

    NSSet *tags = [NSSet setWithObjects:@"New", nil];
    NSBundleResourceRequest *resourceRequest = [[NSBundleResourceRequest alloc] initWithTags:tags];
    
    [resourceRequest beginAccessingResourcesWithCompletionHandler:^(NSError * _Nullable error) {
        if (error) {
            return;;
        }
        // 当前文件从苹果服务器拉去成功,并且成功加载至本地
        // 加载至本地的资源,可直接通过原有的Boundle加载即可
        NSString *path = [[NSBundle mainBundle] pathForResource:@"按需加载_命名图片" ofType:@"jpeg"];
        UIImage *image = [UIImage imageWithContentsOfFile:path];
        // 加载至本地的资源 如下图可直观查看
    }];

3.6 异常处理

  • NSBundleOnDemandResourceOutOfSpaceError 错误在用户设备空间不足,无法下载请求的资源时发生。这会告诉用户清理空间,然后重试。
  • NSBundleOnDemandResourceExceededMaximumSizeError 错误在资源超过该 app 按需加载资源的最大内存限制时发生。这会允许用户去清理部分资源。
  • NSBundleOnDemandResourceInvalidTagError 在所请求的资源 tag 无法找到时发生。这可能是一个 Bug,你应该去确认一下正确的 tag 名称是什么

二、运动燃脂 (删除无用资源)

1. 直接删除项目中无用的代码、无用类文件

通过第三方工具 fui无用类检测工具 可以检测到当前项目中有那些类没有被import,择优去删除, 不过通常每100个代码文件 为0.1M, 除非极限瘦身,否则必要性不大,还可能造成不必要的麻烦。

  1. 删除已废弃代码
  2. 去除相同代码
  3. 删除已经废弃的pod的引用
  4. 减少Release模式下的库的引用
  • 避免一些在deBug模式下的调试用库,放到了线上环境
pod 'XXXX', :configurations => ['Debug']
  1. 删除未使用头文件
  2. 检查每个类占用的大小,改进优化实现方案
  • 利用第三方工具LinkMap针对查找的每个类的情况去优化

检查每个类占用的大小

2. 删除项目中没有被使用或者重复的资源

2.1 检测未使用的图片

使用开源工具LSUnusedResources可以查找出项目中不被使用的图片资源 , 下载之后 打开是一个MAC版本的Xcode项目 点击运行,展现出一个程序如果, 点击Search, 可以扫描出无用的图片资源, 反复核对后进行删除

2.2. 由于当前App不适配非Retina屏幕,所以@1x的图片没必要继续放入项目当中,核对后删除所有@1x的图片资源

2.3. 删除重复资源(内容相同,名称不作为判断条件)

  • 通过fdupes(Linux下的一个工具)查找在当前目录和其子目录的重复文件
  • 安装fdupes
 brew install fdupes
  • 查找重复的资源
fdupes -r 你的文件夹

enter image description here

3. 尽量压缩一些文件

  1. HTML文件 尽量远端化, 如果当前项目无法直接远端化,可以让前端童鞋尽量压缩一下(主要压缩css和js文件, 如果有大图片的话也建议压缩下,效果很明显。) 注意线上环境中本地H5 不应该存在.map格式的文件。
  2. 图片无损压缩利用 tinypng进行无损压缩(经过尝试 基本没有效果)
  3. 采用70% 的有损压缩非透明的图片(如存在透明,压缩之后跟UI设计师核对一下) 压缩后,图像画质

压缩之后图片减少了70% + 压缩后的大小

4. webP格式的文件

一些过大的图片还可以采用webP的格式进行加载,可以利用 SDWebImage/WebP 提供的 UIImage+WebP分类来进行WebP格式图片的转换:

 + (UIImage *)sd_imageWithWebPData:(NSData *)data;

实际用法:

// 1.工程引入SDWebImage开源库;
// 2.引入WebP.framework,下载地址:https://github.com/seanooi/iOS-WebP。
// 3.让SDWebImage支持WebP,设置如下Build Settings -- Preprocessor Macros , add SD_WEBP=1。

NSString *path = [[NSBundle mainBundle] pathForResource:@"我是webP图片名称" ofType:@"webp"];
NSData *data = [[NSData alloc] initWithContentsOfFile:path];
UIImage *img = [UIImage sd_imageWithWebPData:data];
self.imageView.image = img;

三、 阻挡应酬 (编译器选项配置)

1.图片信息

1.1 Compress PNG Files

  • 在工程目录下(非Assets.xcassets)的PNG的图片会出现Compress PNG Files选项设置YES或者NO,依据项目两者打包情况,自行取舍。 为什么要说看情况呢? 因为Compress PNG Files虽然是压缩PNG,但其真实的目的并不是为了压缩图片大小, 而是将PNG转换成iOS更容易处理、更块速度的去识别。 有些图片经过该配置设置反而变大了,但也有些情况确实变小了。 原因涉及到图片相关知识了解更多

1.2 Remove Text Medadata From PNG Files

  • 同上Compress PNG Files, 将 Remove Text Medadata From PNG Files 设置为YES 会移除PNG资源的文本字符, 比如图像名称、作者、版权、创作时间、注释等信息。(如果PNG图片都在Assets.xcassets不用关心这么多问题了 )
1.3 Apple Clang - Code Generation
  • Optimization Level 编译参数决定了程序在编译过程中的两个指标:编译速度和内存的占用,也决定了编译之后可执行结果的两个指标:速度和文件大小。

  • Build Settings -> code Generation -> Optimization LeveL

None[-O0]: 不优化。在这种设置下, 编译器的目标是降低编译消耗,保证调试时输出期望的结果。程序的语句之间是独立的:如果在程序的停在某一行的断点出,我们可以给任何变量赋新值抑或是将程序计数器指向方法中的任何一个语句,并且能得到一个和源码完全一致的运行结果。

Fast[-O1]: 大函数所需的编译时间和内存消耗都会稍微增加。在这种设置下,编译器会尝试减小代码文件的大小,减少执行时间,但并不执行需要大量编译时间的优化。在苹果的编译器中,在优化过程中,严格别名,块重排和块间的调度都会被默认禁止掉。

Faster[-O2]: 编译器执行所有不涉及时间空间交换的所有的支持的优化选项。在这种设置下,编译器不会进行循环展开、函数内联或寄存器重命名。和‘Fast[-O1]’项相比,此设置会增加编译时间和生成代码的性能。

Fastest[-O3]: 在开启‘Fast[-O1]’项支持的所有优化项的同时,开启函数内联和寄存器重命名选项。这个设置有可能会导致二进制文件变大。

Fastest, Smallest[-Os]: 优化大小。这个设置开启了‘Fast[-O1]’项中的所有不增加代码大小的优化选项,并会进一步的执行可以减小代码大小的优化。

Fastest, Aggressive Optimizations[-Ofast]: 这个设置开启了“Fastest[-O3]”中的所有优化选项,同时也开启了可能会打破严格编译标准的积极优化,但并不会影响运行良好的代码。

Smallest, Aggressive Size Optimizations [-Oz]: 与相似-Os,但会进一步减小代码大小,并且可能需要更长的时间才能运行。

Fastest Smallest[-Os] 极小限度会影响到包大小,而且也保证了代码的执行效率,是最佳的发布选项,一般 Xcode 会在 Release 下默认选择 Fastest Smallest[-Os] 选项,较老的项目可能没有自动勾选。

1.4. optimization

optimization 选项设置为 space 可以减少包大小


2. Architectures

如果不支持32位以及 iOS8 ,去掉 armv7 ,可执行文件以及库会减小,即本地 .ipa 也会减小。

  • iPhone5S之后都是64位的(包括5s)。

  • ipad air 之后都是64为的(包括air)。 ipad型号类型

3. 去除符号信息

可执行文件中的符号是指程序中的所有的变量函数枚举变量地址映射关系,以及一些在调试的时候使用到的用于定位代码在源码中的位置的调试符号,符号和断点定位以及堆栈符号化有很重要的关系。

3.1. Strip Style

Strip Style 表示的是我们需要去除的符号的类型的选项,其分为三个选择项:

  • All Symbols: 去除所有符号,一般是在主工程中开启。
  • Non-Global Symbols: 去除一些非全局的 Symbol(保留全局符号,Debug Symbols 同样会被去除),链接时会被重定向的那些符号不会被去除,此选项是静态库/动态库的建议选项。
  • Debug Symbols: 去除调试符号,去除之后将无法断点调试。

3.2. Strip Linked Product

并不是所有的符号都是必须的,比如 Debug Map,所以 Xcode 提供给我们 Strip Linked Product 来去除不需要的符号信息(Strip Style 中选择的选项相应的符号),去除了符号信息之后我们就只能使用 dSYM 来进行符号化了,所以需要将 Debug Information Format 修改为 DWARF with dSYM file。

3.3. Strip Debug Symbols During Copy

与 Strip Linked Product 类似,但是这个是将那些拷贝进项目包的三方库、资源或者 Extension 的 Debug Symbol 去除掉,同样也是使用的 strip 命令。这个选项没有前置条件,所以我们只需要在 Release 模式下开启,不然就不能对三方库进行断点调试和符号化了。

Cocoapods 管理的动态库(use_framework!)的情况就相对要特殊一点,
因为 Cocoapods 中的的动态库是使用自己实现的脚本 Pods-xxx-frameworks.sh 来实现拷贝的,所以并不会走 Xcode 的流程,当然也就不受 Strip Debug Symbols During Copy 的影响。当然 Cocoapods 是源码管理的,所以只需要将源码 Target 中的 Strip Linked Product 设置为 YES 即可。

3.4. Strip Swift Symbols

开启 Strip Swift Symbols 能帮助我们移除相应 Target 中的所有的 Swift 符号,这个选项也是默认打开的。

如下图所示: enter image description here

四、最后的坚持(二进制优化)

将Mach-o可执行文件内的__TEXT的部分代码段安全移动到其他代码段中,避开苹果的加密机制,提高可执行文件的压缩效率,进而让App的下载大小减小。

通俗解释: 就是把一些非必要的经过转换之后的代码,乔迁到别的地方,避开苹果对这些非必要的代码再做什么操作,苹果对这些操作会对下载包变大。

1. Mach-O 文件

iOS 可执行文件是 Mach-O 格式,主要由 HeaderLoad CommandsData 三部分。

  • Header 描述了文件的大概信息。

  • Load Commands 由多条 Load Command 组成,它们描述了 Data 在二进制文件和虚拟内存中的布局信息,有了这个布局信息就能够知道 Data 在二进制文件中和虚拟内存中是怎样排布的,它相当于修房子时的图纸一样。

  • Data 存储了实际的内容,主要是程序的指令和数据,它们的排布完全依照 Load Commands 的描述。

__TEXT 包含 Mach header,被执行的代码和只读常量(如C 字符串)。只读可执行(r-x)。
__DATA 包含全局变量,静态变量等。可读写(rw-)。
__LINKEDIT 包含了加载程序的『元数据』,比如函数的名称和地址。只读(r–)

2. 查看Mach-o文件

MachOview 可以查看当前的文件

enter image description here

在这个文件夹下找到对应的工程文件名/Build/Products/Debug,进入这个目录下,就可以找到我们的可执行文件了。

enter image description here

打开MachOview文件 -> Open enter image description here

Open之后,找到当前项目的.app文件,按照下图寻找,点击Open。 enter image description here

查看当前项目的Mach-O文件,效果如下 enter image description here

3. 更改 Mach-o 文件

enter image description here

将上图中的红框里面的可更改的代码段全部移除值其他代码段中:

  • 在工程的Build Setting 里面的 Other Linker Flag里面逐行添加如下Flag:
-Wl,-rename_section,__TEXT,__cstring,__RODATA,__cstring
-Wl,-rename_section,__TEXT,__objc_methname,__RODATA,__objc_methname
-Wl,-rename_section,__TEXT,__objc_classname,__RODATA,__objc_classname
-Wl,-rename_section,__TEXT,__objc_methtype,__RODATA,__objc_methtype
-Wl,-rename_section,__TEXT,__gcc_except_tab,__RODATA,__gcc_except_tab
-Wl,-rename_section,__TEXT,__const,__RODATA,__const
-Wl,-rename_section,__TEXT,__text,__BD_TEXT,__text
-Wl,-rename_section,__TEXT,__textcoal_nt,__BD_TEXT,__text
-Wl,-rename_section,__TEXT,__StaticInit,__BD_TEXT,__text
-Wl,-rename_section,__TEXT,__stubs,__BD_TEXT,__stubs
-Wl,-rename_section,__TEXT,__picsymbolstub4,__BD_TEXT,__picsymbolstub4,
-Wl,-segprot,__BD_TEXT,rx,rx

enter image description here

重新打包之后把打包的ipa后缀改成zip, 再用MachView 查看解压之后的Mach-o文件,其内部格式已经发生变更 enter image description here

4. 下载大小减少的大致原理:

对项目工程进行Archive 之后会生成.xcarchive文件,该文件中包含了App、dsYMS以及其它信息

xcarchive文件上传到App Store Connect后,苹果对App中的可执行文件进行加密等操作, 将App压缩成ipa文件(和平时打测试包不一样),才发布到App Store,加密对可执行文件的大小本身影响很小,但它会严重影响可执行文件的压缩效率,导致压缩后的ipa文件大小增加, 也就是下载的包变大,事实上这种加密只针对非越狱手机有用,对越狱手机不起作用。

5. 注意

  • 根据现有市场大量成功案例得出结论,以上二进制可优化iOS13以下的iPhone应用 可减小30%左右的下载大小。

  • 以上二进制优化只针对iOS13以下的下载大小有作用, iOS13以及以上苹果内部针对大小做了优化。

苹果更新日志 iOS13

五、减肥之余要注意营养 (补充)

  • 用户实现下载的包大小是看开发者用Xcode导出IPA文件大小。
  • AppStore中看到的包大小是用户下载了实际包大小并且解压安装完成之后的大小。

参考文献

blog.timac.org/2016/1018-a…

juejin.cn/post/691112…

juejin.cn/post/684490…

www.jianshu.com/p/8f3d3f6b6…

blog.csdn.net/youshaoduo/…