iOS客户端安装包大小优化

iOS客户端安装包大小优化,第1张

前言

APP 的大小是分为 APP 下载大小和安装大小两个概念的。

下载大小是指 App 压缩包(也就是 .ipa 文件)所占的空间,用户在下载 App 时,下载的是压缩包,这样做可以节省流量;
当压缩包下载完成后,就会自动解压,解压过程也就是通常所说的安装过程;安装大小就是指压缩包解压后所占用的磁盘空间。

就将“安装包”作为了优化指标。“安装包”减小后,“下载包”自然也会减小。

App Store OTA 下载大小限制:

虽然苹果历年都会调整 App 下载大小,由之前的 100M 到后来的 150M 再到现在的 200M。如今,App 下载大小超出 200 MB 时 ,会出现两种情况:

iOS 13 以下的用户,无法通过蜂窝数据下载 App;iOS 13 及以上的用户,需要手动设置才可以使用蜂窝网络下载 App。

Apple __TEXT 段大小限制:

iOS 7 之前,二进制文件中所有的 __TEXT 段总和不得超过 80 MB;

iOS 7.X 至 iOS 8.X,二进制文件中,每个特定架构中的 __TEXT 段不得超过 60 MB;

iOS 9.0 之后,二进制文件中所有的 __TEXT 段总和不得超过 500 MB。

安装包大小增长的影响

AppStore 下载大小如果在 OTA 下载限制内增长,对用户新增、留存等指标影响不大。而一旦超过 OTA 下载限制,则对整体指标产生明显影响。之前统计的劣化数据指标:当限制在 150MB 并且无法下载的时候,对用户的新增有 10%的影响。由于 iOS13 限制的宽松化,所以在 iOS13 之后设备上这个数据将低于 10%。此数据仅供参考并不能一概而论,对于不同类型的 App 首次安装的场景会呈现差异,比如生活服务、出行类 App 对应蜂窝下载场景会多于影音类、游戏类 App。

其次对仍然需要支持 iOS8 以下的 App, 超出 __TEXT 段大小的限制将会很大程度上影响审核以及发版进度。当然可以通过一些手段进行救急,比如拆分动态库的方式绕过。但是这些手段可能导致安装包整体变得更大。

除了 Apple 的限制外,包大小的劣化一定程度上意味着更加慢的启动速度;更多的的代码逻辑;更低研发效率;过于复杂的代码还会带来对代码修改的风险将对稳定性产生负面影响;让性能等基础体验变差,所以包大小不是一个孤立的指标,它从侧面的反映出 App 的健康状态。

安装包的构成以及如何分析安装包 安装包构成

将 ipa 安装包后缀名改为 zip,将其解压,显示.app 包内容后,就可以很直观的看到安装包的组成部分。一般会包括以下几个部分:

Exectutable: Mach-O 可执行文件Resources:资源文件
图片资源:Assets.car/bundle/png/jpg 等视频 / 音频资源:mp4/mp3 等静态网页资源:html/css/js 等视图资源:xib/storyboard 等国际化资源:xxx.lproj 其他:文本 / 字体/ 证书 等
Framework:项目中使用的动态库
SwiftSupport: libSwiftxxx 等一系列 Swift 库其他依赖库:Embeded Framework
Pulgins:Application Extensionsappex:其组成大致与 ipa 包组成一致 安装包分析

通过分析安装包,了解安装包中可执行文件占用大小、资源占用大小,了解安装包的现状。明确从哪里入手可以获得 ROI 最高的优化手段。而在做包大小分析过程中比较难的是,怎么样通过线下的安装包衡量对下载大小的影响。

但由于上传到 AppStore Connect 到之后,Apple 对安装包做了一些调整,线下安装包的变化无法对应到真正的下载大小变化的变更。而这部分调整包括:

App Slicing 对于不同架构的裁剪,可执行文件只剩下单架构;Asset.car 中图片只留下设备需要的特定尺寸和压缩算法的变体;二进制部分__TEXT 段的通过 FirePlay 进行加密导致 __TEXT 段的压缩比为 1( iOS 13+
以上设备下载变体中苹果移除了这个加密 );

