Cocoapod中静态库与动态库

最近在做一个需求时候,对 Cocoapod 有了一次新的理解,故做下笔记记录一下知识点..

CocoaPods 是开发 OS X 和 iOS 应用程序的一个第三方库的依赖管理工具。利用 CocoaPods,可以定义自己的依赖关系 (称作 pods),并且随着时间的变化,以及在整个开发环境中对第三方库的版本管理非常方便。

CocoaPods 背后的理念主要体现在两个方面。首先,在工程中引入第三方代码会涉及到许多内容。在配置 build phases 和 linker flags 过程中,会引起许多人为因素的错误。CocoaPods 简化了这一切,它能够自动配置编译选项。

以上是 Cocoapod 的一些介绍,所以可以理解成使用第三方库的工具

iOS中的cocoapods, 像 node中的 npm+grunt. Androidgradle

以下代码是普通的项目中 cocopod 配置写法:

1
2
3
4
5
6
7
8
9
10
#公共Pod仓库的索引,通过这个仓库里面的索引找到所有第三方库的github地址
source 'https://github.com/CocoaPods/Specs.git'

platform :ios, '8.0'

target '项目Target名字' do
pod 'MJExtension'
# blablabla 一些第三方库依赖

end

通过在项目根目录下生成 Podfile 并运行 pod install 能为项目自动引入第三方依赖,方便快捷.

再大的项目最初都是从 Xcode 提供的一个非常简单的工程模板慢慢演化来的。在项目的演化过程中,为了实现新的功能,不断有新的类被创建,新的代码被添加。不过除了自己添加代码,我们也经常会直接把第三方的开源代码导入到项目中,从而避免重复造轮子,节约开发时间。

所以使用 Cocoapods 的好处有如下几个:

  1. 避免直接导入文件的原始方式,方便后续代码升级
  2. 简化、自动化集成流程,避免不必要的配置
  3. 自动处理库的依赖关系
  4. 简化开发者发布代码流程

以上就是简单的对 Cocoapod 使用入门,下面就开始动态库与静态库研究

前言

在 iOS 8 之前,iOS 平台不支持使用动态 Framework,开发者可以使用的 Framework 只有苹果自家的 UIKit.Framework,Foundation.Framework 等。这种限制可能是出于安全的考虑。换一个角度讲,因为 iOS 应用都是运行在沙盒当中,不同的程序之间不能共享代码,同时动态下载代码又是被苹果明令禁止的,没办法发挥出动态库的优势,实际上动态库也就没有存在的必要了。

由于上面的限制,开发者想要在 iOS 平台共享代码,唯一的选择就是打包成静态库 .a 文件,同时附上头文件(例如微信的SDK)。但是这样的打包方式不够方便,使用时也比较麻烦,大家还是希望共享代码都能能像 Framework 一样,直接扔到工程里就可以用

iOS 8/Xcode 6 推出之后,iOS 平台添加了动态库的支持,同时 Xcode 6 也原生自带了 Framework 支持(动态和静态都可以).为什么 iOS 8 要添加动态库的支持?唯一的理由大概就是 Extension 的出现。Extension 和 App 是两个分开的可执行文件,同时需要共享代码,这种情况下动态库的支持就是必不可少的了。但是这种动态 Framework 和系统的 UIKit.Framework 还是有很大区别。系统的 Framework 不需要拷贝到目标程序中,我们自己做出来的 Framework 哪怕是动态的,最后也还是要拷贝到 App 中(App 和 Extension 的 Bundle 是共享的),因此苹果又把这种 Framework 称为 Embedded Framework(可植入性Framework)。

划一下重点 Embedded Framework

iOS 出现了APP Extensionswift编程语言也诞生了,由于 iOS 主 APP 需要和 Extension 共享代码,Swift 语言的机制也只能有动态库,于是提出了一个概念Embedded Framework,这种动态库允许APPAPP Extension共享代码,但是这份动态库的生命被限定在一个 APP 进程内。简单点可以理解为 被阉割的动态库

动态库静态库

静态库和动态库都是以二进制提供代码复用的代码库

  • 动态库形式:.dylib和 .framework
  • 静态库形式:.a 和 .framework

静态库和动态库的区别

静态库:链接时会被完整的复制到可执行文件中,所以如果两个程序都用了某个静态库,那么每个二进制可执行文件里面其实都含有这份静态库的代码

动态库: 链接时不复制,在程序启动后用 dyld 加载,然后再决议符号,所以理论上动态库只用存在一份,好多个程序都可以动态链接到这个动态库上面,达到了节省内存(不是磁盘是内存中只有一份动态库),还有另外一个好处,由于动态库并不绑定到可执行程序上,所以我们想升级这个动态库就很容易,windows 和 linux 上面一般插件和模块机制都是这样实现的。

上图中的绿框表示app的可执行文件。

在 iOS 平台上规定不允许存在动态库,并且所有的 IPA 都需要经过苹果爸爸的私钥加密后才能用,基本你用了动态库也会因为签名不对无法加载,所以 iOS8 之后就出现了上面的 阉割版动态库 Embedded Framework

