iOS中的库

什么是库

库(Library)可以粗暴分为源码库和二进制库。源码简单来说就是一堆可以复用的代码文件的集合,二进制库就是对源码库编译之后的二进制库文件。本文主要讨论的是iOS中二进制库。

动态库、静态库、framework

iOS端在使用的时候有两种link方式,静态链接和动态链接,于是便有了静态库和动态库。

我们可以根据文件类型直观的分辨动态库和静态库。
一般来说,动态库以.dylib或者.framework为文件后缀;静态库为.a或者framework.

严格来说framework并不是库的类型,它是一种打包动态库或者静态库后的包,通常里面还可以包含资源文件、配置文件等东西

静态库

静态库即静态链接库(Windows 下的 .lib,Linux 和 Mac 下的 .a)。之所以叫做静态,是因为静态库在编译的时候会被直接拷贝一份,复制到目标程序里,这段代码在目标程序里就不会再改变了。

静态库的好处很明显,编译完成之后,库文件实际上就没有作用了。目标程序没有外部依赖,直接就可以运行。当然其缺点也很明显,就是会使用目标程序的体积增大。

动态库

动态库即动态链接库(Windows 下的 .dll,Linux 下的 .so,Mac 下的 .dylib/.tbd)。与静态库相反,动态库在编译时并不会被拷贝到目标程序中,目标程序中只会存储指向动态库的引用。等到程序运行时,动态库才会被真正加载进来。

动态库的优点是,不需要拷贝到目标程序中,不会影响目标程序的体积,而且同一份库可以被多个程序使用(因为这个原因,动态库也被称作共享库)。同时,运行时才载入的特性,也可以让我们随时对库进行替换,而不需要重新编译代码。动态库带来的问题主要是,动态载入会带来一部分性能损失,使用动态库也会使得程序依赖于外部环境。如果环境缺少动态库或者库的版本不正确,就会导致程序无法运行(Linux 下喜闻乐见的 lib not found 错误)

framework

除了上面提到的 .a 和 .dylib/.tbd 之外,Mac OS/iOS 平台还可以使用 Framework。

Framework 实际上是一种打包方式,将库的二进制文件,头文件和有关的资源文件打包到一起,方便管理和分发,和静态库动态库的本质是没有什么关系。

在 iOS 8 之前,iOS 平台不支持使用动态 Framework,开发者可以使用的 Framework 只有苹果自家的 UIKit.Framework,Foundation.Framework 等。因为 iOS 应用都是运行在沙盒当中,不同的程序之间不能共享代码,同时动态下载代码又是被苹果明令禁止的,没办法发挥出动态库的优势,实际上动态库也就没有存在的必要了。
由于上面提到的限制,开发者想要在 iOS 平台共享代码,唯一的选择就是打包成静态库 .a 文件,同时附上头文件。

iOS 8/Xcode 6 推出之后,iOS 平台添加了动态库的支持,同时 Xcode 6 也原生自带了 Framework 支持。

制作静态库和静态库

制作静态库

新建一个工程,并创建一个framework类型的target,在build settings种修改Mach-O Type为static。创建一个简单的类,并添加一行简单的代码。
static
staticCode

Cmd+B,我们会在Products目录下发现编译完成的静态库framework

制作动态库

与制作动态库类似,创建一个dynamci类型的framework target,并加上简单的代码
dynamic
dynamicCode

测试frameworks

将制作完成的两种类型的framework拖入到测试工程,Cmd+R运行,会直接报错
loadError

报错信息为动态库没有被加载,我们查看发现手动拖入工程的动态库,Xcode并不会默认帮我们设置为Embed,对于静态库来说没有影响,但是动态库没有设置就会直接运行时加载错误。手动修改为Embed & Sign
embed
再次Cmd+R,运行成功
success

总结来说,在使用dynamic的时候需要配置为Embed & Sign,否则可能会出现运行时加载错误

两者在ipa中的区别

使用tree命令查看ipa包的内容如下:
tree

  • Headers:包含了Framework对外公开的 C & Obj-C headers, Swift 并不会用到这些 Headers, 如果你的 framework 是用 Swift 写的, Xcode 会自动帮你创建这个文件夹以提供互用性

  • .swiftmodule:包含了 LLVM, Swift 的 Module 信息. .modulemap 档案是给 Clang 使用的。.swiftmodule 文件夹下的档案类似 headers, 但是不像是 headers, 这些档案是二进制的且 无格式也有可能会改变, 在你 Cmd-click 一个 Swift 函数时 Xcode 就是利用这些档案去定位其所属的 module
    尽管这些都是二进制文件, 但他们仍是一种叫 llvm bitcode 的结构, 正因如此, 我们能用 llvm-bcanalyzer and llvm-strings 取得相关信息

  • Binary:虽然他被 finder 标注成 Unix executable File, 但他其实是一个 relocatable shared object file

  • .bundle:bundle 文件

  • 动态库是独立放在一个frameworks的目录下

  • 静态库则是会被一起打入testLib这个二进制文件中