所以线下评估的时候,通过删除 Asset.car 中图片带来 10MB 的包大小的减小,但对最后的下载大小影响可能远远小于 10MB 。而当增加的 2MB 的代码 ,最后的下载大小实打实地增长了 2MB。

可执行文件分析

安装包中的可执行文件,占了安装包中很大一部分空间,而这部分不光和代码有关还和编译、链接过程中添加的参数,编译的机器环境、Xcode 版本等等都有关系。而常常通过 LinkMap 来对可执行文件进行分析。

LinkMap 中包含了可执行文件的架构信息,段表,链接了的所有文件,以及文件中各符号占用的大小。其实通过分析 LinkMap 就基本得到了可执行文件中包含了哪些东西。这部分数据也有助于针对性的进行一些优化。

常见的包大小优化手段 资源文件瘦身 去除无用 / 重复的资源

业务的迭代开发,出现无用的图片资源是比较正常的,我们可以借助工具找出哪些图片资源没有被使用过。推荐下面两款工具:

LSUnusedResources:可视化客户端工具;FengNiao:命令行工具,可嵌入到Run Script中或者在CI系统中使用,支持的模式匹配更加强大。

因为这类工具的原理都是在相关文件(.m、.swift 等等)中利用正则表达式检测是否有图片名称的字符,所以存在以下问题。 问题点:

如果代码中使用的图标名称是拼接而成的,就会误以为相关图片是废弃图片;

可以利用Duplicate Photos从内容上检测重复/相似图片。

之所以要使用自动化工具来检测重复资源的原因是因为资源是弱类型,我们在项目迭代过程中手动去维护是相当麻烦的一个过程。转换一下思维,如果资源变成强类型了,那我们维护起来就相当容易了。目前就有这样一个工具 R.swift一定意义上将资源变成强类型,类似于 Android 开发中的 R 文件。

可利用fdupes查找项目中的重复文件。其原理是对比不同文件的签名,签名相同的文件就会判定为重复资源。

mac 上可直接通过 brew install fdupes 进行安装,可以使用 fdupes -Sr 文件夹名称 来查看所有涉及到的目录和子目录中的重复文件的大小,其余相关指令可自行查阅,不建议使用 fdupes 相关命令直接删除搜索出来的重复资源,风险比较高。
结论:

考虑到工具的不准确性,可以利用工具粗检测一下哪些资源没有被使用,然后经人工确认后才统一进行删除。对于工具无法检测出来的资源,就只能人工进行筛查了,可每人分配几个模块,提高效率。

资源压缩

请注意:这里的资源不包括 Assets Catalog 管理的资源。

PNG 资源
Xcode 的 Build Setting 提供的给我们两个编译选项来帮助压缩 PNG 资源。

Remove Text Medadata From PNG Files(默认开启):能帮助我们移除 PNG 资源的文本字符,比如图像名称、作者、版权、创作时间、注释等信息。Compress PNG Files(默认开启):当设置为 YES 后,打包的时候会利用 pngcrush 工具自动对项目中所有 PNG
图片进行无损压缩以及修改文件格式,该工具是开源的–pngcrush地址。

Compress PNG Files 设置为 YES 后,XCode 会调用该路径的脚本
/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/usr/bin/iphoneos-optimize。

pngcrush 工具在其同级目录存放,iphoneos-optimize 脚本中关于 PNG 压缩的内容如下:

sub optimizePNGs {
    my $name = $File::Find::name;
    if ( -f $name && $name =~ /^(.*)\.png$/i) {
        my $crushedname = "-pngcrush.png";
        // $PNGCRUSH便是pngcrush工具路径
        my @args = ( $PNGCRUSH, "-q", "-iphone", "-f", "0" );
        if ( $stripPNGText ) {
             push ( @args, "-rem", "text" );
        }
        push ( @args, $name, $crushedname );

        if (system(@args) != 0) {
            print STDERR "$SCRIPT_NAME: Unable to convert $name to an optimized png!\n";
            return;
        }
        unlink $name or die "Unable to delete original file: $name";
        rename($crushedname, $name) or die "Unable to rename $crushedname to $name";
        print "$SCRIPT_NAME: Optimized PNG: $name\n";
    }
}


从内容上来看,脚本是通过png 后缀名来判断是否为 png 图片,如果图片改变后缀名,则该图片则不会被 pngcrush 工具进行处理。