.a是一个纯二进制文件,不能包含其他的资源文件.framework中除了有二进制文件之外还有资源文件。

.a文件不能直接使用,至少要有.h文件配合,.framework文件可以直接使用。

.a + .h + sourceFile = .framework

静态库和动态库的加载

编译: 将我们的源代码文件编译为目标文件

链接: 将我们的各种目标文件加上一些第三方库,和系统库链接为可执行文件。

由于某个目标文件的符号(可以理解为变量,函数等)可能来自其他目标文件,其实链接这一步最主要的操作就是 决议符号的地址。

  • 若符号来⾃静态库(本质就是.o 的集合包)或 .o,将其纳⼊链接产物,并确定符号地址
  • 若符号来⾃动态库,打个标记,等启动的时候再说—交给 dyld 去加载和链接符号

于是链接加装载就有了不同的情况

  1. Load 装载:将库⽂件载⼊内存
    • Static Loading:启动时
    • Dynamic Loading:启动后(使⽤时)
  2. Link 链接:决议符号地址
    • Static Linking:构建(链接)时
    • Dynamic Linking:运⾏时(启动时或使⽤时)

组合起来就是 2 * 2 = 4 了

  1. Static Loading + Static Linking
  2. Static Loading + Dynamic Linking
  3. Dynamic Loading + Dynamic Linking
  4. Dynamic Loading + Static Linking

第一种是纯静态库相关了

第二种就是静态加载(启动时),动态链接 ,链接时,动态库参与链接,但是这时候只是给符号打了标记告诉我这个符号来自与动态库,程序启动时,iOS 或者 Mac OS 操作系统的 dyld 自动 load + link。既然全部都是自动的。那么符号的调用方完全不知道你到底是源码还是静态库,动态库 。

第三种收到调用 dlopen + performSelector,由于不能上架,通常用于线下开发( 著名的 Injection)

动静态库对比

Apple 官方推荐每个 App 使用 Dynamic Framework 的数量是 6 个,这是有原因的。因为在 App 启动的时候,dyld 会load 这些动态库,这会造成启动时间的增加;而且动态库数量过多,还会在 iOS 9 的设备上造成 dyld 的 crash

但是,并不是全部达成静态库就很好,因为打成静态库,会导致包增大,所以需要根据项目来选择静态库还是动态库。目前来说,如果引入了 swift 库的话,基本 cocoapod 都打成了动态库, 不然无法编译,但是好像也有办法解决,比较麻烦,我的理解来说,当然一些简单功能实现,没必要使用第三方库.(维护是个问题,另外,使用第三方库,很多糅杂的类与方法不需要使用,导致以后启动速度变慢),最好就是简单的功能,参考源码自己写一套。第一可以提高自己对源码,框架设计的理解,第二自己写的东西知道流程方便维护。当然一些经历了长时间迭代的框架库直接使用就行了(如 AFNetworking , SD等,没必要重复造轮子).

当然还有一些特殊情况下是需要使用动态库的,如:

iOS9 以下对 __text 段 60M 的限制使用了动态库方案

自动化动静态库依赖历史

早期的项目

