iOS项目优化

Author Avatar
xiaoLit Created: Mar 06, 2019 Updated: Sep 06, 2019

一、分析

1. 审查安装包中的每个文件

审查安装包中的每个文件是最为简单有效的挖掘优化点的方式,在包大小优化过程中也应被反复执行。

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

在编译时开启Xcode build setting中的Write Link Map File开关,Xcode就会生成一份link map文件。

在编译目录里找到Link Map文件(txt类型),默认的文件地址:

~/Library/Developer/Xcode/DerivedData/项目名-xxxxxxxxxxxxx/Build/Intermediates.noindex/项目名.build/Debug-iphoneos/项目名.build/

分析软件很多,贴一个github开源的LinkMap解析工具

二、代码级别的优化

在项目里新建一个类,给它添加几个方法,但不要在任何地方import它,build完项目后观察linkmap,你会发现这个类还是被编译进可执行文件了。这是因为object-cruntime 性质,按C++的经验,没有被使用到的类和方法编译器都会优化掉,不会编进最终的可执行文件,但object-c不一样,因为object-c的动态特性,它可以通过类和方法名反射获得这个类和方法进行调用,所以就算在代码里某个类没被使用到,编译器也没法保证这个类不会在运行时通过反射去调用,所以只要是在项目里的文件,无论是否又被使用到都会被编译进可执行文件。又比如我们的项目里会引入很多第三方静态库,如果能知道这些第三方库在可执行文件里占用的大小,就可以评估是否值得去找替代方案去掉这个第三方库。

1. 工具的介绍

Otool可以提取并显示iOS下目标文件的相关信息,包括头部,加载命令,各个段,共享库,动态库等等。它拥有大量的命令选项,是一个功能强大的分析工具,当然还可以做反汇编的工具使用。

说到Otool就不得不提到mach-O ,那什么是mach-O

Mach-O格式全称为Mach Object文件格式的缩写,是mac上可执行文件的格式,类似于windows上的PE格式 (Portable Executable), linux上的elf格式 (Executable and Linking Format)。

2. 查找无用selector

结合LinkMap文件的__TEXT.__text,通过正则表达式([+|-][.+\s(.+)]),我们可以提取当前可执行文件里所有objc类方法和实例方法(我们称为SelectorsAll)。再使用otool命令otool -v -s __DATA __objc_selrefs逆向__DATA.__objc_selrefs段,提取可执行文件里引用到的方法名(我们称为UsedSelectorsAll),我们可以大致分析出SelectorsAll里哪些方法是没有被引用的(类似SelectorsAll-UsedSelectorsAll)。

命令为:

otool -V -s __DATA __objc_selrefs 项目.app/项目 | open -f

注意,系统APIProtocol可能被列入无用方法名单里,如UITableViewDelegate的方法,我们只需要对这些Protocol里的方法加入白名单过滤即可。

另外第三方库的无用selector也可以这样扫出来的。


简单使用:
有位大佬写的脚本,让使用起来非常方便,运行脚本填写路径即可。思路相同,文章:iOS代码瘦身实践:删除无用的方法。对应库:selectorsunref

3. 查找无用OC类

通过otool命令逆向__DATA.__objc_classlist段和__DATA.__objc_classrefs段来获取当前所有oc类和被引用的oc类,两个集合相减就是无用oc类。
命令为:

//查找所有类
otool -V -s __DATA __objc_classlist 项目.app/项目 | open -f

//查找引用的类
otool -V -s __DATA __objc_classrefs 项目.app/项目 | open -f

结果下图:

这些都只提供了类在二进制文件中的位置地址,而没有提供类名等可读信息。所以在获取到差集后,还需要结合otool -o + BinaryName 将地址转换成可读的类名。


简单使用:
有位大佬写的脚本,让使用起来非常方便,运行脚本填写路径即可。思路相同,文章:iOS代码瘦身实践:删除无用的类。对应库:classunref

4.扫描重复代码

可以借助类似pmd这样的第三方工具,但需要重构代码,成本较高。

pmd使用非常方便可以直接将脚本写入到xcoderun script中,然后利用xcode的警告方式提示出来。

安装:

brew install pmd

在Xcode中添加Run Script脚本了:

Build Settings加号New Run Script Phase

#检测swift代码
#pmd cpd --files ${EXECUTABLE_NAME} --minimum-tokens 50 --language swift --encoding UTF-8 --format net.sourceforge.pmd.cpd.XMLRenderer > cpd-output.xml --failOnViolation true

#检测objective-c代码
pmd cpd --files ${EXECUTABLE_NAME} --minimum-tokens 50 --language objectivec --encoding UTF-8 --format net.sourceforge.pmd.cpd.XMLRenderer > cpd-output.xml --failOnViolation true