我们可以通过下面命令手动使用 pngcrush 工具。

# image.png 经编码后 生成 image1.png
xcrun -sdk iphoneos pngcrush -iphone -f 0 image.png image1.png

# image.png 经解码后 还原成 image1.png
# 即使还原回去与原图片也不会完全一致,这里就不展开描述了
xcrun -sdk iphoneos pngcrush -revert-iphone-optimizations image.png image1.png

pngcrush 工具编码结果主要变化内容如下:

在 IHDR 块之前插入了 CgBI 块来表示这种格式修改 IDAT 块中的数据,去除 zlib 压缩头和 Adler-32 校验和;八位真彩色图像按 BGR/BGRA 顺序存储,而不是按 IHDR 块中指示的 RGB 和 RGBA 顺序存储;图像像素使用预乘 alpha;修改后的文件使用。png 为有效图像定义的文件扩展名以及内部文件结构,但符合 PNG 的查看和编辑软件不再能够处理它们;增加了一个 iDot 数据块,是 Apple 自定义的数据块,暂时不知其作用;

其本质是使正常的 png 图片变成了一个优化后的 CgBI 格式的 png

可以利用pngcheck查看处理前的图片信息,利用pngdefry查看处理后的图片信息(其还可以将 CgBI 格式的 png还原回去,这个功能跟pngcrush工具解码功能类似)。

从上述变化内容来看, pngcrush 工具编码过程并不是简单的压缩数据,更重要的是对文件格式做了修改。因为 iPhone 中,图像是以 BGRA 格式在内存中处理的,所以修改后的格式变成了 iPhone 能更方便处理的格式,加快处理速度。

根据我自己测试的压缩效果来看,对于 Bundle 中放置的 png 图片,经过 pngcrush 的处理,大小不降反增,目前暂时没有找到哪些具体因素影响其压缩效果。

结论:

Compress PNG Files 虽然是压缩 PNG,但其最主要的目的并不是为了压缩图片大小, 而是将 PNG 转换成 iOS
更容易处理、更快速度的去识别的格式,可以根据项目在开启、关闭两种情况下的打包大小,自行取舍。

非 PNG 资源

非 PNG 资源压缩包含两种方式

直接通过一些压缩工具将资源进行压缩,格式保持不变,如一些图片资源、音视频资源等,图片压缩工具下文会有介绍。还有一些文本资源,如 json 文件、html 文件等,无法使用上述的方式压缩,可以采用压缩成 zip 等压缩格式的方式,可分为三步:
压缩阶段:在 Build Phase 中添加脚本,构建期间对白名单内的文本文件做 zip 压缩; 解压阶段:在 App启动阶段,在异步线程中进行解压 *** 作,将解压产物存放到沙盒中;读取阶段:在 App 运行时,hook 读取这些文件的方法,将读取路径从
Bundle 改为沙盒中的对应路径;

结论:

可选用合适的压缩工具对音视频、非 Assets Catalog
管理的图片资源进行压缩。对于一些比较大的文本文件可选用第二种运行时解压读取的方式,如 Lottie 动画的 json 文件。

Assets Catalog

Assets Catalog 涉及的技术点比较多,后续可能会单独开一篇博文专门讲这一部分内容。

去除 @1x 图片

@1x 图是 iPhone 3Gs 用的,iPhone 4 开始使用 @2x 图了,iPhone 6p 开始使用 3x 图。

结论:

可以删除 Assets 中所有 @1x 的图片资源。

图片压缩

Assets.car 文件是工程中 Asset Catalog 的构建产物。Xcode 构建过程中,在 compile asset catalog 节点时, 构建 Asset Catalog 的工具 actool 会首先对 Asset Catalog 中的 png 图片进行解码,得到 Bitmap 数据,然后再运用 actool 的编码压缩算法进行编码压缩处理。

如果你开发时放入到 Assets 中的是 jpg 格式文件,在最终生成的 Assets.car 文件中也会成为 png 图片。

xcrun assetutil --info Assets.car 。可使用该命令检查 Assets.car 中每张图片使用的编码压缩算法。

