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

不断的开发迭代,产品经理不断的添加需求,引入的资源文件几乎是只加不减,猛然回首,iOS包已经100多m,看来iOS瘦身迫在眉睫啊!!!!

iOS瘦身的好处


我们先来讨论 iOS瘦身的好处,正所谓知其然知其所以然。iOS瘦身有哪些好处?

1. However, consider download times when determining your app’s size. Minimize the file’s size as much as possible, keeping in mind that there is a 100 MB limit for over-the-air downloads. Abnormally large build files are usually the result of storing data, such as images, inside the compiled binary itself instead of as a resource inside your app bundle. If you are compiling an image or large dataset into your binary, it would be best to split this data out into a resource that is loaded dynamically by your app.

1.首先对于新用户当第一安装你的iOS程序时,需要下载完整的一个.ipa文件。(注意不同于升级),相同环境下iOS包越小用户的下载时间越少,这一点对于用户体验来说至关重要,特别是有些情况apple store访问不太顺利,用户下载你的iOS包肯定是迫切想了解使用你的产品,如果用户等半天(等待对于用户总是漫长的),用户会烦躁带着这种心情去了解你的产品,第一印象肯定不爽

2.回想起来你的app第一个版本,有可能才10几m,才一年多的时间,你的app已经变成100m了,然而产品业务需求是不断迭代的,如果不加以控制,后面只会越来越大

3.可能绝大部分的原因,你的产品并非是强势app(比如支付宝,qq,百度)所以你也就不能随便耍流氓,用户手机的存储空间是有限的(特别是那些16g的iPhone),除了那些强势app,你必须要在剩余不多的空间,争取用户不会删除你的app(特别是那些工具类型app)

4.less is more ,对于代码和那些资源文件也是如此,无用的代码和资源文件越多,就导致项目冗余越大,维护也越麻烦,比如很可能同一张图片有好几个不同命名,如果要改图片了就要全部更换 !

知道了瘦身的好处接下来,我们就谈论一下iOS该怎么瘦身

iOS该怎么瘦身  


在做任何相关优化之前,我们需要做一些权衡。通过权衡,可以知道把优化的重点集中在什么地方。我们上文提到,当第一安装iOS程序时,需要下载完整的一个.ipa文件。实际上.ipa文件就是一个.zip结构。简单的将后缀为.ipa文件修改为.zip,然后利用Finder将其解压出来。右键单击解压出来的.app bundle,选择显示包内容,以查看里面的资源文件。通过该方法我们可以看到哪些文件占的空间最大。记住:.app bundle是经过压缩的,并且有些文件的压缩效果要比别的文件好,所以压缩后的效果才是才是最重要的。不过一般情况下在压缩前最大的文件,在压缩后依旧是最大的文件。

而这些资源文件(包括图片、声音以及其它配置文件)通常占了ipa 很大部分,所以我首先针对资源文件优化。

(1).资源文件瘦身

a.删除无用的图片资源

一个项目开发的越久,添加的功能模块就越多,相应的,也会慢慢引入更多的图片资源。但是在移除一些不再使用的模块时,开发者往往不会将对应的图片资源一起删除,因为图片资源和源码是分离的,长久以来,项目中就会出现大量没有使用的图片资源。删除无用的资源一般来说 能减掉个2~3MB。 这个时候就要使用工具自动迅速找出工程中所有没被使用的资源文件喽(想想就知道了 ,用手工得多慢多累啊) ,工欲善其事,必先利其器。

我首先推荐的是https://github.com/tinymind/LSUnusedResources  整个过程非常的快, 比shell脚本不知道方便到哪里去了, 为了照顾那些懒癌症患者 我把使用方法也贴出来

使用方法如下

1.点击 Browse.. 选择一个文件夹;

2.点击 Search 开始搜索;

3.等待片刻即可看到结果;

4.选中某些行,然后点击 Delete 可以直接删除资源

第二种方法就是你也可以用万能的脚本 https://github.com/examplecode/unused-image/blob/master/unused-image.sh   &&  http://stackoverflow.com/questions/6113243/how-to-find-unused-images-in-an-xcode-project/6113449#6113449

b.对资源压缩

