浅谈 SDK 开发(一)五种 Mach-O 类型的凛冬之战

  

0x01 前言

在开始开发一个 SDK 之前,知道 SDK 各种类型对应的文件属性,可以帮助我们进行基本的选择。然后我们应该要知道的是开发一个 SDK 并不是就像写一个开源库一样简单随意。所以,稳定的实现要求的基础就从这篇文章开始吧。0x02 用 Xcode 生成五种 Mach-O 类型的二进制文件

  

  Mach-O_Type_Xcode

我们从 Xcode 入手,在 Build Setting -> Mach-O Type 这个选项下,有着这五个类型。而如果我们创建一个 Framework,不手动修改的默认配置即为 Dynamic Library(动态库)。

文件类型 看不懂的描述
Executable 可执行二进制文件
Dynamic Library 动态库
Bundle 非独立二进制文件,显式加载
Static Library 静态库
Relocatable Object File 可重定位的目标文件,中间结果

  一般情况下,制作一款 SDK,无论是什么产品模式,都会考虑的是要使用 Dynamic Library(动态库)还是 Static Library(静态库)。但是极少有人真的考虑过是否要使用另外的三种类型来制作 SDK。

  先问可不可以,再问为什么。现在就来试试分别使用 Executable, Bundle, Relocatable Object File 这三种类型来制作一款 SDK,看看是否具有实操性。

  Executable

  创建一个 Framework 项目,更改 Build Setting -> Mach-O Type 为 Executable。CMD + B 编译项目。可以看到如下两个错误。

clang: error: invalid argument '-compatibility_version 1' only allowed with '-dynamiclib'
clang: error: invalid argument '-current_version 1' only allowed with '-dynamiclib'

  根据提示去除默认的 -compatibility_version 和 -current_version 两个配置,继续编译,还是出现了如下错误。

ld: entry point (_main) undefined. for architecture x86_64

注意:因为是使用的模拟器进行编译,所以是 x86_64 架构。

对于 Executable 类型,这里基本是终点了。可执行文件会直接被系统执行,所以需要一个 _main 函数入口,而需要创建和使用的 SDK 明显是不符合这个条件的。放弃这个 Mach-O 类型,继续实验。

  Bundle

创建一个 Framework 项目,更改 BuildSetting -> Mach-O Type 为 Bundle。同样修改了 -compatibility_version 和 -current_version 两个配置之后,CMD + B 编译成功。

当然,这样还是不知道生成的 Framework 是否可以使用。所以我们建立对应的 Target 来进行测试。

建立 Demo 并将生成的 Framework 嵌入进应用后,CMD + B 出现如下的错误:

ld: can't link with bundle (MH_BUNDLE) only dylibs (MH_DYLIB) file '/Users/cbangchen/Library/Developer/Xcode/DerivedData/SDK_Mach_O_Type_Bundle_Demo-gzjgmzohehdzvvbknavovstdtyfm/Build/Products/Debug-iphonesimulator/SDK_Mach_O_Type_Bundle_Demo.framework/SDK_Mach_O_Type_Bundle_Demo' for architecture x86_64

提示明确的说明了,系统不支持链接 MH_BUNDLE 类型的文件。

这样就结束了吗?不如挣扎一下。

回头看看上面那个表格,对应 Bundle 类型的描述写的是 —— 非独立二进制文件,显式加载。这描述我自己都看不明白,但既然是 Bundle 文件,就来试着用平时的方式加载一下吧。

首先将 Bundle 用资源加载的方式(Build Phases -> Copy Bundle Resources)进行添加。然后在 ViewController 里面写入下面的语句。

  NSString *bundleString = [[NSBundle mainBundle] pathForResource:@"SDK_Mach_O_Type_Bundle_Demo" ofType:@"framework"];

  NSBundle *SDKBundle = [NSBundle bundleWithPath:bundleString];

使用上面的语句获取到 Bundle 文件。好像还缺了一点东西。

[SDKBundle load];

把它给 load 掉。再用 Runtime 来看看有没有 load 成功。

Class justForTestClass = NSClassFromString(@"JustForTest");

  [justForTestClass performSelector:@selector(justForTestMethod)];

  

  SDK_Mach_O_Type_Bundle_Log

输出了预设的语句。普天同庆,它跑起来了。

从这里开始就变得有点意思了,但我们先不探究这种方式制作出来的 SDK 会有什么更具体的区别。继续实验。

  Relocatable Object File