iOS开发早期未形成工程化的时候,是不存在所谓依赖管理的概念的,早期 iOS 开发如果想要使用第三方库,是非常繁琐的一件事情。

  1. 将第三方库源代码复制到工程目录,或者使用 git submodule 将其作为项目的一个模块
  2. 设置工程文件,添加第三方库依赖的系统库
  3. 对于某些第三方库,可能需要设置一些编译选项( etc. -fno-objc-arc
  4. 最后就是管理第三方库代码的更新

iOS 开发终究还是很传统的 Unix 开发,直接把第三方库源代码抽离出来,打包成静态库就行了(PS:在 iOS 8 之前苹果只允许使用静态库,而 iOS 8 后就可以打包动态库了,当然,实际上是一样的。)。这样的话就不需要担心过多的源代码导入的繁琐,也不需要担心第三方库究竟需不需要编译选项,而且第三方库更新只需要更新静态库就行了

进化中的项目配置

封装成静态库或动态库看起来确实美好了,方便快捷,这也是 Unix 下的解决方案。但是只要仔细一想,问题一大堆。

  1. 某些第三方库很可能直接依赖另一项第三方库,依赖如果使用手工解决将会是很大的工作。
  2. 动态库不能用于 iOS 7 及以下版本
  3. 工程依旧需要设置依赖的系统库
  4. 仍然需要手工更新第三方库版本

对于 Unix 环境来说,动态库可以存放在系统默认搜索路径下,这样所有的应用都可以共享同一个内存副本,而且升级动态库的时候可以一起升级,不需要到各处寻找。但是 iOS 的动态库实际上是缩水的,因为苹果将动态库限制在了沙盒内部,其他 App 完全不能访问此动态库。这就限制了动态库的优点。还有一点原因就是指令集架构的不同,iOS 模拟器使用的是 x86 架构指令集,而真机则是 ARM64 等指令集,如果想要方便使用,最好需要打包通用架构的静态库,但是这是很繁琐的工作。
由于以上原因,手工封装静态库或者动态库实际上在项目小的时候是可以的,但是项目规模一旦扩大就会导致效率低下。

自动化依赖时代

为了能够自动化流水作业,就引出了依赖包管理的概念,也就是 Cocoapods。Cocoapods 本质上还是上面所说的封装动态库静态库,但是它解决的最大问题就是依赖管理。开发者不需要从 Github 的地方辛苦的寻找代码,只需要一条命令,就能下载整合第三方库。

至于为什么可以自动化依赖,这里不做更多叙述可以阅读文章 : Cocoapods原理总结

上面说了一下历史进程,下面说下一些坑

静态库的坑

在使用 静态库 打包组件或者第三方库时候,有时候会遇到相同名字的函数,会在调用相同方法时候,有时候会出现崩溃,因为,在 link 静态库时候,linker 会把它需要的东西复制到可执行文件中,在 Static Library 中的函数符号出现在了最后的 binary 中(T 代表全局代码段符号),但由于这两个函数的名字都是 test,最终只有一个函数实现被合并进去了(有点像 Category方法覆盖)。具体可以阅读下方 [1] 的链接

Cocoapod使用动静态库

podfile 中使用 添加 use_frameworks! 即可将所有第三方库打包成动态库,不添加的话就是打包成静态库(注:暂时来说如果使用了第三方swift库,一定需要打成静态库!),如果你的第三方库里面包含了静态库,(如什么百度地图,微信等那些,比较难解决了)

暂时了解到(2019.04.28), 有几种操作:

  1. cocoapod1.5.0 版本之后,不使用 use_frameworks! ,使用 use_modular_headers!打成静态库. 然后其他 oc 库后面需要添加 :modular_headers => false , 如 : pod 'YYKit',:modular_headers => false
  2. 将静态库重新包一层动态库来使用, 先挖个坑,迟点看有没有时间做个 Demo,原理如下

静态库和动态库依赖关系

第一种静态库互相依赖,这种情况非常常见,制作静态库的时候只需要有被依赖的静态库头文件在就能编译出来。但是这就意味者你要收到告诉使用者你的依赖关系。

第二种动态库依赖动态库,两个动态库是相互隔离的具有隔离性。在制作的静态库的时候需要被依赖动态库参与链接,最终具体的符号决议交给dyld来做。

第三种,静态库依赖动态库,也很常见,静态库制作的时候也需要动态库参与链接,但是符号的决议交给dyld来做。

第四种,动态库依赖静态库,这种情况就有点特殊。首先我们设想动态库编译的时候需要静态库参与编译,但是静态库交由dyld来做符号决议,这和我们前面说的就矛盾了啊。静态库本质是一堆.o 的打包体,首先并不是二进制可执行文件,再者你无法保证主程序把静态库参与链接共同生成二进制可执行文件。

利用动态库解决相关问题

处理多个动态库依赖一个静态库问题

通过前面我们知道可执文件(主程序或者动态库)在构建的链接阶段,遇到静态库,吸附进来;遇到动态库,打标记,彼此保持独立。

正因为动态库是保持独立的,那么我们可以自定义一个动态库把依赖的静态库吸附进来。对外整体呈现的是动态库特性。其他的组件依赖我们自定义的动态库,由于隔离性的存在,不会出现问题。

总结

通过上面的研究与资料收集,动静态区别可以归纳成下面表格

动态库和静态库的区别如下

动态库 静态库
命名空间 有单独的命名空间,不同库同名文件不会冲突 使用import<XXX/xxx.h>的方式引入 没有单独命名空间,同名文件冲突 引入方式import”xxx.h”
加载时机 在启动时加载,加载时间较长 构建时加载
依赖关系 可以依赖动态库,不能依赖静态库 可以依赖动态库和静态库
是否能使用swift 可以包含swift文件 在cocoapods1.4.0之后,可以使用use_framework!的方式包含swift文件 framework支持static_framework

根据不同项目合理选用技术是必要的,在我看来,由于项目中必定存在第三方,其实少量第三方使用动态库,对启动速度影响并不是太大的,可以放心使用动态库。由于swift不支持静态库,未来也会出现更多第三方swift库,与其将swift库静态化,还不如将静态库动态化(静态库上套多一层 .framework)来实现组件化

另外可以通过 file这个命令判断库是静态库还是动态库

是 .framework 里面的可执行文件才能执行这个

Reference

1.两个 Framework 中如果定义了相同名字的 C 函数会发生什么?

2.理解Cocoapods

3.iOS动态库、静态库及使用场景、方式

4.iOS 动态库和静态库简介

5.升级 CocoaPods 1.5,使用 Swift Static Framework

*6.组件化-动态化实践

7.cocoapods的静态库和动态库

Cocoapod中静态库与动态库

https://swlfigo.github.io/posts/6823/

Author

Sylar

Posted on

2019-04-28

Updated on

2021-11-14

Licensed under

Comments