首先 尽量使用8-bit的PNG图片,比32-bit的图片能减少4倍的压缩率。由于8-bit的图片支持最多256种不同的颜色,所以8-bit的图片一般只应该用于一小部分的颜色图片。例如灰度图片最好使用8-bit。然后并不能事事都如意, 设计师提供的图片资源往往都是直接从sketch中剪切后的资源,大小非常大,这个时候就需要对png进行无损压缩,假设我们的项目中有30M的图片,然后将它们有损压缩到80%的质量,那么就可以减掉6MB左右。可以使用以下两种方法进行图片的压缩:用的是ImageOptim工具和compress命令(具体怎么使用我不想写了)。但是并不建议对资源做有损压缩,因为有损压缩通常压缩后效果不尽人意需要设计一个个检查。

c. BitCode

首先我们来介绍一下  BitCode 是啥?

Bitcode is an intermediate representation of a compiled program. Apps

you upload to iTunes Connect that contain bitcode will be compiled and

linked on the App Store. Including bitcode will allow Apple to

re-optimize your app binary in the future without the need to submit a

new version of your app to the store.

说的是bitcode是被编译程序的一种中间形式的代码。包含bitcode配置的程序将会在App store上被编译和链接。bitcode允许苹果在后期重新优化我们程序的二进制文件,而不需要我们重新提交一个新的版本到App store上。呀,真有这么高级?

LLVM是目前苹果采用的编译器工具链,Bitcode是LLVM编译器的中间代码的一种编码,LLVM的前端可以理解为C/C++/OC/Swift等编程语言,LLVM的后端可以理解为各个芯片平台上的汇编指令或者可执行机器指令数据,那么,BitCode就是位于这两者直接的中间码. LLVM的编译工作原理是前端负责把项目程序源代码翻译成Bitcode中间码,然后再根据不同目标机器芯片平台转换为相应的汇编指令以及翻译为机器码.这样设计就可以让LLVM成为了一个编译器架构,可以轻而易举的在LLVM架构之上发明新的语言(前端),以及在LLVM架构下面支持新的CPU(后端)指令输出,虽然Bitcode仅仅只是一个中间码不能在任何平台上运行,但是它可以转化为任何被支持的CPU架构,包括现在还没被发明的CPU架构,也就是说现在打开Bitcode功能提交一个App到应用商店,以后如果苹果新出了一款手机并CPU也是全新设计的,在苹果后台服务器一样可以从这个App的Bitcode开始编译转化为新CPU上的可执行程序,可供新手机用户下载运行这个App.

扯了这么多 ,推出Bitcode的好处是啥? 跟iOS瘦身啥关系?之前打包,可以运行在各个不同型号的iOS设备上,是因为在打包的时候,苹果帮我们把app在各型号设备上运行所需要的“东西”一并全部打到包里了。假设我们在打包的时候,只把要运行的设备所需的“东西”打到包里,而不需要其他型号运行所需要的“东西”,这样不就达到减小ipa大小的目的了么?BitCode就是来完成这个任务的中间件。

d.正确导入图片的姿势


图片的导入方式有如下几种:

1.加入到Assets.xcassets中

只支持png格式的图片

图片只支持[UIImage imageNamed]的方式实例化,但是不能从Bundle中加载

在编译时,Images.xcassets中的所有文件会被打包为Assets.car的文件

2.CreateGroup

黄色文件夹图标;Xcode中分文件夹,Bundle中所有所在都在同一个文件夹下,因此,不能出现文件重名的情况

可以直接使用[NSBundle mainBundle]作为资源路径,效率高!

可以使用[UIImage imageNamed:]加载图像

3.CreateFolderRefences

蓝色文件夹;Xcode中分文件夹,Bundle中同样分文件夹,因此,可以出现文件重名的情况

需要在[NSBundle mainBundle]的基础上拼接实际的路径,效率较差

不能使用[UIImage imageNamed:]加载图

4.PDFs矢量图(Xcode6+)

5.Bundle(包)中的图片素材

那这不同的导入方式,会对打出的包的大小有影响么?

经过测试得知:CreateGroup、CreateFolderRefences两种方式打出来的包,图片都会直接放在.app文件中,所以打包前后,图片的大小不会改变

而加入到Assets.xcassets中的方法则不同,打包后,在.app中会生成Assets.car文件来存储Assets.xcassets中的图片,并且文件大小方面也大大降低

所以,使用Assets.xcassets来管理图片也可以达到ipa瘦身的效果~~

话说PDFs矢量图呢 ,利用矢量图能不能帮助iOS App减少整体空间?

iOS对矢量图的支持其实只是一种方便开发者的选择, 本质上在XCode编译的阶段矢量图会自动生成对应Target的@1x,@2x和@3x的png格式图像。在iOS实际运行中使用的图片实际上已经是png格式的图片了~