如何判断为动态库还是静态库

我们可以用file命令来查看framework中的二进制文件的具体类型,

对于动态库:
fileDynamic

对于静态库:
fileStatic

动静态库的混用

我们可以在一个项目中使用一部分动态库, 再使用一部分静态库, 如果涉及到第三方库与库之间的依赖关系时, 那么遵守如下原则:

  • 静态库可以依赖静态库
  • 动态库可以依赖动态库
  • 动态库不能依赖静态库! 动态库不能依赖静态库是因为静态库不需要在运行时再次加载, 如果多个动态库依赖同一个静态库, 会出现多个静态库的拷贝, 而这些拷贝本身只是对于内存空间的消耗

cocoapods 中混合使用动静态库

在 动静态库的混用 中我们我们知道动态库不能依赖静态库, 因此在实际项目中会有一种需要特别注意的情况: 如果项目中有一个库必须是静态库时, 那么其整个依赖链路上的所有库都必须以静态库被引入, 如下图:
dependency

在 库 4 为静态库的情况下, 整个依赖链路上的所有库(库 5 与库 3)都必须以静态库形式被项目依赖

这时我们需要使用 cocoapods 在版本 1.5 之后推出的新功能: s.static_frameworks = true. 这个命令使用在库的 .podspec 文件中, 用来指定本库作为静态库被其他项目作为 包含静态库的 .framework 文件 引入. 这样我们就可以在开发库的时候手动指定本库被以静态库还是动态库形式被引入了.

动态库的加载时机以及为什么动态库不能动态加载

在 iOS app 启动时系统会查找我们所依赖的所有动态库并加载, 这降低了我们 App 的启动速度, 那么可不可以将动态库的调用时间延迟到 app 运行时? 答案是不能!

不能动态加载动态库的原因是系统的限制. 查看苹果的 API 文档, 会发现有一个方法提供了加载可执行文件的功能, 那就是 NSBundleload 方法 (底层实现为 dlopen 函数), 如下所示:
bundle

然而, 这个方法的使用是有前提的. 那就是库和 app 的签名必需一致. iOS 可能是出于安全考虑, 在加载可执行代码前, 需要校验签名. load 方法的内部实现是调用了 dlopen, 而真机的 dlopen 内部还会调用 dlopen_preflight 先校验签名. 如果库不是事先打包进 app(打包进 app 的话会与 app 有相同签名), 就会报签名错误, 从而加载不成功. 如下图所示:
testcode

因此动态加载加载动态库在模拟器上可以实现, 但是真机上不能运行

那么肯定有人会问, 既然无法加载成功, 苹果为什么要提供这个方法? 答案是, 虽然 iOS 无法使用, 但是 Mac OS 可以使用, 很明显这个方法目前是提供给 Mac OS 使用的. 如果以后系统放开签名校验, 那么 iOS 中也就可以动态加载了.

总结

  • 动态库不能依赖静态库
  • .a 是纯二进制文件, .framework 中除了有二进制文件之外还可以有资源文件。.a 文件不能直接使用, 至少还要有 .h 文件配合, .framework 文件可以直接使用, 因为本身包含了 .h 文件 和其他文件
  • .a.hsource = .framework, 建议使用 .framework
  • 静态库与动态库区别:
    静态库: 链接时完整地拷贝至可执行文件中, 被多个依赖多次使用就会有多份冗余拷贝.
    动态库: 链接时不复制, 程序运行时由系统动态加载到内存, 供程序调用, 系统只加载一次, 多个程序共用, 节省内存.(这个优点是针对系统动态库来说的, 比如 UIKit.framework)
  • 如果想通过 cocoapods 制作一个静态库被其他项目依赖, 那么可以在 pod 的 podspec文件中使用 s.static_framework = true 命令, 这个命令会使 pod 变为由 .framework 包裹的静态库 (即使项目的 Podfile 中使用了 use_frameworks! 时使用 pod 也会以静态库使用), 这在解决 动态库不能依赖静态库 的问题上非常有用
  • 系统动态库和自己编译的动态库本质上是一样的, 只是使用方式不一样. 自己编译的动态库由于签名校验限制, 只能当作静态库一样使用; 系统的动态库不受签名校验限制, 可以动态加载
    本篇介绍了静态库和动态库最基本的一些概念和区别,下篇会介绍实际中更多使用的Cocoapods集成中区别。

https://zhuanlan.zhihu.com/p/346683326
https://www.jianshu.com/p/e414032f5671