原文链接
苹果已经为我们提供了各式工具, 可以将一个复杂的 APP 分解为若干的 module, library 或 framework.
这里首先要说明的是, Framework 不仅仅是为了将代码和资源进行模块化打包管理, 也并非为了加速编译而生的. 它的主要作用是:
简化 App 工程的 code base.
加速 debug
打包代码, 让代码尽可能可重用
以一种更加被动的方式来达到形成代码边界的目的.
不过为了能理解 Framework 的作用, 还需要理解如下的内容:
Static Libraries
Dynamic Libraries
Framework 及其结构
Linking 和 Embedding 在 Xcode 中的区别
其他
下面对上述内容做一一介绍.
1 Staitic Libraries
为了明白 Static Libraries, 就需要知道 object
文件这个东西, 它是编译器生成的可重定位文件, 在链接时可以由链接器进行链接, 从而构成可执行文件.
object 文件有如下四种形式:
relocatable: 可重定位
executable: 可执行
shared: 可共享
bundle: 包
其中 relocatable 类型的 object 文件可以在编译的时候被静态链接, 从而形成一个可执行文件. executable 类型的 object 文件可以被直接装载到内存中执行. shared 类型的 object 文件是一种特殊的 relocatable, 文后会展开来讲这种类型的 object 文件. bundle 类型的 object 文件这里不详细将, 只要知道在 macOS 上它主要用于 plugins 上.
比如当编译一个不带 main 函数的 C 文件, 结果就会生成 relocatable 类型的 object 文件:
fancy.c
void fancySwap(int *xp, int *yp) {
if (xp == yp) return; // try it!
*xp = *xp ^ *yp;
*yp = *xp ^ *yp;
*xp = *xp ^ *yp;
}
cc -c fancy.c
结果就是生成了一个 fancy.o 文件, 里面的内容就是 fancy.c 文件经过编译器编译为机器码后的内容.
为了让多个 object 文件在使用的时候更加方便, 故将它们一起打包到一个 .a(代表 archive) 文件中. 而这个 .a 文件就是常说的静态库(static library)或静态包(static archive).
如果程序需要用到 .a 文件中的某些数据, 则链接器会自动且非常智能地链接需要的部分. 不过, 链接器会将 .a 的全部代码都拷贝并且重定位到可执行的 object 文件中, 这些代码将会拥有一个固定的静态地址, 这样就可以在执行的时候找到代码的位置了.
正因为有拷贝和重定位这些代码到可执行文件中的动作, 导致静态链接的可执行文件偏大, 进而导致可执行文件载入内存的时候会花费更长的时间. 另外, 如果在同一设备上的十个程序都是使用到同一个 .a 文件, 则相当于对这个 .a 文件在该设备上存放有十份副本, 且在运行时无法共用.
同时, 如果这个 .a 文件进行了升级, 则需要重新对整个 app 进行编译, 重新发布.
正是因为静态库这样的缺点, 故 dynamic library 横空出世.
(苹果在 Xcode 9.0 之后正式支持 static Swift libraries)
2 Dynamic Libraries
Dynamic library 又被称为共享库 (shared library) 或动态链接库 (dynamically linked library).
它也是 object 文件包, 且可以在程序装载或运行时被加载到内存的任意位置, 这个过程称为动态链接, 并且这个工作是由动态链接器(在 macOS 上是 dyld)完成. 常见的动态链接库文件后缀在 macOS 上是 .dylib
, 在 windows 上是 .DLL
, 在 linux 上是 .so
.
相比静态链接库在编译时由链接器进行静态链接, 动态链接库是在装载时或运行时由动态链接器进行动态链接.
运行时动态链接实际开销是相当大的, 如果有兴趣可以使用 man dlopen
来查看动态装载的一些概要信息. 不过在高性能服务器上, 运行时动态链接/装载还是非常常用的. 使用动态链接的程序可以在不影响程序运行的情况下动态替换/升级所用到的动态链接库.
另外使用动态链接库的话, 有的场景下还可以根据不同的请求来动态加载/卸载所需的不同动态链接库, 以减小内存占用.
如果还要举一个不怎么恰当的例子, 那就是有些 APP 可以动态加载/卸载 自身的插件.
不过动态链接库主要是要解决静态库的缺点, 有了动态链接库, 多个程序间可以共享磁盘上的同一个库, 且库的升级也不会需要程序重新编译(指接口不变的情况下). 并且动态链接库的依赖也可以自动被加载和链接(如果这些依赖是在 search path 中的话). macOS 对动态链接库的使用非常广泛, 可以查看 /usr/lib
文件夹内的内容. 如果想让你自己的库也可以被动态链接, 则可以有如下的方式:
将这个库嵌入(embedded)到 APP 包中
制作一个安装包, 将动态链接库在安装时放入到
/usr/local/lib
文件夹内, 由于/usr/lib
文件夹是操作系统的, 一般都是放到 local 下的 lib 文件夹内.
但是使用动态链接库的话, 总是要记住: 保证向前的兼容性, 即 API 的变化不会影响到之前的程序使用, 换句话说就是接口的稳定.
3 Frameworks
说了上面两个东西, 好像也就解决了全部问题了, 为什么还要有一个 Framework 的概念呢?
实际上大多数 Framework 的本质就是动态链接库加上所需资源的打包形式, 说白了就相当于是一个文件包而已. 在文件包里包含了诸如图片, xib 文件, 动态链接库文件, 静态库文件, 文档文件等等等等. 将这些文件打包到一个 Framework 中, 实际上是为了使用时更加方便.
下面就来看看一个 Framework 中的主要内容:
3.1 Headers 文件夹
在这个文件夹下包含的是 Framework 暴露给外界的 C 或 OC 头文件, Swift 并不使用这些 Header. 如果一个 framework 是用 swift 写的, Xcode 仍然会生成这个文件夹, 不过仅仅是为了让 Swift 和 OC 或 C 进行互操.
3.2 Modules 文件夹
这个文件夹内包含的是 LLVM 和 Swift 模块信息. clang编译器会使用到这里面提供的 .modulemap
文件, 详见这个链接. 另外在 framework名称.swiftmodule
文件夹内包含的是和 Header 文件夹类似的头文件. 只不过这里的文件是二进制形式的, 且还没有完全定型, 这里的文件内容就是当你在 xcode 中使用 cmd 加 左键点击某个 framework 方法时候进入看到的东西, 即只有相关信息, 而没有源码.
我们可以利用 llvm-bcanalyzer
或 llvm-strings
工具来查看这些二进制文件的内容, 因为这些文件遵循的是 llvm bitcode 的数据结构.
3.3 和 framework 同名的二进制文件
这里 Finder 将其标记为 executable, 实际它是一个动态链接库文件, 且类型是 relocatable, 即可重定位类型的.
4 Xcode 中 Linking 和 Embedding 的区别
下面就来看我们天天见, 却可能一直都没有搞清楚的东西, 就是 Xcode 中的通用设置里面的两个内容: Linked Frameworks and Libraries 和 Embedded Binaries.
这里先自问一下: 为什么每次添加到 embedded binaries 里面的 framework 都会自动出现在 link 那一栏呢?
有了上面的知识就可以回到这个问题了: 实际上 embedded 是将 framework 放入到程序的 bundle 中, 如果仅仅将 framework 放入到程序包中, 实际上什么作用也不会有, 只有把这个 framework 链接后, 程序才能使用它. 系统提供的 framework 早已被预置到系统的固定位置(比如之前提到的'/usr/lib'), 而自定义的 framework 此时当然不会在某个系统文件夹下, 故只有随程序一起放置, 当使用的时候再进行链接.
且这里也是在将动态链接和静态链接的概念给模糊化了, 如果引入的是一个静态库(.a)文件, 则 Xcode 是不允许将设置到 embedded binaries 中的, 因为静态链接库是在编译时就进行链接的, 并不需要复制到程序包中去链接.
5 Framework 的使用建议
下面是一些 Framework 的使用建议, 当考虑使用 Framework, 或是将代码打包到 Framework 中之前, 可以参考一下如下的建议:
如果是在开发一套程序集, 而又有很多代码可以复用的时候, 这些可复用代码的绝佳"住处"就是 Framework.
如果仅仅是在多个程序中存在一小部分公共代码, 则不建议使用 Framework, 因为开支大于收益.
如果是希望将代码模块化, 则完全没有必要使用 Framework, 因为 Swift 的访问控制工具才是正确的选择. 千万不要把 Framework 作为一种名空间机制来使用, 因为常常有人在工程中创建私有的 framework target 作为隔离可重用代码和 app 专用代码的手段(FireFox 的 iOS 客户端中也看到过).
不要把 Framework 作为 "减少编译代码时间" 的手段. 因为这样会将代码的复杂度提高, 且不利于维护, 实际收益并不会太高.
如果想要把代码共享给三方使用, 则这个时候可以考虑使用 framework.
6 一些命令行工具介绍
最后来介绍一些命令行工具, 可以协助日常调试:
file: 这个工具可以打印一个文件的详细信息.
nm: 列出一个 object 文件的符号表. 什么是符号表?
lipo: 不常用.
objdump: 非常好的查看 object 文件内部的工具.
ar: 一个将若干 relocatable object 文件打包为 .a 文件的工具.
llvm-bcanalyer: 它可以将 llvm bitcode 文件输出为人类可读的格式.
后记
关于符号表的相关信息还是不足, 需要补充.
另外在文中讲到的 Framework 的适用情况, 私有 Framework 来组织模块, 然后单独测试, 独立发布模块, 看似的确比较好, 但是为什么不推荐呢? code base 的代码量降低, 代码独立出来测试, 不是应该复杂度更低了吗?