用简单粗暴的实验来对比说明, 步骤如下:

使用pdf原始文件编译生成通用IPA

从生成的IPA文件中提取Asset.car文件

利用iOS Image Extractor提取Asset.car文件

将提取出来的@1x、@2x、@3x放置回工程, 并删除原始pdf中重新编译

对比步骤1生成的car文件和步骤4生成的car文件大小

结果如下:

在iOS8.3以下, 相同压缩比例的条件下, 矢量图是无法帮助App减少空间。但是在iOS8.3以上, 利用xcassets可以避免多余的资源图片下载, 只下载对应的倍率的图片。因此, 严格意义下, 利用矢量图并不能帮助App节省空间(其实跟用Assets.xcassets的方式效果差不多)。但是pdf矢量图使用起来非常的方便, 建议使用。iOS本质上并不支持矢量图, 但是在编译阶段会将矢量图转化成目标设备对应的尺寸图, 同时会利用xcassets的特性在iOS8.3以上设备下支持部分资源下载, 带到包瘦身的效果。每次都要让UI给多个尺寸的图, 肯定没有给一张方便吧? 当然, 前提是UI的童鞋是基于矢量图工具制作的图片的前提下~

简单的iOS瘦身技巧讲完了 ,我们来点儿稍微高级的,毕竟步子要一步一步走,迈步太大容易扯着蛋


(2).代码级别的优化

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

这个时候就要介绍一下LinkMap了,LinkMap文件是Xcode产生可执行文件的同时生成的链接信息,用来描述可执行文件的构造成分,包括代码段(__TEXT)和数据段(__DATA)的分布情况。比如说可执行文件的构成是怎样,里面的内容都是些什么, 

1、使用LinkMap文件对可执行文件安装包进行分析

在xcode的设置中 Project->Build Settings->Write Link Map File为YES,并设置Path to Link Map File,build完后就可以在设置的路径看到LinkMap文件了

注意:此时最好使用真机进行编译,不然可能无法找到我们想要的文件。

在以下目录可以看到LinkMap文件,如下:

/Users/chenxintao/Library/Developer/Xcode/DerivedData/AppName-fnpgyspdoyxnotbpoliocmwypkff/Build/Intermediates/AppName.build/Debug-iphoneos/AppName.build/AppName-LinkMap-normal-arm64.txt

LinkMap文件主要分为以下三部分:

1.1 Object files

整个可执行文件里包含的所有.O文件,前面的数字是这个.o文件的序号。样式如下:

# Object files:

[0] linker synthesized

[1]/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator7.0.sdk/usr/lib/crt1.o

[2]/Users/bang/Library/Developer/Xcode/DerivedData/yishu-eyzgphknrrzpevagadjtwpzzeqag/Build/Intermediates/yishu.build/Debug-iphonesimulator/yishu.build/Objects-normal/i386/TKPFileInfo.o

...

[280] /Users/bang/Downloads/yishu/yishu/Classes/lib/UMeng/MobClick/libMobClickLibrary.a(UMANJob.o)

[281] /Users/bang/Downloads/yishu/yishu/Classes/lib/UMeng/MobClick/libMobClickLibrary.a(UMANWorker.o)

[282] /Users/bang/Downloads/yishu/yishu/Classes/lib/UMeng/MobClick/libMobClickLibrary.a(MobClick.o)

[283] /Users/bang/Downloads/yishu/yishu/Classes/lib/UMeng/MobClick/libMobClickLibrary.a(UMANLaunch.o)

前面中括号里的是这个文件的编号,后面会用到,像项目里引用到静态链接库libMobClickLibrary.a里的目标文件都会在这里列出来。

1.2 Sections

接着是一个段表,描述各个段在最后编译成的可执行文件中的偏移位置及大小,包括了代码段(__TEXT,保存程序代码段编译后的机器码)和数据段(__DATA,保存变量值)。样式如下:

# Sections:

# Address  Size    Segment  Section

0x00002740 0x00273890 __TEXT __text

0x00275FD0 0x00000ADA __TEXT __symbol_stub

0x00276AAC 0x00001222 __TEXT __stub_helper

0x00277CCE 0x00019D9E __TEXT __objc_methname

0x00291A70 0x00012847 __TEXT __cstring

0x002A42B7 0x00001FC1 __TEXT __objc_classname

0x002A6278 0x000046A7 __TEXT __objc_methtype

0x002AA920 0x000061CE __TEXT __ustring

0x002B0AF0 0x00000764 __TEXT __const