这个类型的名字可够长的,打的我手抖。创建一个 Framework 项目,更改 BuildSetting -> Mach-O Type 为 Relocatable Object File。同样修改了 -compatibility_version 和 -current_version 两个配置之后。CMD + B 编译出现如下错误:

ld: -r and -dead_strip cannot be used together

  移除 Dead Code Stripping,CMD + B,如下:

d: -rpath can only be used when creating a dynamic final linked image

这就有点扎心了,赤裸裸的鄙视啊,但还是把 -rpath 移除。CMD + B 编译成功。

同样开一个 Target 来测试一下使用。

... 时间过去了一分钟。

这个就比较简单了,没有搞什么幺蛾子,Link 一下就成功了。到这里就开始又变得有趣了,我们除了传统的 Static Library 和 Dynamic Library 之外又多了两种可用的 SDK 类型。相比来说会有什么区别呢?继续往下看。

  0x03 四种 Mach-O 类型的二进制包大小比较

在确定可用的情况下,我们先不探讨使用上是否方便的问题。因为如果你认真看了前面的内容,会知道 Bundle 类型的 Library 需要进行手动加载(劣势扎心啊,老表)。那我们给它一个机会,先来看看同样内容的文件最后生成的包大小分别为多少。

// .h

  + (void)justForTestMethod;

  // .m

  + (void)justForTestMethod {

  NSLog(@"�� 浅谈 SDK 开发");

  }

内容设置为上面的几行代码。分别进行项目编译,单个架构选择为 x86_64。

  文件大小(表格)

文件类型 二进制包的大小 不容置疑的证据
Static Library 21KB

  

  Mach-O_Type_Library_Static_Size

Dynamic Library 31KB

  

  Mach-O_Type_Library_Dynamic_Size

Bundle 31KB

  

  Mach-O_Type_Library_Bundle_Size

Relocatable Object File 18KB

  

  Mach-O_Type_Library_ROF_Size

  文件大小(柱形图)

  

  Mach-O_Type_Library_Size-c450

大小都差的不多,±13KB。但这是因为我们用来编译生成的文件内容也很少的原因。简单分析一下,可以看到 Bundle 不太争气,文件有点大。Relocatable Object File 这个类型的文件大小则是出乎意料的最小,才 18KB。

SDK的体积是很重要的一部分,同样的功能可以做到更小,是一件喜闻乐见的事情。

而其实如果是我,看到这里,一定会非常好奇,很想知道为什么 Relocatable Object File 可以相对减少那么多的体积,而 Bundle 却烂泥扶不上墙。那么我们一起进入下一部分的探索。

  0x04 来看一下各类二进制文件的结构Executable

格式并不是决定一个文件是否是可执行文件的原因,在 Windows 操作系统下,可执行程序可以是 .exe 文件, .sys 文件或者 .com 等类型的文件。

说起来比较玄乎,不如简单的理解为是操作系统允许的,可以单独运行的文件。(可以运行,不代表不会闪退啊,笑)。

Executable 需要一个 _main 入口。

当然,前面我们也说了 Executable 需要一个执行入口,而给 SDK 设置一个执行入口,并不是一件理智的事情,所以这篇文章,给到 Executable 的戏份就到这里了。回见 ��。

  Dynamic Library

动态库对于 iOS 系统的构成有着极其重要的意义。把共同的代码抽离出来,保证系统运行过程中,相同内容的库只被加载一次。解耦重用就是动态库出现的直接理由。

注意

  这里受到 @ValiantCat同学的指正。添加补充如下:

  只有相同内容,且有着相同签名的动态库,才可以进行系统级别的共享。

而为了保证这一点,动态库生成的二进制文件并没有被合并到可执行二进制文件中。

否则。

  

  SDK_Mach_O_Type_ Redundant_Library-c450

试想,几乎每一个 iOS 应用都使用到了 Foundation 和 UIKit 中的函数方法,来构建界面或者进行基本的数据处理,如果每一个应用都嵌入了这样的两个库,那整个系统中就有多余的 2(n - 1) 个库了。这多恐怖,多铺张浪费啊。��。

再来看看这个动态库的结构:

  

  Mach-O_Type_Library_Dynamic_MachO_Str_PNG-c45

可以看到左边,有一大坨的东西指定了需要依赖链接的其他二进制文件的路径信息。而右边呢就只是一些基本的程序代码信息了。

如果我们不采用动态库的方式来进行 SDK 的开发,更浅显的原因可能是。

