iOS动态库和静态库

1,292 阅读11分钟

问题

静态库和动态库的区别

静态库

  • 静态库完全复制进可执行的二进制里面
  • 后缀是.a或者.framework
  • 存在符号冲突的问题
  • 启动速度快,浪费磁盘和内存

动态库

  • 动态库是在程序冷启动时候被链接到手机内存或者 App 内存里面
  • 后缀是.tbd或者.framework
  • 相同符号,使用第一个连接
  • 启动速度慢

动态库的共享缓存(dyld shared cache)

在iOS系统中,每个程序依赖的动态库都需要通过dyld(位于/usr/lib/dyld)一个一个加载到内存,然而如果在每个程序运行的时候都重复的去加载一次,势必造成运行缓慢,为了优化启动速度和提高程序性能,共享缓存机制就应运而生。所有默认的动态链接库被合并成一个大的缓存文件,放到 /System/Library/Caches/com.apple.dyld/ 目录下,按不同的架构保存分别保存着.

静态库使用分类

参考:www.jianshu.com/p/f7b0aa817…

Object-C的链接器并不会为每个方法建立符号表,而是为每个类建立链接符号。这样的话静态库中定义了已存在的类的分类,链接器就以为这个类存在了,不会将分类和核心类代码关联(合并)起来,这样在最后可执行文件中,就会找不到分类里所定义的方法。

可以通过other linker flags设置解决

  • -ObjC:链接器会将静态库中的每个类和分类加载到最后的可执行文件,当然,这个参数会导致可执行文件比较大,原因是加载了更多的额外对象的代码到可执行文件当中去,但是这会解决 Objec-C 中静态库中已存在的类包含的分类问题。

  • -all_load:把所找到的目标文件都加载到可执行文件当中去,但是这就存在一个问题了,如果两个静态库中,都使用了同一份可重定位的目标文件(这是一个很常见的问题,例如大家的目标文件都使用了用以名字 base64.o)就会发生 ld: duplicate symbol 符号冲突问题,所以不太建议使用。

  • -force_load:该参数的作用跟 -all_load 其实是一样的,但是 -force_load 需要指定要进行全部加载的库文件的路径,这样的话,只要完全加载一个库文件,不影响其余库的可重定位目标文件的按需加载。

  • -dead_strip:让 Clang 编译器帮助我们去除重复符号的可重定位目标文件问题。
    但是使用这个参数却有一个问题,就是如果我们使用了该参数,就不能使用 -all_load 或 -force_load。

基础概念

静态库

静态库的本质是一系列目标文件(.o)的集合,就是一个压缩包,其常见的表现形式为 .a 或者 .framework 后缀的文件,使用 file 命令可以看到其描述如下:current ar archive

优势:

  1. 不需要重复编译,在构建阶段直接参与链接就可以生成最终的可执行文件
  2. 相较于动态库,没有动态链接过程,启动速度更快

劣势:

  1. 无法直接使用,需要搭配头文件
  2. 静态库改变会导致所有下游依赖方重新编译
  3. 静态库冗余问题

动态库

和静态库类似,都是一系列目标文件的集合,区别在于动态库不会在构建阶段直接参与链接,而是在APP 运行时动态链接。其常见的表现形式为 .tbd.dylib 或者 .framework 后缀的文件,使用 file 命令可以看到其描述如下:Mach-o dynamically linked shared library

在标准系统中,动态库具备以下优势:

  1. 应用具备动态化能力,无需重新构建,就可以获得新的能力,支持插件化
  2. 多应用共享,降低内存占用,减小包大小

劣势:

  1. APP 不具备自完备性,依赖运行环境才能运行
  2. 应用启动阶段包含动态链接过程,启动速度慢于静态库

在 iOS8 系统之前,开发者只能使用静态库,也就是说 APP 所有的逻辑最终都会被打包到一个可执行文件中。iOS8 系统发布后,苹果引入了 App Extesion 特性,允许开发者在 APP 中受限制的使用“动态库”,以达到在宿主和扩展中共享代码和资源的目的。

在 iOS 系统中,受沙盒机制的限制,APP 只能访问自己沙盒或者同证书 APP Group 共享区域的内容,因此我们并不能使用真正意义上的动态库,苹果把这种机制下的动态库称为 Embedded Framework

相较于系统动态库,放置在系统目录下,所有应用程序共享,运行时只需加载一次即可,Embedded Framework 存在于应用的包中,应用间不可共享,系统在启动宿主应用或者扩展时,按需动态加载。

Framework

上文提到动态库和静态库都可以表现为一个 .framework结尾的文件,那么 Framework 是什么?

What are Frameworks?

A framework is a hierarchical directory that encapsulates shared resources, such as a dynamic shared library, nib files, image files, localized strings, header files, and reference documentation in a single package. Multiple applications can use all of these resources simultaneously. The system loads them into memory as needed and shares the one copy of the resource among all applications whenever possible.

根据苹果官方的解释,Framework 本质上只是一个目录结构,包含了动态库,nib、图片、头文件、文档等资源:

其中,Headers 存放公开的头文件,Info.plist 存放框架的基本信息,比如名字,版本号等,Modules 存放了该 Framework 作为 Clang Module 使用的必要信息,比如这里的 modulemap 文件。

从上图可以看到,无论是动态库还是静态库,Framework 所定义的目录结构是一样的。注意静态库的签名文件和动态库不一致,这也会为什么静态库不能 embed 到 APP 包中的原因,下文会提到。

库是怎么创建出来的

Xcode 在工程模板中提供了两种库模板:

Framework 用于创建动态库,或者包含资源文件的静态库,最终产物为 .framework 文件。

Static Library 用于创建纯代码的静态库,最终产物为 .a 文件。

静态链接库

静态链接库只需要经过源文件的编译,生成目标文件后,打包在一起就完成了所有工作。.framework 相较 .a 格式,多了一些 info.plistmodulemap 等处理。

动态链接库

如果我们选择 Framework 模板,则可以在编译设置中配置产物类型:

  1. Executable:经过编译的可以直接执行的二进制文件
  2. Dynamic Library:动态链接库
  3. Bundles:资源包
  4. Static Library:静态链接库
  5. Relocatable Object File:编译后得到的可重定位的目标文件

动态链接库的生成过程也和静态链接库不一样:

从图上可以看到,动态链接库生成过程中,除了编译源文件生成目标文件,还会多出链接的过程,待链接的目标文件信息来自于上边生成的 LinkFileList 文件。

使用库

在 Xcode 中引入一个库,有几种配置选项:

  1. 系统库是真正意义上的动态库,因此不需要嵌入到 APP 的包中,选择 Do Not Embed。
  2. 开发者创建的动态库库属于 Embedded Framework,需要嵌入到 APP 包中,并且使用 APP 签名所用的证书签名,才能在 APP 启动时,通过验签逻辑。
  3. 开发者创建的静态库如果以 Framework 形式呈现,这里不能选择 Embed,否则虽然可以打包成功,但是在上传 APP Store 或者用户安装(企业途径)时都会因静态库签名不被支持导致失败。

APP 上传 APP Store 之后,苹果会再做一次签名,因为拿不到私钥,所以开发者动态下载的库无法通过验签逻辑,进而无法实现动态下发库来实现动态化能力。

Status 配置库的依赖方式:

  1. Required 标记的库,会在 APP 启动时加载,不存在会直接终止应用,为默认选项。
  2. Optional 标记的库,同样会在 APP 启动时加载,但是不存在不会终止应用,如果运行时调用到了库中的符号,则会导致应用终止。一个典型的应用场景:如果我们的库在特定版本的系统上才能运行,那么可以将其标记为 Optional,然后在相关逻辑中判断系统版本,符合要求时才调用相关功能。

苹果在其文档中提到,把大型库标记为 Optional,有助于加快启动速度。

Required (the default) frameworks and libraries must be present at the time an app launches. Optional frameworks must be present when they are needed by the app. Launch time is faster when large libraries and frameworks are marked as optional.

上边两个选项在最终链接生成主二进制的时候,差异点如下:

从苹果的文档上可以大概了解到:

-framework(-library):默认选项,在应用启动时,由 dyld 加载,并进行 rebase、binding 等操作,如果出现符号找不到的情况,会产生一个运行时绑定错误,然后终止当前线程,即我们看到的启动崩溃。

-weak_framework(-weak_library):同样由 dyld 在应用启动时加载,但是如果找不到符号,会将该符号地址置为 NULL,然后继续后续流程。在使用这些符号时,我们必须显示的将其和 NULL 或者 nil 判断,然后调用。如果我们调用地址为 NULL 的符号,会产生一个运行时错误,随机终止当前线程。

weak 符号的判断必须显式的和 NULL 或者 nil 判等,不能使用取反预算法 !

在打包 APP 包时,ld64 参与最终的链接过程,对于上述两种链接参数,会在 Mach-O 文件中呈现以不同的 Command 类型:

在 dyld 中,针对上述两种 Command 的处理也不一样,对于使用 LC_LOAD_DYLIB 方式加载的强符号,动态加载过程会保证其定义全局唯一。而对于 LC_LOAD_WEAK_DYLIB 方式加载的弱符号,动态加载过程不会验证其全局唯一性,并且在链接之前,会把所有的弱符号打包在一起,后续的查找和符号决议过程相比强符号也会快很多。

动态链接的过程,对于强弱符号的处理还有很多差异,有兴趣的同学可以通过苹果开源的版本深入了解。

符号冲突和依赖关系

测试工程:

符号冲突

  1. 两个静态库不能暴露相同符号,否则会在链接的时候报符号重复:

  1. 两个动态库暴露相同的符号,并不会导致编译报错,最终会根据链接顺序,使用先参与链接的实现版本,存在逻辑风险:

当多个动态库暴露了相同的符号时,不同符号的加载不受影响,即相同符号用第一个参与链接的实现,不同符号正常加载。

使用类似的测试手段,还可以得到以下结论:

  1. 两个动态库 A、B 分别依赖动态库 C、D,C、D 暴露相同的符号,A、B 调用时会走各自依赖的实现
  2. 两个动态库 A、B 分别依赖静态库 C、D,C、D 暴露相同的符号,A、B 调用时会走各自依赖的实现,静态库代码会被链接到动态库中

依赖关系

  1. 静态库构建后的产物(.a.framework)会包含其依赖的 .a 静态库,但不会包含 .framework 静态库或者动态库。

  1. 同样的,动态库构建后的产物,会包含其依赖的 .a 静态库,但不会包含 .framework 静态库或者动态库

  1. .framework 的动态库或者静态库因为不会被链接到其上游依赖方,因此使用上游库的时候,还需要导入这些依赖
  2. -ObjC 配置能保证静态库的类别也被链接动态库或app工程生成的可执行文件里面:

增加 -ObjC 标记后,扩展成功链接到了产物中:

关于 -ObjC 相关的内容,见:Building Objective-C static libraries with categories