0x002B1254 0x000028B8 __TEXT __gcc_except_tab

0x002B3B0C 0x00004EBC __TEXT __unwind_info

0x002B89C8 0x0003662C __TEXT __eh_frame

0x002EF000 0x00000014 __DATA __program_vars

0x002EF014 0x00000284 __DATA __nl_symbol_ptr

0x002EF298 0x0000073C __DATA __la_symbol_ptr

0x002EF9E0 0x000030A4 __DATA __const

0x002F2A84 0x00000590 __DATA __objc_classlist

0x002F3014 0x0000000C __DATA __objc_nlclslist

0x002F3020 0x0000006C __DATA __objc_catlist

0x002F308C 0x000000D8 __DATA __objc_protolist

0x002F3164 0x00000008 __DATA __objc_imageinfo

0x002F3170 0x0002BC80 __DATA __objc_const

0x0031EDF0 0x00003A30 __DATA __objc_selrefs

0x00322820 0x00000014 __DATA __objc_protorefs

0x00322834 0x000006B8 __DATA __objc_classrefs

0x00322EEC 0x00000394 __DATA __objc_superrefs

0x00323280 0x000037C8 __DATA __objc_data

0x00326A48 0x000096D0 __DATA __cfstring

0x00330118 0x00001424 __DATA __objc_ivar

0x00331540 0x00006080 __DATA __data

0x003375C0 0x0000001C __DATA __common

0x003375E0 0x000018E8 __DATA __bss

1.3 Symbols

可执行文件中各种symbol的大小,包括各个symbol的起始地址,占用大小,来自哪一个.o文件(使用之前提到的.o文件的序号)。样式如下:

# Address   Size  File    Name

0x00002740 0x0000003E [ 1] start

0x00002780 0x00000400 [ 2] +[TKPFileInfo parseWithDictionary:]

0x00002B80 0x00000030 [ 2] -[TKPFileInfo fileID]

...

计算某个.o文件在最终安装包中占用的大小,主要是解析Object files和Symbols两个部分,从Object files读取出每个.o文件名和对应的序号,然后对Symbols中序号相同的文件的Size字段相加,即可得到每个.o文件在最终包的大小。 同样首列是数据在文件的偏移地址,第二列是占用大小,第三列是所属文件序号,对应上述Object files列表,最后是名字。例如第二行代表了文件序号为2(反查上面就是TKPFileInfo.o)的parseWithDictionary方法占用了1000byte大小。

我们看到在这个里面除了可以看到DATA字段与TEXT字段的大小外,它还会列出所有类对象下的成员函数与类函数。其实这点很重要因为这样我们就可以知道工程中所有实现的函数了。通过相应的正则表达式,我们就可以提取出函数内容,其正则表达式为[+|-][.+\s(.+)],然后我们通过另外一个强大的反编译工具otool,可以提取出工程中所使用的函数列表(Used Selectors All)。

先说那什么是otool呢?

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

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

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

Mach-o包含三个基本区域:

1.头部(header structure)。Mach-o的头部帮助内核迅速确定当前文件所支持的CPU架构。

2.加载命令(load command)。

3.段(segment)。可以拥有多个段(segment),每个段可以拥有零个或多个区域(section)。每一个段(segment)都拥有一段虚拟地址映射到进程的地址空间。

4.链接信息。一个完整的用户级Mach-o文件的末端是链接信息。其中包含了动态加载器用来链接可执行文件或者依赖库所需使用的符号表,字符串表等等。 如下图左边就是苹果给出的mach-O格式的示意图 ,第二个图是我们使用machOView来分析某个可执行文件中的armv7的格式。可以看出他们两者的关系是对应的。



我们是如何找出工程中所使用的函数列表的呢,其实就是使用命令字otool -V -s __DATA __objc_selrefs 项目.app/项目 | open -f。这里的项目地址指的是项目.app的路径地址,在Xcode7中的路径为

/Users/用户名/Library/Developer/Xcode/DerivedData/项目名/Build/Products/Debug-iphonesimulator/项目名.app/项目名

另外一个要注意的是-V要大写,因为大写和小写的命令是不一样的。当然大家也可以试试把DATA __objc_selrefs改成TEXT __objc_classname看看有什么不一样。

下面就聊一聊如何对可执行文件进行瘦身。

a.查找无用selector