# Running script
php ./cpd_script.php -cpd-xml cpd-output.xml

注意:记得将${EXECUTABLE_NAME}替换为项目名字。

  • --minimum-tokens指定重复代码的最少token数量。这里的token是一个比较抽象的概念,不是字符,不是单词,也不是短语的意思。根据经验来说,Swift语言的最优值是50:太大,会漏掉重复代码;太小,会将一些代码误判为重复代码。
  • --formant指定输出格式,这里指定为xml文件。
  • --failOnViolation标识为设置为true,意思是只要检测到重复代码,就不继续进行编译。(经测试,无论–failOnViolation设置成true,还是false,都不能阻断编译的正常运行。所有的warning都能正常地输出。)

在工程主目录下,创建cpd_script.php文件,文件内容如下:

<?php
foreach (simplexml_load_file('cpd-output.xml')->duplication as $duplication) {
    $files = $duplication->xpath('file');
    foreach ($files as $file) {
        echo $file['path'].':'.$file['line'].':1: warning: '.$duplication['lines'].' copy-pasted lines from: '
            .implode(', ', array_map(function ($otherFile) { return $otherFile['path'].':'.$otherFile['line']; },
            array_filter($files, function ($f) use (&$file) { return $f != $file; }))).PHP_EOL;
    }
}
?>

注意:需要安装php环境。

brew install php

编译即可

会看到提示以警告形式展示。

5. 将代码放到静态库中

这个操作更加需要重构代码,成本浩大。
iOS应用程序瘦身的静态库解决方案

三、资源文件优化

1. 压缩图片资源

综合各种因素,无论是无损还是有损pngquant都表现很好。使用方式有很多种,常用的命令行和GUI都很方便。这里选用的是ImageOptim,不但性能高效,使用方便,还集成了很多目前最好的图片优化工具。

ImageOptim会对每张图片分别应用勾选的压缩算法,然后对比每种压缩算法产出的图片,选取最小的那张作为输出结果,非常方便。


我们先看一下无损压缩
无损压缩,通过删除图片中不必要的元数据,实现优化图片大小。

Xcode’s conversion is applied even when it makes files bigger. It will undo ImageOptim’s savings.

可以看到imageoptim官网提到关于Xcode's built-in (de)optimization
原来xcode 在编译的时候会对 png 图片进行 recompress,生成苹果喜欢的 CgBI 格式,就会undoImageOptim所做的操作,为了优化图片解码,减少不必要的 GPUCPU 的开销。

注:详细说明xcode在打包时对图片所做的事情:iOS减包实战:Compress PNG Files作用分析,通过此文我们可以知道xcode pngcompression时都采取了什么操作,所以引言中undo不应该为撤销,翻译成取消或者破坏比较合适。以下对此开关做个对比实验。

Compress PNG Files就是xcode用于控制图片转换的开关,默认开启。
如果你项目没有这个配置。解决没有Compress PNG Files - Packaging

实验结果如下:

Assets管理图片原始图片无损压缩图片
图片大小35.3MB27.6MB
Compress开启,.app大小76.1MB76.1MB
Compress关闭,.app大小76.1MB76.1MB
项目中原始图片无损压缩图片
图片大小38.9MB31.3MB
Compress开启,.app大小38.2MB37.7MB
Compress关闭,.app大小38.3MB30.6MB

素材图片是一堆大小不一的图

这里的大小衡量的是ipa解压后Payload文件夹下.app的大小。

由此可以看出只有图片在项目目录的情况下,无损压缩才生效。证明开关是有效的,但是对Asset Catalog中的图片无效,那么有没有其他设置可以控制Asset Catalog中的图片不进行这种转换呢。

Asset Catalog其中有个参数Compression,让我们测试一下是否能发挥无损压缩图片的作用。

项目中原始图片无损压缩图片
图片大小14.5MB12.3MB
.app大小,默认Automatic27.9MB27.9MB
.app大小,Lossless27.9MB27.9MB
.app大小,Basic27.9MB27.9MB
.app大小,GPU Best Quality35.2MB35.2MB
.app大小,GPU Smallest Size35.2MB35.2MB

以上数据Compress开关处于NO
从上图可以看到并没有能发挥出无损压缩图片作用的开关。

自此有查阅了很多资料,发现几乎没有相关内容,只有一篇《干货|今日头条iOS端安装包大小优化—思路与实践》提到,并且探索了很远,非常值学习一下,膜拜。可是最终结果还是失败了,苹果并没有留下任何操作Asset的“软硬开关”。可以得出结论,Compress PNG Files还是开启比较好,毕竟还能提升加载速度。关于iOS的无损压缩到此告一段落。