目前 actool 会使用的压缩算法包括 lzfse 、 palette_img 、 deepmap2 、 deepmap_lzfse 、 zip ,影响其使用何种算法的因素包括 iOS 系统版本、ASSETCATALOG_COMPILER_OPTIMIZATION 设置(位于 Build Setting 中)等;

iOS 11.x 版本:对应的压缩算法为 lzfse、zip;iOS 12.0.x - iOS 12.4.x: 对应的压缩算法为 deepmap_lzfse、palette_img;iOS 13.x: 对应的压缩算法为 deepmap2 ;

按照压缩比来讲 lzfse < palette_img ~= deepmap_lzfse < deepmap2

无损压缩通过变换图片的编码压缩算法减少大小,但是不会改变 Bitmap 数据,对于 actool 来说,它接收的输入(Bitmap 数据)没有改变,所以无损压缩无法优化 Assets.car 的大小,但是可以用来优化非 Asset Catalog 管理的图片。使用有损压缩方式并采用合适的压缩方法是可以减小 Assets.car 的大小。可以对图片采用RGB with palette(调色板算法)编码方式来达到图标压缩的效果,这种编码方式进行压缩特别适合内部颜色相对接近的图标。但是需要注意如果图片中有半透明效果,这种压缩方式可能会导致半透明的地方出现噪点,所以压缩之后请注意仔细检查一下

RGB with palette 编码的得到的字节流首先维护了一个颜色数组。颜色数组每个成员用 RGBA 四个分量维护一个颜色。图像中的每个像素点则存储颜色数组的下标代表该点的颜色。颜色数组维护的颜色种类和数量由图片决定,同时可以人为的限制颜色数组维护颜色的种类的上限,默认为最大值 256 种,具体原理详见底部相关链接 --【Palette Images】;

使用下文提到的 ImageOptim-CLI 工具,我们可以改变图片的编码方式为 RGB with palette,命令如下:

imageoptim -Q --no-imageoptim --imagealpha --number-of-colors 16 --quality 40-80 ./1.png

–number-of-colors:控制颜色数组维护颜色的数量; --quality:控制图片的质量变为原来的百分比; 命令中的数值可以在显著减少包大小的同时维持肉眼看不到的质量变化。

以图片资源举例,我们可以使用工具对其进行压缩,推荐几款工具如下:

TinyPng:网页工具,有损压缩;TinyPNG4Mac:TinyPng的客户端工具,无需联网使用浏览器;ImageOptim:
客户端工具,支持无损压缩及有损压缩两种形式,可自定义设置压缩方式。ImageOptim-CLI:Mac
可使用brew install imageoptim-cli安装,其会根据你的指定,选择性调用
JPEGmini、ImageAlpha、ImageOptim 等工具,实现中间过程自动化。

如果想将car文件中的png提取出来,可以使用Asset Catalog Tinkerer。

引申一下,好的工具是开发利器,Mac效率工具,可见Mac效率软件

结论:

能用 Asset Catalogs 管理的资源,尽量使用 Asset Catalogs 来管理。使用 Asset Catalog
管理图片不要对图片进行无损压缩,最终起不到压缩效果,如果想要压缩,可以采用上面所提到的有损压缩方式,并检查压缩后的效果。

图片资源使用 Webp 格式

谷歌开源的格式,Webp 压缩率比较高,同时支持有损和无损两种压缩模式,可以带来更小的图片体积,而且肉眼看不出差异。

根据 Google 的测试,无损压缩后的 WebP 比 PNG 文件少了 26%的体积,有损压缩后的 WebP 图片相比于等效质量指标的 JPEG 图片减少了 25%~34% 的体积。

但是 WebP 与 JPG 以及 PNG 相对比,在编解码的 CPU 消耗以及解码时间上会差一些,因为编码是用户上传图片时的一次性 *** 作,并且编码过程是在服务器后台进行,对用户的影响不大,对用户影响大的主要是解码过程,会导致图片加载速度慢一些。所以,我们需要根据项目的实际情况在性能和体积上做取舍。

如果从服务器带宽以及流量来看,因为图片的体积变小,所以会减小带宽,降低成本。

推荐二种转 WebP 格式的方法

iSpart:腾讯出品,GUI工具;webp工具: 在Mac下,可以使用Homebrew安装WebP工具–brew install webp;