结合LinkMap文件的__TEXT.__text,通过正则表达式([+|-][.+\s(.+)]),我们可以提取当前可执行文件里所有objc类方法和实例方法(SelectorsAll)。再使用otool命令otool -v -s __DATA __objc_selrefs逆向__DATA.__objc_selrefs段,提取可执行文件里引用到的方法名(UsedSelectorsAll),我们可以大致分析出SelectorsAll里哪些方法是没有被引用的(SelectorsAll-UsedSelectorsAll)。注意,系统API的Protocol可能被列入无用方法名单里,如UITableViewDelegate的方法,我们只需要对这些Protocol里的方法加入白名单过滤即可。

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

b. 查找无用oc类

查找无用oc类有两种方式,一种是类似于查找无用资源,通过搜索"[ClassName alloc/new"、"ClassName *"、"[ClassName class]"等关键字在代码里是否出现。另一种是通过otool命令逆向__DATA.__objc_classlist段和__DATA.__objc_classrefs段来获取当前所有oc类和被引用的oc类,两个集合相减就是无用oc类。

c.扫描重复代码

可以利用第三方工具simian扫描(怎么使用自己去搜)。扫描重复代码,但维护成本过高,因为需要重构代码,没有删除代码来得直接(看自己的夜雾需求)。

(3).编译选项优化

这个最有用的一个选项是Strip Linked Product / Deployment Postprocessing / Symbols Hidden by Default 在release版本应该设为yes

原理是打开这两个选项后构建ipa会去除掉symbol符号,就是那些类名啊函数名啊啥的。这样子的影响就是运行时你没法进行线程回溯,符号都没了回溯了也是乱码。但是不会影响正常的崩溃日志生成和解析。在本机专门测试过,如果使用符号表来解析崩溃日志,则完全不受影响。

第二个 就是 Build Settings->Optimization Level有几个编译优化选项,release版应该选择Fastest, Smalllest,这个选项会开启那些不增加代码大小的全部优化,并让可执行文件尽可能小(不过默认就是如此)。


终于你也看完了这么多内容,你以为总算看完了,呵呵,你还是太年轻了,同志们注意了,我要开始装逼了,


接下来我要把更高级点的 iOS瘦身(从方案来自滴滴大神的分享,说实在的这也算是代码级别的瘦身,这不过这种方法发挥了极致

众所周知,代码之间存在调用关系。假设iOS APP的主入口为-[UIApplication main],则所有开发者的源代码(包括第三方库)可分为两类:存在一条调用路径,使得代码可以被主入口最终调用(称此类代码为被最终调用);不存在一条调用路径,使得代码最终不能被主入口调用(称此类代码为未被最终调用)。

假设有一个源代码级别的分析工具(或编译器),可以辅助分析代码间的调用关系,这样就使得分析最终被调用代码成为可能,剩下的就是未被最终调用的代码。

这种工具目前有成熟可用的吗?答案是肯定的,就是clang插件。除可用于分析未被最终调用代码外,clang还可辅助发现重复代码。作为LLVM提供的编译器前端,clang可将用户的源代码(C/C++/Objective-C)编译成语言/目标设备无关的IR(Intermediate Representation)实现。其可提供良好的插件支持,容许用户在编译时,运行额外的自定义动作。(后来想想其实clang插件可以做更多的事情)

我们的目标是使用clang插件减少包大小。其原理是,针对目标工程,基于clang的插件特性,开发者可以编写插件以分析所有源代码。编译过程中,将插件作为clang的参数载入并生成各种中间文件。编译完成后,还需编写一个工具去分析所有包含源码的方法(包括用户编写,以及引入的第三方库源代码),检查这些方法中哪些最终可被程序主入口调用,剩余即是疑似无用代码。简单的一个复查,移除那些确定无用的代码,重新编译,便可以有效去除无用的代码从而减少包大小。

首先“如何编写一个clang插件并集成到Xcode” (这个要不你们自己搜吧 实在篇幅命令很长 ,但是按照命令一步一步来很简单的,没啥技术含量)

第二“如何实现代码级别的包瘦身” (代码指的是OC中的形如-/+[Class method:\*]这种形式的代码,调用关系典型如下:)

@interface ViewController : UIViewController

@end

@implementation ViewController

- (void)viewDidLoad {

[super viewDidLoad];

[self.view setBackgroundColor:[UIColor redColor]];

}

@end

则称:-[ViewController viewDidLoad]调用了:

-[UIViewController viewDidLoad]

-[ViewController view](语法糖)

+[UIColor redColor]

-[UIView setBackgroundColor:]

我先缓缓啊 等会儿补充

推荐阅读更多精彩内容