接下来我们看一下有损压缩
《干货|今日头条iOS端安装包大小优化—思路与实践》中的实验数据得到结论,综合App Slicing等因素我们还是使用Asset Catalog来管理图片最好,所以就不考虑项目中零散图片的处理了。

Assets管理图片原始大小有损压缩80%
图片大小14.3MB6.6MB
Compress开启,.app大小28.6MBMB19MB
Compress关闭,.app大小28.6MBMB19MB

换了一批大图用于测试,15张。
可以非常直观的看到,开关对有损压缩后的图片不起任何作用。80%有损压缩还是非常有成效的,打包后的包体小了9MB,并且仍然很清晰,对比了多张几百KB的图片一般体积会变小一半,但没有肉眼可见的区别。


webP
谷歌有一个webP格式的图片,压缩效果也非常优秀,不过对比上面的imageoptim并没有出色太多,而且对于iOS使用起来稍微麻烦一些,而且比PNG多消耗2倍左右CPU和解码时间,感觉不是大量依赖图片的app没必要采用。
webP 格式图片在 iOS 中的应用
在iOS项目中使用WebP格式图片

2. 使用工具清理未使用的图片

LSUnusedResources
这个app的原理是,对某一文件目录下所有的源代码文件进行扫描,用正则表达式匹配出所有的@"xxx"字符串(会根据不同类型的源代码文件使用不同的匹配规则),形成“使用到的图片的集合”,然后扫描所有的图片文件名,查看哪些图片文件名不在“使用到的图片的集合”中,然后将这些图片文件呈现在结果中。

新的加入的正则

regex: ([-_]?\d+)

不过比较复杂的名字拼接方法,还是手动查看一下图片资源是否被使用最为稳妥。

3. 删除重复的资源

这里的重复资源(主要指图片)不是指命名重复而是内容相同。
介绍一个fdupes是Linux下的一个工具,它由Adrian Lopez用C编程语言编写并基于MIT许可证发行,该应用程序可以在指定的目录及子目录中查找重复的文件。fdupes有各种选项,可以实现对文件的列出、删除、替换为文件副本的硬链接等操作。
文件对比以下列顺序开始:
大小对比 > 部分MD5签名对比 > 完整MD5签名对比 > 逐字节对比

$ brew install fdupes             # mac通过brew安装

$ fdupes -r ~/path                # 搜索重复子文件在终端展示
$ fdupes -Sr ~/path               # 搜索重复子文件&显示每个重复文件的大小在终端展示
$ fdupes -Sr ~/path > ~/log.txt   # log输出到指定路径文件夹

4. 判断 png 图片是否使用 Alpha

图像处理 imagemagick
中文站

四、编译器优化选项

1. CPU指令集优化

注:看到很多文章还在说能减小包体难道都穿越回去了吗?
应用程序切片App Slicing是2015年iOS9增加的功能。当用户从app store上下载app时,可以只下载适用于其设备的app架构版本和所需资源,从而减少app所占的空间。
也就是说额外的CPU指令集将不再会影响用户在AppStore看到的和实际安装的包体。
Working with App Thinning in iOS 9

CPU指令集相关
iOS 中的 armv7,armv7s,arm64,i386,x86_64 都是什么

实际打包所包含的指令集是这两个选项的集合。

2. Generate Debug Symbols

注:好多都说把这个设置为NO可以减小包体,确实是这样。但是效果如何?都考虑包体大小问题了,难道不考虑解决线上问题需要符号表吗?连bugly都不使用吗?

实测中型项目60+ MB,打包出来的Symbols不足10KB
有可能是特例,但是证明具体问题具体分析。

3. 按需加载资源(On Demand Resources)

苹果从iOS9开始引入了On Demand Resource功能,即一部分图片可以被放置在苹果的服务器上,不随着app的下载而下载,直到用户真正进入到某个页面时才下载这些资源文件。
Xcode 的设置中(在 Build Setting 里),开启按需加载资源需要把Enable On Demand Resources改为YES。下载和管理按需加载资源是由操作系统完成的,无需担心。

具体使用方式大佬写的非常详细:
iOS On-Demand Resources

注意:大佬测试,我们却发现了一个Xcode巨坑的问题:当工程需要支持iOS9以下系统时,Xcode会在打包完成上传app store时失败。

4. 中间代码(Bitcode)