要 Embedded Library 啊。因为是不与可执行二进制文件合并的文件,需要存储在 .app/ 的 Framework/ 目录下,所以需要在 Xcode 中进行 embedded。而对于开发经验稍显不足的同学来说,就会在这里,有些许困惑。我们开发 SDK,希望最大程度上的使得开发者减少工作量,这里可以做一点小的体现。

  Bundle

当我看了 Bundle 的 Mach-O 结构后,我有点懵逼。这家伙玩的是一手伪装术嘛。

我们来对比一下 Bundle 和 Dynamic Library 的结构:

  

  Mach-O_Type_Library_Dynamic_Bundle_MachO_Different_PNG-c45

这根本就是同一个东西嘛,换了一个名字就来浑水摸鱼。

但其实一样吗?至少有一样东西是不一样的。我们使用类型为 Dynamic Library 这种 Mach-O 类型的二进制文件,我们需要在 Xcode 中进行嵌入,但是直接使用 Bundle 则会受到 Xcode 的拒绝,要求手动加载。

但是苹果已经很明确禁止,在程序运行的时候,手动加载可执行代码,这种行为,是具有绝对的危险的。我们做 SDK 的,一定要规避这样的风险。

所以这里,给到 Bundle 的戏份也从这里结束了。

  Static Library && Relocatable Object File

时间稍微推前一两个月,如果你问我开发 SDK 的 Mach-O 格式应该选择什么,我会毫不犹豫的告诉你。

我不知道。

额,是静态库。

但是今天,说到这里,是想说有一个和之前不同的结论。那就是 Relocatable Object File。先不管那么多,来看看对比的结构图。

  

  Mach-O_Type_Library_Static_ROF_MachO_Different_PNG-c450

这张图比较寡淡啊,只可以看出 JustForTest.o 的结构和 Relocatable Object File 的整个很像。

当然,那是因为 JustForTest.o 的内容就是 Relocatable Object File 中的内容了,.o 文件经过进一步的 ar 操作可以归档成静态库文件。

  

  Mach-O_Type_Merge_PNG-c450

可以简单的理解 Relocatable Object File 是组装静态库和动态库的零件,而静态库和动态库就是可执行二进制文件的组件。这里用了零件和组件的概念,零件是不可缺少的,组件则是可选的。正好形容 .o 文件,静动态库和可执行二进制文件之间的关系。

  Ox05 Dynamic Library,Relocatable Object File 和 Static Library 的再次对比

虽然前面说到了 Dynamic Library 具有使用上的一点小小的不方便,但是如果动态库的使用依然是被要求的,而且可以为项目带来提升,就需要被考虑。

前面说到了 Relocatable Object File 是组装 Static Library 的零件,所以 Size 要小于 Static Library。但我们只做了编译单个文件情况下的文件大小对比。

现在就来看一下,多个 .m 文件编译下的 Dynamic Library,Relocatable Object File 和 Static Library 的二进制大小。

用 AFNetworking 来做个例子,源文件有 273KB。

  文件大小(表格)

文件类型 二进制包的大小 不容置疑的证据
Static Library 780KB

  

Dynamic Library 431KB

  

Relocatable Object File 601KB

  

  文件大小(柱形图)

  

  -c4500x06 结论

可以看到文件较小的时候,二进制文件最小的是 Relocatable Object File 这个类型。而当编译的 .m 文件开始比较多的时候,却是动态库比较占优势。

因为我们一般编译的文件都会超过 AFNetworking 这个类库,所以我们就按照大文件的结果来进行结论总结。

如果使用动态库,需要考虑的是:

缺点:

  1. 使用上的区别,没有静态库那么方便。

  2. 可能性的风险,苹果政策随时可能发生变化。

  3. 对于保密要求高的线下渠道 SDK,可能会被从 .app/ 中单独拿出来,反编译研究具体实现。

优点:

  1. 全家桶代码共享。

  2. 二进制文件小。 (这一点非常重要)

如果使用静态库,则比较安全一点,比较保险。而如果要使用静态库,建议使用 Relocatable Object File 来减少二进制文件的大小。

最后,还要说的是,最好是同时采取 Dynamic Library 和 Relocatable Object File 两种方式来进行代码分发。提供多样的选择,满足不同的需求。

完。

  • 作者:陈狗剩

  • 链接:https://juejin.im/post/59799780f265da3e1e5bc53e

  • 程序员大咖整理发布,转载请联系作者获得授权

声明:本文由入驻搜狐公众平台的作者撰写,除搜狐官方账号外,观点仅代表作者本人,不代表搜狐立场。
推荐阅读