iOS 原生并不支持 WebP 格式加载,需要引入 SDWebImage/WebP,或者进行自研。

结论:

该方案适合整个大前端及后端统一调整,整体进行优化,如果是单一的客户端进行调整,可能达不到最优效果。

资源动态化

除了上文提到的使用 On-Demand Resources 方式将部分资源放在苹果服务器之外,我们也可以将一些本地资源转移到自己的服务器上去。这样不仅降低了安装包大小,也将这些资源动态化了。适合放在服务器的资源应包含以下几个特性:

不影响首屏加载体验;变化频率较高;尺寸很大;

如一些 Banner 广告图、主题资源、音视频资源、H5 资源资源。

结论:

可尽量将满足上述特性的资源放置在服务器。

图标优化 使用 tint color 精简单色图标;使用图标字体(IconFont)替换单色图标;将部分相似图标进行整合;

结论:

如果项目有相对的设计规范及标准图标样式,使用图标字体是一个很好的方案。剩余的优化点根据项目实际情况决定是否使用。

Mach-O 可执行文件瘦身

在对 Mach-O 文件进行瘦身优化时,我们可以通过分析 Link Map 文件来给我们一定的数据参考,帮助我们分析 Mach-O 文件的构成。

Link Map 是编译链接时可以生成的一个 txt 文件,它生成目的就是帮助程序员分析包大小。Link Map 记录了每个方法在当前的二进制架构下占据的空间。通过分析 Link Map,我们可以了解每个类甚至每个方法占据了多少安装包空间。

开启 Build setting 中的 Write Link Map File 开关,Xcode 就会生成一份 Link Map 文件。其中生成的 Link Map 文件路径如下:
~/Developer/Xcode/DerivedData/项目/Build/Intermediates.noindex/项目.build/Debug-iphonesimulator/项目.build/项目-LinkMap-normal-x86_64.txt

如果直接阅读 Link Map 文件,效率会比较低,也不直观,我们可以使用一些工具帮助我们分析

LinkMap工具地址

无用代码的清理:

一般的无用代码筛查方式可以分为动态和静态两种方式。静态的方式主要是通过代码扫描、参与编译构建过程或者分析最终产物来确认哪些代码没有被用到。而动态的方式主要是靠插桩或者运行时信息来获取哪些代码没有执行。由于 Objc 强大的动态特性,我们在样本量足够大的场景使用动态方式会比静态方式准确率高很多。

静态筛查筛查方案:

比较简单的方式是:基于 otool dump 最终产物中的 __objc_class_list & __objc_class_refs 做差集找到未使用的 Objc 类。

如果代码采用 C 、C++ 等静态语言编写代码时,编译期已经确定了基本的代码逻辑,所以编译器会帮助我们将没有使用到的代码标记为 Dead code 最终不会打包到安装包中。但 Objc 是典型的动态语言,很多逻辑都是在运行时决议的,我们通过静态扫描的方式扫描出来的误差会比较大,抖音对于这静态结果初筛的得到未使用类的准确性只有 24% (总样本 264 个,命中 64 个)

Objc 动态特性引入的的主要的问题包括:

实际用到了但被扫描成无用类:

一个类确实没有被其他地方使用, 但是本身逻辑依赖 +load 、+initialize、attribute((constructor)) 在启动时调用;通过 string 动态调用;抽象基类、基类等会被认为是无用类;通过运行时动态生成的代码引用了某个类;一个类专门作为通知处理类;MTLModel 等,通过运行时消息机制 assign value 的无法通过 classref 统计;典型的 DI 场景。如果一个类声明遵循了某个 Protocol,外部使用的时候使用了这个 Protocol 进行方法调用。

实际没用到但被认为有用到:

某个对象被另外一个对象引用,但是另外一个对象本身未被使用到。这时候会遗漏掉这个对象所属 Class 的检查。

动态筛查方案:

基于插桩的行级别代码覆盖率:

基于 GCOV 或者 LLVM Profile 二进制的插桩方案可以实现在运行时收集插桩数据来指导无用代码的删除。但插桩方案局限性也显而易见,插桩会劣化二进制本身的大小和性能,同时原生的插桩方案是无法过审上线。数据收集只能局限于线下。

基于 Runtime 的轻量级运行时「类覆盖率」方案:

Objc 的类首次调用类初始化时,+initialize 被执行,系统会自动标记已被调用,在 metaClass 中 data 的 flags 字段第 29 位就存着这个这个状态。可以使用 flags & RW_INITIALIZED 获取。iOS14 之后这个值的获取方式有变化

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


上报的数据可以让我们了解我们线上真实的 Class 使用情况,对得到的数据不仅可以用来删减未使用的代码。还可以分辨使用率低的场景,如果是低频且必须的场景可以考虑使用跨端技术这种对原生包大小影响比较小的方案实现。而如果这些场景是某个渗透率很低的需求可以考虑直接下线为其他需求做置换。

手动去除 已经下线的陈旧代码,AB 试验已经下线的代码;通过转 H5、Hybrid 或者 RN 实现的 Native 功能,可以定期清理;将部分功能进行重构,以此去除一定的代码。 多个可执行文件中去除相同代码

这里的多个可执行文件一般是指 APP 宿主程序与 Extension 程序,如果 APP 宿主程序与 Extension 程序都依赖同一个静态库库时,就会导致两个可执行文件中都包含相同的代码;个人觉得有两种解决方案:

考虑到 Extension 程序相对宿主程序来说功能较小,可尽量使用原生功能,不接入三方库;如果想要接入同一份库,可将该库以动态库的方式引入,最终两个可执行文件会动态链接同一份库,避免了重复代码; 动态库的段压缩

动态库的段压缩。将动态库中一些段进行压缩存入到文件中,在动态库加载的时候将这部分手动加载到内存中,将牺牲一部分启动性能。

编译选项改进

Xcode 支持编译器层面的一些优化选项,通过修改 Build Setting 的一些相关配置,可以让我们介于更快的编译速度和更小的二进制大小并且更快的执行速度之间自由选择想要进行的优化粒度,这些选项有的会影响资源文件,有的会影响可执行文件,因为内容比较多,所以起一个独立的章节描述。

这种方式的性价比很高,改动一项配置,就可能会带来收益,但是可能具有一定的风险,需要谨慎。

下文中提到的一些 Xcode 默认配置可能在低版本 Xcode 上不是默认配置,如果不是默认,可手动勾选。

去除无用架构

可以在 Build Setting - Excluded Architectures 项设置排除的架构。

先看一下几种架构的含义:

模拟器 32 位处理器测试需要 i386 架构;模拟器 64 位处理器测试需要 x86_64 架构;真机 32 位处理器需要 armv7, 或者 armv7s 架构;真机 64 位处理器需要 arm64 架构。

结论:

理论上只保留 arm64 架构其实就够用了,可以去除 armv6 、 armv7 、 armv7s 三种架构。

使用链接时优化 LTO(Link-Time Optimization)

可以在 Build Setting - Link-Time Optimization 项设置优化方式

其提供三种选项:

No 不开启链接期优化;(默认项)Monolithic 生成单个 LTO 文件,每次链接重新生成,无缓存高内存消耗,参数 LLVM_LTO=YES;Incremental 生成多个 LTO 文件,增量生成,低内存消耗,参数 LLVM_LTO=YES_THIN;

LTO 能带来的优化有:

将一些函数內联化:不用进行调用函数前的压栈、调用函数后的出栈 *** 作,提高运行效率与栈空间利用率;去除了一些无用代码:如果一段代码分布在多个文件中,但是从来没有被使用,普通的 -O3 优化方法不能发现跨中间代码文件的多余代码,因此是一个局部优化。但是 Link-Time Optimization 技术可以在 link 时发现跨中间代码文件的多余代码;对程序有全局的优化作用:这是一个相对广泛的概念。举个例子来说,如果一个 if 方法的某个分支永不可能执行,那么在最后生成的二进制文件中就不应该有这个分支的代码。

LTO 会降低编译链接的速度,所以建议在打正式包时开启; 开启了 LTO 之后,Link Map 的可读性明显降低,多出了很多数字开头的类(LTO 的全局优化导致的),所以如果需要阅读 Link Map,可以先关闭 LTO;