App 瘦身的最后一个内容是中间代码。中间代码有点抽象,但在本质上,它是在 App 被下载前,苹果优化它的新途径。中间代码使得 App 可以在任何设备上尽可能快速和高效执行。中间代码可以为最近使用的编译器自动编译 App,并且对特定的架构做优化(例如 arm64 64 位处理器,如 iPhone6siPad Air 2)。
中间代码会和上文提到的其他瘦身技巧一起使用,去除针对其他架构的优化内容,只下载需要的优化内容,从而减少下载文件的大小。
iOS 中,中间代码是一种新特性,并且在新的工程中需要手动开启。这个过程可以在 Build Setting 下把 Enable bitcode 修改为 YES

苹果在2016年的WWDC What’s new in LLVM中详细介绍了这一功能。

LTO能带来的优化有:

  • 将一些函数內联化
  • 去除了一些无用代码
  • 对程序有全局的优化作用

build setting中开启Link-Time Optimization为Incremental,苹果还声称LTOapp的运行速度也有正向帮助。

LTO也会带来一点副作用。LTO会降低编译链接的速度,因此只建议在打正式包时开启。开启了LTO之后,link map的可读性明显降低,多出了很多数字开头的“类”(LTO的全局优化导致的)。

实测减少了大概300K。

6. 去掉异常支持

如果不适用C++的话,Enable C++ Exceptions完全可以设为NO

Enable Objective-C Exceptions看需要设为NO

并且Other C Flags添加-fno-exceptions,可以对某些文件单独支持异常,编译选项加上-fexceptions即可。

会有明显的变化。

7. Optimization Level

release版应该选择FastestSmalllest这个选项会开启那些不增加代码大小的全部优化,并让可执行文件尽可能小。
debug版应该选择NO Optimization[-Onone],否则lldbpo会报错。类似:error: <EXPR>:3:1: error: use of unresolved identifier 'dataArr' dataArr ^~~~~~~

8. Optimization

Build Setting > Asset Catalog Compiler - Options

该选项xcode默认nothing

nothingspace
小项目.app大小76.1MB61.6MB
大项目.app大小185MB174MB

至于time选项则是运行时的读取时间优化,对于主流的设备而言来说并无太大意义,也很难测算太详细数据。

9. Flatten Compiles XIB Files

官方文档:

官方解释是:指定是否在编译时剥离nib文件以优化它们的大小,设为YES时编译出来的nib文件会被压缩但是不能编辑,谨慎选择。

10. Dead Code Stripping

build setting = YES(好像默认就是YES)。 确定 dead code(代码被定义但从未被调用)被剥离,去掉冗余的代码,即使一点冗余代码,编译后体积也是很可观的。

2.4. 清理无用代码
2.4.1. 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. Remove functions and data that are unreachable by the entry point or exported symbols

Xcode 默认会开启此选项,C/C++/Swift 等静态语言编译器会在 link 的时候移除未使用的代码,但是对于 Objective-C 等动态语言是无效的。因为 Objective-C 是建立在运行时上面的,底层暴露给编译器的都是 Runtime 源码编译结果,所有的部分应该都是会被判别为有效代码。

Dead Code Stripping 设置为YES 也能够一定程度上对程序安装包进行优化,只是优化的效果一般,对于一些比较小的项目甚至没有什么优化体现,所以这里也就没有上测试数据。Dead Code Stripping 是对程序编译出的可执行二进制文件中没有被实际使用的代码进行Strip操作。对于更深层次的解读,在参考链接的文章里有详细描述。参考:Dead Code Stripping

11. Precompile Prefix Header

Precompile Prefix Header设为NO时,头文件pch不会被预编译,而是在每个用到它导入的框架类库中编译一次。每个引用了pch内容的.m文件都要编译一次pch,这会降低项目的编译速度。

Precompile Prefix Header设为YES时,pch文件会被预编译,预编译后的pch会被缓存起来,从而提高编译速度。 需要编译的pch文件在Prefix Header中注册即可。

五、项目中的一些表现

  • 80%有损压缩
    105MB->86.7MB

  • Options设置space
    86.7MB->82.6MB

  • 使用LSUnusedResources删除不用资源文件
    删除多余静态库
    82.6MB->67.8MB

  • 使用fdupes找到226条记录
    67.8MB->67.6MB

  • 删除历史遗留业务和又一次LSUnusedResources删除资源
    67.8MB->62.2MB

Reference

iOS 本地图片优化实践

干货|今日头条iOS端安装包大小优化—思路与实践

On-Demand Resources Guide中文版(按需加载资源–下)

如何使用 iOS 9 App 瘦身功能

当我们谈论iOS瘦身的时候,我们到底在谈论些什么

使用Simian进行重复代码检测

探秘 Mach-O 文件

将代码查重工具CPD集成到Xcode

iOS微信安装包瘦身

HOW TO HIGHLIGHT YOUR TODOS, FIXMES, & ERRORS IN XCODE