最近项目末期, 我们团队为了 ipa 的大小使用不少的体积减小的方法, 除了一些常规的方法之外, 我分享一下自己研究出来的新思路。
首先我们来简单的介绍一下 mach-O。
什么是 mach-O?
Mach-O 格式全称为 Mach Object 文件格式的缩写,是 mac 上可执行文件的格式,类似于 windows 上的 PE 格式 (Portable Executable ), linux 上的 elf 格式 (Executable and Linking Format)。
(点击放大图像)
上面第一个图是苹果给出的mach-O 格式的示意图,而第二个图是我们使用machOView 来分析某个可执行文件中的armv7 的格式。可以看出他们两者的关系是对应的。
在machO 这其中包含了很多的有效的信息,包括字符串,代码段,oc 类,oc 协议等各种的信息,利用这些信息我们也做到分析代码或者程序逻辑的作用,比如,下面这个数据就是我从这个machO 文件里面导出来的,获取到了某个framework 一个OC 类中的所有基本元素。
(点击放大图像)
那什么又是FatFile/FatBinary
(点击放大图像)
简单来说,就是一个由不同的编译架构后的Mach-O 产物所合成的集合体。例如上面我就只截取armv7 的Mach-O 格式座位示例, 而实际上常用的还有arm64/x86_64/i386 等格式。
而实际上,包括我们使用的那些framework,大多数也是的。比如下图我们继续用machOView 分析一下。
可以看到arm64/armv7 架构的存在。
(点击放大图像)
FrameWork 跟最终可执行文件的区别在哪里?
这里我们先随便写一个简单的 framework, 在这个 framework 中我们实现了两个 oc 类,如下图所示:
(点击放大图像)
紧接着我们干净用machOView 来看看这个新鲜出炉的framework,可以看到该FrameWork 在armv7 的格式下,里面存在多个.o 文件。
(点击放大图像)
如果我们选择其中一个继续看看的话,你就会看到一个完整的Mach-O 格式的文件。
(点击放大图像)
由此我们可以得知frameWork 也是另外一种情况的Mach-O 集合体,是由多个不同的子mach-O 文件所组合而成的,他们可以单独的拆开,而可执行文件则把同一架构下的所有Mach-O 文件都进行了合并,他们不能拆开,如果想要更加清晰的定义的话,可以去研究一下苹果的定义,这里不做过多的阐述。
我们能做什么?
可能到这里你还有点乱,没关系,我们直接来拆开一个framework 给大家看看!
(点击放大图像)
到了这一步,我们就已经知道了我们能把FrameWork 中的各个子Mach-O 文件拆开, 那么我们能不能把这些Mach-O 文件中有效的部分重新组装一下, 生成新的一个FrameWork 呢?
这必须是可以的,但是其实最重要的关键是在于怎么去确定这个Mach-O 文件有没有被我们的程序使用到。
怎么确定一个Mach-O 有没有被使用到?
我们直接再来一个简单的demo,尝试一下
(点击放大图像)
此时进行编译
(点击放大图像)
成功…
然后拆分老的framework 文件, 删掉拆分得到的MachOClassA, 并且用剩下的mach-O 文件合并生成一个新的framework, 替换到工程中去并进行编译
(点击放大图像)
你没看错, 他确实是失败了, 如果在程序中代码直接使用了某些类或者某些方法, 而其mach-O 文件不存在的情况下, 会导致编译不过(找不到对应的方法), 这也就是说, 我们能够使用最简单粗暴的方法来判断这个machO 文件是不是被需要的!
不过, 需要注意的是, category 的实现方式是不一样的,故如果我们删除了category 的方法, 但是直接把Mach-o 删除的话, 编译时是不会报错的。有兴趣的同学可以自己去看看oc 中关于category 的实现。
另外还有在程序运行中动态使用的performselector 方法(可以通过查询字符串列表排除)。
下面是相关的流程图。
(点击放大图像)
怎么把Mach-O 的删除工具应用到XCode 中去
现在我们已经通过编译的手段获得了一堆mach-O 文件, 但是很多都是pod 中引进的, 这个时候我们需要在代码编译器执行删除.o 文件的脚本 刚好Xcode 确实有这么一个地方可以设置
(点击放大图像)
成果
在debug 模式下大概减少了0.5M, 实际二进制文件减小大概1.2M, 如果计算到最终提交到苹果并且经过DRM 加密后, 预计可以减小1M 左右。
(点击放大图像)
PS
category 是需要过滤的, 这货有点特别
删除找出来的.o 文件之后, 可能会引起一些特殊的情况, 当然一般是 crash, 因为有一些特别的代码他们用法并不是直接引用某个方法, 而是通过 NSString 相关的方法来获得 Sel 或者 Class
把查找.o 文件的操作放在本地, 而在编译器上进行编译的时候就直接执行删除, 不占用编译器的时间(我们的项目要使用六个小时以上的时间来进行查找)
建议进行操作再跑一遍回归测试, 确保各个功能模块正常
其他实践与猜想以及做过的尝试
.o 文件其实是可以直接引进到工程里面直接编译的, 也就是说其实可以把 frameWork 拆开, 然后加到工程中, 一样能够正常使用
一开始其实是想把.o 文件中 __text 段中无用的函数进行删除, 但发现流程过于复杂, 而暂时放弃, 查找程序中无用函数的方法以后有机会再进行分享(如果这个成功的话, 估计会减小至少 3~4M 左右的 ipa 大小), 附上查找到的程序中无用方法结果的示例
(点击放大图像)
其实看了上面那种方法之后, 我们紧接着又能想到, 暴力的将.m 文件中的代码删除, 然后看看哪些工程中可见的代码是可以删除的(ps. 主要针对非framework, 另外也同样需要注意category 以及performselector 的问题, 需要配合查找字符串列表一起进行, 或者手工进行判断)。
作者
靖明@阿里移动安全
感谢徐川对本文的审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ , @丁晓昀),微信(微信号: InfoQChina )关注我们。
评论