LTO 虽然是链接期优化,但是仍然需要编译期参与,加入了 LTO 的编译出来的 .a 本质是 LLVM 的 BitCode,如果使用未开启 LTO 构建出来的的 .a 直接是机器码了。直接链接是无法完成 LTO 优化的。
开启 LTO 之后跨编译单元的重复代码会被链接器单独生成以 .lto.o 为后缀的目标文件进行链接。尤其是对于 Objc Runtime 需要的一些结构, 比如方法签名的 literal string、protocol 的结构等有比较大的优化。同时开启 Oz 和 LTO 可以让外联函数都只存在一份能够最大限度的优化安装包体积(是全局的优化作用,将已经外联的函数去重)。如果项目中大量的使用了 Protocol 建议还是开启这个选项。

结论:

可将Link-Time Optimization选项由 NO 改为 Incremental 。

语言编译优化

OC

OC 关于编译内联优化的参数位于 Build Settings -> Apple Clang - Code Generation ->
Optimization Level ,选项如下:

None[-O0]: 编译器不会优化代码,意味着更快的编译速度和更多的调试信息,默认在 Debug 模式下开启;Fast[-O, O1]: 编译器会优化代码性能并且最小限度影响编译时间,此选项在编译时会占用更多的内存;Faster[-O2]:编译器会开启不依赖空间 /
时间折衷所有优化选项。在此,编译器不会展开循环或者函数内联。此选项会增加编译时间并且提高代码执行效率;Fastest[-O3]:编译器会开启所有的优化选项来提升代码执行效率。此模式编译器会执行函数内联使得生成的可执行文件会变得更大。一般不推荐使用此模式;Fastest Smallest[-Os]:编译器会开启除了会明显增加包大小以外的所有优化选项。默认在 Release 模式下开启;Fastest, Aggressive Optimization[-Ofast]:启动 -O3
中的所有优化,可能会开启一些违反语言标准的一些优化选项。一般不推荐使用此模式。

结论:

使用默认配置即可,无需修改。

Swift

Swift 关于编译内联优化的参数位于 Build Settings -> Swift Compiler - Code Generation -> Optimization Level ,可选参数如下。

No optimization[-Onone]:不进行优化,能保证较快的编译速度。默认在 Debug 模式开启;Optimize for Speed[-O]:编译器将会对代码的执行效率进行优化,一定程度上会增加包大小。默认在 Release
模式下开启;Optimize for Size[-Osize]:编译器会尽可能减少包的大小并且最小限度影响代码的执行效率。

Optimize for Size 的核心原理是对重复的连续机器指令外联成函数进行复用,和函数内联的原理正好相反。因此,将其开启,能减小二进制的大小,但同时理论上会带来执行效率的额外消耗,对性能(CPU)敏感的代码使用需要评估。

配合其使用的还有Compliation Mode设置,其含有两个选项

Single File:单个文件优化,可以减少增量编译的时间,并且可以充分利用多核
CPU,并行优化多个文件,提高编译速度。但是对于交叉引用无能为力;Whole Module:模块优化,最大限度优化整个模块,能处理交叉引用。缺点不能利用多核 CPU 的优势,每次编译都会重新编译整个
Module;

在 Relese 模式下 -Osize 和 Whole Module 同时开启效果会发挥的最好,从现有的案例中可以看到它会减少 5%~30% 的可执行文件大小,并且对性能的影响也微乎其微(大约 5%)。

结论:

将 Release 默认下配置改为 Optimize for Size[-Osize],Compliation Mode选项改为Whole Module

死代码裁剪

可以在 Build Setting - DEAD_CODE_STRIP 项设置。

在构建完成之后如果是 C、C++ 等静态的语言的代码、一些常量定义,如果发现没有被使用到将会被标记为 Dead code。开启 DEAD_CODE_STRIP = YES 这些 Dead code 将不会被打包到安装包中。在 LinkMap 这些符号也会被标记为 <> 。
该项其实也属于在清除无用代码。

结论:

默认配置即为 YES,所以使用默认配置即可,无需修改。

去除符号信息

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

Strip Style

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

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

结论:

主工程选择All Symbols,静、动态库选择Non-Global Symbols。

Strip Linked Product

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

需要注意Strip Linked Product 选项在 Deployment Postprocessing 设置为 YES 的时候才生效,而 Deployment Postprocessing 在 Archive 时不受手动设置的影响,会被强制设置成 YES。

结论:

将Deployment Postprocessing设置为 NO,将Strip Linked
Product设置为YES,将Release模式的下的Debug Information Format 修改为 DWARF with
dSYM file。

Strip Debug Symbols During Copy

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

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

结论:

Strip Debug Symbols During Copy在Release 模式下设置为YES,在Debug模式下设置为false。

Strip Swift Symbols

开启 Strip Swift Symbols 能帮助我们移除相应 Target 中的所有的 Swift 符号,这个选项也是默认打开的。
这一选项是出现 Xcode 将 xcarchive 包导出成 ipa 文件过程中出现的,不是通过Build Setting设置的。

结论:

一般默认勾选,如果没勾选请手动勾选。

选项设置方式优化

大部分项目都会使用 Cocoapods 工具进行管理,Cocoapods 的 project 文件在每次 pod install 或者 pod update 会重置,所以需要 hook pod install 来设置 Pods 中每个 Target 的编译选项。

post_install do |installer|
    installer.pods_project.targets.each do |target|
        target.build_configurations.each do |config|
            config.build_settings['ENABLE_BITCODE'] = 'NO'
            config.build_settings['STRIP_INSTALLED_PRODUCT'] = 'YES'
            config.build_settings['SWIFT_COMPILATION_MODE'] = 'wholemodule'
            if config.name == 'Debug'
                config.build_settings['SWIFT_OPTIMIZATION_LEVEL'] = '-Onone'
                config.build_settings['GCC_OPTIMIZATION_LEVEL'] = '0'
            else
                config.build_settings['SWIFT_OPTIMIZATION_LEVEL'] = '-Osize'
                config.build_settings['GCC_OPTIMIZATION_LEVEL'] = 's'
            end
        end
    end
end


影响包大小的一些编码习惯和建议

由于 Objc 语言的特性,编写的代码会在编译过程中出现生成各种类结构、方法签名,protocol 结构体等等副产物。这些产物在我们工程很大很复杂的时候常常会导致我们安装包大小极具劣化。所以在保证不影响编码,尝试一些对包大小友好的编码方式。积少成多长期对包大小有正向影响。

Class Method vs C 函数

通常我们对于一些基础和通用的函数会采用工具类的方式对外暴露。使用类方法完成功能。但当我们采用 Class Method, 这种方式在编译的时候需要生成 Class 的类结构。调用的方法会通过 objc_methodSend。如果采用 C 函数的方式可以减小这部分的开销。如果只是自己组件内部使用的私有的功能性函数还是建议使用 C 函数的方式实现。

Property vs IVAR

Objc 对于 Class 的 property,会自动的生成 set、get 方法,比如这个 property 是 Class 的私有属性的时候,我们可以直接使用 ivar 来代替 property。减小这部分的包大小开销。这里需要注意,当我们使用 property 的 getter 实现 LazyLoad 或者 setter 存在一些其他副作用的时候还是需要保留 property 的。

减少 Block 的使用

我们知道 Block 是一个特殊的 OC 对象。需要占用部分二进制空间来表征一个 Block 对象。所以在非必要使用 Block 的场景。去掉 Block 实现可以优化不少包大小,常见的比如 Masonry 通过 Block 实现的链式调用。

缩减方法参数 & 函数参数的个数

我们调用一个函数的时候,传入的参数会存在一个参数列表中。当我们调用的时候传入参数很多的时候会对我们安装包大小有较大的影响,尤其是类似网络请求的的方法,动辄 7、8 个参数,而且调用的地方又很多。所以在对外 API 设计的时候如果参数超过 3 个尝试通过构造对象解决传参问题。

高频率使用的宏不要使用多行的方式

这个问题常见于我们组件化依赖注入场景、Log 记录等。当一个展开为 3 行的宏,经过上千次的调用之后最后占用的大小也是非常恐怖的。

尽量避免代码的复制粘贴

如果组件化做得不好或者一些大型的 app 存在一些业务闭环的中台业务场景时候,代码里头会存在不少的重复代码。有些可能是改了前缀或者命名空间。可以考虑通过 PMD 对工程中的源码进行扫描。将重复代码进行统一。

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/web/996691.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-21
下一篇 2022-05-21

发表评论

登录后才能评论

评论列表(0条)

保存