iOS开发

iOS 中的静态库与动态库

一、前言


这篇主要是记录一下 iOS 下静态库与动态库的打包流程,以便以后用到时快速查阅,供自己也供大家学习记录。同时也简述了一下动态库与静态库的区别。

如果你经常困惑 iOS 开发中的静态库和动态库的作用与区别, 那么这篇文章可以为你解惑

库是写好的现有的,成熟的,可以复用的代码。本质上来说库是一种可执行代码的二进制形式,可以被操作系统载入内存执行。库有两种:静态库和动态库。

二、静态库与动态库


静态库 (Static Libraries)

链接时会被完整的复制到可执行文件中,被多次使用就有多分拷贝。如下图,抖音和微信的可执行文件加载静态库时,每个可执行文件链接时都要加载这份静态库。

静态库简单的理解是多个目标文件 (object file, 以 .o 为后缀) 的打包集合. 静态库的存在形式:

  • Mac/iOS: .a 或封装成 .framework
  • Linux: .a
  • Windows: .lib

优势

  • 提供的是目标文件, 所以不需要重新编译, 只需要链接即可
  • 加载 App 速度更快, 因为在编译时已经进行了链接, 因此启动时不需要进行二次查找启动

动态库 (Dynamic Libraries)

动态库 (Dynamic Libraries, 也称作 Shared Library, Shared object, 动态链接库), 跟静态库一样是多个 object files 封装起来的, 但是动态库并不会在编译时直接置入 app, 而是将动态库的信息置入 app, 然后 app 在被运行的时候去动态查找动态库并进行链接, 这一步也叫做 动态链接.

根据动态库的载入时间 (load time) 我们将动态库分为以下两种:

  • 动态链接库: 在启动 app 时立刻将动态库进行加载 (随程序启动而启动)
  • 动态加载库: 当需要的时候再使用 dlopen 等通过代码或者命令的方式来加载 (在程序启动之后)

以上行为是由动态链接器 (Dynamic linker, macOS 称 dyld) 来完成

动态库的存在形式分为以下几种:

  • MacOS/iOS: .tbd, .dylib 或封装成 .framework
  • Linux: .so
  • Windows: .DLL

macOS 大规模地使用 shared libraries, 可以前往路径 /usr/lib 文件夹查看系统的动态库.

然而在运行时进行才做链接其实是一个笨重的负担, 应合理安排哪些库需要 load 以及时机.

因为动态库不需要在编译时置入 app 中, 因此理论上体积会更小, 而且可以做到动态库内容改变所有结果文件不需要重新编译即可获得最新功能

❝以上只是对于标准的系统动态库来说的, 对于 iOS 开发来说, 因为我们只能使用 Embedding Frameworks 来使用动态库, 这样的动态库并不是真正的动态库, 其会在编译时全部置入 app, 然后在 app 启动时全部加载, 这样的话会导致体积大, 加载速度慢.

系统的动态库与我们自己打包的动态库还不大一样,先来看一下系统的动态库。

链接时不复制,程序运行时由系统动态加载到内存,系统只加载一次,多个程序共用,节省内存。
例如下图:抖音和微信的可执行文件加载动态库时,内存中只有一份动态库,不用加载到可执行文件。

系统的动态库

自己打包的动态库:则是在应用程序里的,但是与静态库不同,它不在可执行文件中。

自己打包的动态库

三、对framework的类型判断


上边介绍了了不同类型的framework,那么对于最常用的动态库静态库类型我们如何去判断呢。

  • 首先切换到framework目录下,cd xx.framework
  • 运用file命令,file xx 注释:xx为.framwork下的二进制文件
  • 判断:动态库包含“dynamically linked shared library”字样

四、制作静态库与动态库


1、制作静态库

我们在 iOS 下能创建的静态库分两种,分为 .a.framework 两种。

1.1、制作 .a 静态库

  • 创建:首先创建一个工程,选择为 Static Library 类型。
  • 编译

然后我们在 Products 下就可以看到一个 .a 文件,但是现在它还是不存在的,需要我们 command+B 编译一下,就可以找到它了,由于我们是在模拟器下编译的,所以它生成在 “Debug-iphonesimulator” 文件夹下。

如果添加了新的 .h 文件,需要在 Phases 中把头文件加上,此处 FanjiduoStatic.h 是创建工程时默认加进去了。

然后它会自动把你的头文件放到 include 里面

2、制作 .framework 静态库

  • 首先创建一个工程,选择为 Framework 类型。

创建好发现是 .framework 结尾了

  • 这里要设置一下,因为默认是动态库,要改为静态库。Build Settings => Mach-O Type 改为 Static Library

3、测试静态库

  • 编写测试类

首先编写了一个 Student 类,实现了一个 study 的打印方法,
然后把 Student 类 import 进了 FanjiduoFrameworkTest.h 总的头文件里面。

  • 公开头文件

然后在 build Phases => Headers 中把头文件公开,从默认的 Project 中拖拽到 Public

然后再编译一下,就大功告成了。

  • 测试

建立一个名为 staticTestDemo 的工程引入 framwork 试一下,发现可以成功调用 study 方法!

4、制作动态库

  • 制作步骤

动态库有很多种,目前像 .tbd .dylib 这种的动态库我们无法打包,只能打包 .framework 格式的动态库。
系统默认设置的就是动态类型,然后就是将新添加的头文件公开,整个流程和 .framework 静态库一样,这里就不再赘述了。

  • 测试

创建一个项目来测试一下这个动态库,直接拖入工程,引入头文件,调用动态库中的方法,然后编译,编译通过没问题。
但是当我们运行的时候,会报下面的错误:

其实这是因为没有将它添加到 Embedded 中,在老板的 Xocde 中,Embedded 是单独设置的,但是不知道从那一版 Xocde 开始,Embedded和其他设置合并在一起了。现在我们只需要在 “General => Frameworks, Libraries, and Embedded Content” 中,把 “Do Not Embed” 改为 “Embed & Sign” 即可,如下图:

然后就可以正常运行了

五、iOS 开发中 .framework 及动 / 静态库的区分


标准的动态库与静态库定义如上, 但是在 iOS 系统中, Apple 为我们提出了另一种可以包含依赖库的模式 — .framework

一个 .framework 其实就是一个有着特定结构的文件夹装着各种共享的资源. 这些资源通常是 图片, Xibs, 动态库, 静态库, 文档 等, .framework 毫不掩饰的表明它纯粹就是一个文件夹.

由于有 .framework 的存在, 我们在判断一个库到底是静态库还是动态库就有了麻烦, 因为一个 .framework 既可以是动态库也可以是静态库, 依赖于其内部的文件类型, 而.framework中的二进制文件有可能有后缀, 也有可能没有后缀.

为了区分其类型我们可以借助MachOView, 或者是在 Xcode 的 Targets -> build setting 中查找 mach-o type 选项.

动静态库的混用

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

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

六、结合实际 – CocoaPods 中的动态库静态库使用

静态库使用

默认情况下, 当我们在 Podfile 文件中写下:

platform :ios, '10.0'
source 'https://cdn.cocoapods.org/'

target 'HLTest' do
  pod 'AsyncSwift'
end

的时候, cocoapods 默认会使用静态库, 我们可以在 Products 文件夹中看到编译出的 .a文件

在项目的 .app 中, 我们可以看到静态库被编译进入可执行文件 (mach-o 文件), 导致文件大小为 14.9M

动态库使用

cocoapods 提供了 use_frameworks! 选项让我们可以以 .framework 的形式导入第三方库, cocoapods 默认我们开启了此选项后在 .framework 文件夹中放的是动态库, 因此我们可以在 Podfile 中加入 use_frameworks! 来达到引入动态库的效果, 如下:

platform :ios, '10.0'
source 'https://cdn.cocoapods.org/'
use_frameworks!

target 'HLTest' do
  pod 'AsyncSwift'
end

然后经过 pod update 之后, 结果如下:

cocoapods 编译生成的结果文件已经变为了 .framework 文件夹

再来看项目结果文件 .app:

我们可以看到

  • 由于动态库未被编译进入可执行文件 (mach-o 文件), 导致文件大小减小到 14.8M
  • 多了一个 Frameworks 文件夹用于存放 .framework 文件

cocoapods 中混合使用动静态库

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

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

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

总结


  • 动态库不能依赖静态库!
  • 对于 Swift 项目, CocoaPods 提供了 .framework 的支持, 通过 use_frameworks!选项控制. 需要注意的是如果使用此选项那么所有依赖的 pod 都会以 .framework 包裹的动态库类型引入, 如果想让某些 pod 使用动态库引入, 某些 pod 使用静态库引入, 那么请看下面
  • .a 是典型的静态库, 在 Xode -> File -> New -> Project 中的 Static Library 即可新建 .a 静态库
  • .framework 可以做成静态库, 也可以做成动态库, 在工程中修改某个 target 的 Build SettingMach-O Type 即可. 在 Xode -> File -> New -> Project 中的 Static LibraryFramework 即可新建 .framework 静态库
  • .a 是纯二进制文件, .framework 中除了有二进制文件之外还可以有资源文件. .a 文件不能直接使用, 至少还要有 .h 文件配合, .framework 文件可以直接使用, 因为本身包含了 h 文件 和其他文件
  • .a.hsource = .framework, 建议使用 .framework
  • 静态库与动态库区别:
    • 静态库: 链接时完整地拷贝至可执行文件中, 被多个依赖多次使用就会有多份冗余拷贝.
    • 动态库: 链接时不复制, 程序运行时由系统动态加载到内存, 供程序调用, 系统只加载一次, 多个程序共用, 节省内存.(这个优点是针对系统动态库来说的, 比如 UIKit.framework)
    • 系统的 Framework 不需要拷贝到目标程序中, 我们自己做出来的 Framework 哪怕是动态的, 最后也还是要拷贝到 App 中, 因此苹果又把这种 Framework 称为 Embedded Framework.
  • 当不想发布代码的时候, 也可以使用 Framework 发布 Pod, CocoaPods 提供了 vendored_framework 选项来使用第三方 Framework
  • 如果想通过 cocoapods 制作一个静态库被其他项目依赖, 那么可以在 pod 的 podspec文件中使用 s.static_framework = true 命令, 这个命令会使 pod 变为由 .framework 包裹的静态库 (即使项目的 Podfile 中使用了 use_frameworks! 时使用 pod 也会以静态库使用), 这在解决 动态库不能依赖静态库 的问题上非常有用.
  • Mach-O 格式的几种文件和 iOS 工程 Build Settings 里面的配置项是对应的.
  • 系统动态库和自己编译的动态库本质上是一样的, 只是使用方式不一样. 自己编译的动态库由于签名校验限制, 只能当作静态库一样使用; 系统的动态库不受签名校验限制, 可以动态加载.
  • .a.framework 都是库 (Library), 库都是二进制的, 看不到源码的, 只能看到头文件, Cocoapods 方式集成的可以看到源码是因为将源码放在一个新构建的 Pods 工程中了, Pods 的主目标是一个 target, 这个 target 依赖了我们所有导入的第三方库, 然后主项目对 Pods 工程中的这个 target 的生成 product 进行依赖, 形成了我们好像直接可以使用第三方库源码的错觉