0x01 前言
在开始开发一个 SDK 之前,知道 SDK 各种类型对应的文件属性,可以帮助我们进行基本的选择。然后我们应该要知道的是开发一个 SDK 并不是就像写一个开源库一样简单随意。所以,稳定的实现要求的基础就从这篇文章开始吧。
0x02 用 Xcode 生成五种 Mach-O 类型的二进制文件
我们从 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 会有什么更具体的区别。继续实验。
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 | |
Dynamic Library | 31KB | |
Bundle | 31KB | |
Relocatable Object File | 18KB |
文件大小(柱形图)
大小都差的不多,±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 同学的指正。添加补充如下:
只有相同内容,且有着相同签名的动态库,才可以进行系统级别的共享。
而为了保证这一点,动态库生成的二进制文件并没有被合并到可执行二进制文件中。
否则。
试想,几乎每一个 iOS 应用都使用到了 Foundation 和 UIKit 中的函数方法,来构建界面或者进行基本的数据处理,如果每一个应用都嵌入了这样的两个库,那整个系统中就有多余的 2(n - 1) 个库了。这多恐怖,多铺张浪费啊。????。
再来看看这个动态库的结构:
可以看到左边,有一大坨的东西指定了需要依赖链接的其他二进制文件的路径信息。而右边呢就只是一些基本的程序代码信息了。
如果我们不采用动态库的方式来进行 SDK 的开发,更浅显的原因可能是。
要 Embedded Library 啊。因为是不与可执行二进制文件合并的文件,需要存储在 .app/ 的 Framework/ 目录下,所以需要在 Xcode 中进行 embedded。而对于开发经验稍显不足的同学来说,就会在这里,有些许困惑。我们开发 SDK,希望最大程度上的使得开发者减少工作量,这里可以做一点小的体现。
Bundle
当我看了 Bundle 的 Mach-O 结构后,我有点懵逼。这家伙玩的是一手伪装术嘛。
我们来对比一下 Bundle 和 Dynamic Library 的结构:
这根本就是同一个东西嘛,换了一个名字就来浑水摸鱼。
但其实一样吗?至少有一样东西是不一样的。我们使用类型为 Dynamic Library 这种 Mach-O 类型的二进制文件,我们需要在 Xcode 中进行嵌入,但是直接使用 Bundle 则会受到 Xcode 的拒绝,要求手动加载。
但是苹果已经很明确禁止,在程序运行的时候,手动加载可执行代码,这种行为,是具有绝对的危险的。我们做 SDK 的,一定要规避这样的风险。
所以这里,给到 Bundle 的戏份也从这里结束了。
Static Library && Relocatable Object File
时间稍微推前一两个月,如果你问我开发 SDK 的 Mach-O 格式应该选择什么,我会毫不犹豫的告诉你。
我不知道。
额,是静态库。
但是今天,说到这里,是想说有一个和之前不同的结论。那就是 Relocatable Object File。先不管那么多,来看看对比的结构图。
这张图比较寡淡啊,只可以看出 JustForTest.o 的结构和 Relocatable Object File 的整个很像。
当然,那是因为 JustForTest.o 的内容就是 Relocatable Object File 中的内容了,.o 文件经过进一步的 ar 操作可以归档成静态库文件。
可以简单的理解 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 |
文件大小(柱形图)
0x06 结论
可以看到文件较小的时候,二进制文件最小的是 Relocatable Object File 这个类型。而当编译的 .m 文件开始比较多的时候,却是动态库比较占优势。
因为我们一般编译的文件都会超过 AFNetworking 这个类库,所以我们就按照大文件的结果来进行结论总结。
如果使用动态库,需要考虑的是:
缺点:
使用上的区别,没有静态库那么方便。
可能性的风险,苹果政策随时可能发生变化。
对于保密要求高的线下渠道 SDK,可能会被从 .app/ 中单独拿出来,反编译研究具体实现。
优点:
全家桶代码共享。
二进制文件小。 (这一点非常重要)
如果使用静态库,则比较安全一点,比较保险。而如果要使用静态库,建议使用 Relocatable Object File 来减少二进制文件的大小。
最后,还要说的是,最好是同时采取 Dynamic Library 和 Relocatable Object File 两种方式来进行代码分发。提供多样的选择,满足不同的需求。
完。