最近由于要接入第三方库,因此想要了解库的相关知识。网上查阅了许多资料,仍然比较疑惑。比如,静态库能否以及如何引入动态库?动态库能否以及如何引入静态库?自建动态库(非系统动态库)的好处体现在哪,怎么实现?等等。在一番探索之后,总结了一篇较为详尽全面的库的使用方法。
先来看一张思维导图:
概念
什么是库
库是共享程序代码的方式。库从本质上来说是一种可执行代码的二进制格式,可以被载入内存中执行。在开发过程中,一些核心技术或者常用框架,出于安全性和稳定性的考虑,不想被外界知道,所以会把核心代码打包成库,只暴露出头文件以供使用。库分静态库和动态库两种。
库的分类
静态库
存在 .a
和 .framework
两种形式。 .a
是一个纯二进制文件,.framework
中除了有二进制文件之外还有资源文件。 .a
,要有 .h
文件以及资源文件配合, .framework
文件可以直接使用。总的来说,.a + .h + sourceFile = .framework
。所以创建静态库最好还是用.framework
的形式。
对于静态库而言,类似于一个编译好的 .o
的集合。在build的过程中,只会参与链接的操作,链接器会将静态库中被使用的部分合并到可执行文件中去,用函数的实际地址来代替函数引用。链接流程如下图:
动态库
存在.framework
和.tbd
两种形式。
在 iOS8 之前,苹果不允许第三方框架使用动态方式加载,从 iOS8 开始允许开发者有条件地创建和使用动态框架,这种框架叫做 Cocoa Touch Framework。虽然同样是动态框架,但是和系统 framework 不同,app 中的使用的 Cocoa Touch Framework 在打包和提交 app 时会被放到 app bundle 中,运行在沙盒里,而不是系统中。也就是说,不同的 app 就算使用了同样的 framework,但还是会有多份的框架被分别签名,打包和加载。不过 iOS8 上开放了 App Extension 功能,可以为一个应用创建插件,这样主app和插件之间共享动态库还是可行的。
动态链接是使用了 Procedure Linkage Table (PLT)。首先这个 PLT 列出了程序中每一个函数的调用,当程序开始运行,如果动态库被加载到内存中,PLT 会去寻找动态的地址并记录下来,如果函数被调用过的话,下一次调用就可以通过 PLT 直接跳转了。
优劣
静态库,在链接时会被完整地复制到可执行文件中,被多次使用就有多份冗余拷贝。
好处很明显,编译完成之后,库文件实际上就没有作用了。目标程序没有外部依赖,直接就可以运行。当然其缺点也很明显,就是会使用目标程序的体积增大。
动态库,与静态库相反,动态库在编译时并不会被拷贝到目标程序中,目标程序中只会存储指向动态库的引用。等到程序运行时,动态库才会被真正加载进来。
系统的动态库不需要拷贝到目标程序中,自建的动态库可以由工程内的多个库共享,因此可以减小目标程序的体积。但是,由于其把静态链接做的事情都搬到运行时来做,程序的启动会变慢。
库的创建
.a静态库的创建
创建一个 .a
静态库项目,如下图所示:
静态库的文件列表如下,在 products 文件夹内的就是要生成的静态库。此刻是红色的,等到生成成功就会变成黑色。
现在新建自己的类PrintString.h
,声明和实现一个第三方库的方法。
现在可以打包这个静态库了。由于模拟器和真机架构不同,需要选择该包将运行在哪个环境下,如下图所示,选择运行在真机上:
打包生成了静态库在 products 文件夹内:
打开 products 文件夹, 但是此时暴露出来的头文件并没有PrintString.h
。需要对暴露的头文件进行设置。
如下图,在 Build Phase ,的 Copy Files 目录下加入想要公开的头文件:
现在再 run 一次,就得到了正确的静态库。
实践证明,即使不在 CopyFiles 中设置,只要把想暴露的头文件提供给使用者,照样能够使用。这里只是把你想要暴露的头文件单独汇集在一个目录中
现在,可以测试一下这个静态库。可以再创建一个工程,把库连同要暴露的头文件一起拖进去。这里我新建了一个 target :
先要在工程和库间建立关联。如下图所示,在 Link Binary With Libraries 中添加库:
在 ViewController.m
中调用库的方法:
现在可以运行了,不过运行前要选择对 target :
可以在控制台看到库中的方法被调用了:
.framework的创建
动态framework
创建一个framework:
创建后的文件列表如下,可以看到只有一个 framework.h
头文件。通过注释,我们可以理解,这个头文件是所有 public 头文件的集合:
实践证明,即使你不按照他的要求这样 #import <Frameworks/PublicHeader.h>
而是 #import "PublicHeader.h"
也是没问题的
将前面创建的 PrintString.h
和 PrintString.m
导入,并且 import 到 framework.h
中去:
设置需要暴露的头文件,头文件默认在 project header 中,将需要暴露出来的拖到 public header 中去。
实践证明,不想暴露的头文件不要多次一举的放到 Private 中去。如果在 Private 中添加了文件,生成的 Framework 中是会有一个 PrivateHeaders 文件夹的,并且这些 PrivateHeaders 是能被应用方使用的。所以 Private 的直接不理,让它们留在 Project 中就行了。
我们可以看到此处有三种头文件,分别是 project header , public header , private header 。区别如下,不想让别人知道的放在 Project 下就行了:
Public: The interface is finalized and meant to be used by your product’s clients. A public header is included in the product as readable source code without restriction.
Private: The interface isn’t intended for your clients or it’s in early stages of development. A private header is included in the product, but it’s marked “private”. Thus the symbols are visible to all clients, but clients should understand that they’re not supposed to use them.
Project: The interface is for use only by implementation files in the current project. A project header is not included in the target, except in object code. The symbols are not visible to clients at all, only to you.
切记切记 要暴露的头文件一定要放在 Public 下,否则到时候引入的时候会出现如下错误(反正我是老是忘记这个):
这个错误是因为,我在 DynamicWithStatic.h
中 import 的头文件并没有被被设置为 Public 导致的。
生成的 framework 文件目录如下:
将生成的 framework 放入工程中测试。和 .a直接 #import "文件名.h"
不同,framework 引入头文件的时候要按照 #import <framework名/头文件名.h>
的方式引入,静态的 Framework 也要如此。
现在编译可以通过,运行时出现如下错误:
需要将动态库嵌入工程的 bundle 中。因此,需要在 General 中的 Embedded Binary 一项中加入相应动态库:
现在就可以正确运行了
静态framework
静态framework和动态framework创建的基本流程一致,唯一的区别需要设置 Mach-O Type 为 Static Library
:
Bundle 的创建与使用
有时候,我们需要自己创建一个 Bundle,包含一些图片或者 xib 文件。那么如何创建以及使用 Bundle 呢?
新建 Target,在 iOS 选项中找不到 Bundle,那么我们就在 macOS 下选取创建 Bundle:
这个创建的是 macOS 使用的 Bundle,需要修改其 Base SDK
为 iOS 使用:
我们在这个 Bundle 里添加一张图片,现在运行生成 Bundle。将生成好的 Bundle放到测试工程中,可以看到图片添加了进去。
使用的时候由于不是直接在 mainBundle 下,所以不能直接使用 [UIImage imageNamed:@"author"]
。需要找到 Bundle 所在的路径:
1 | [UIImage imageNamed:[[[NSBundle mainBundle] pathForResource:@"NewBundle" ofType:@"bundle"] stringByAppendingString:@"/author.png"]] |
通过路径拼接就能找到正确的图片路径。(对于有些不在 mainBundle 中的 bundle,比如动态库的 bundle,将在用 Cocoapods 创建一个库中介绍)
静态库与动态库的引用
一个库的开发经常会需要用到其他的库(如 AFNetWorking
)的配合,因此,需要在库中嵌入其他的库。如何在自己的静态/动态库中集成第三方的静态/动态库,以及如何配合使用 cocoapods 是我比较困惑的点。下面我就对其进行探。
网上没有找到相关教程,以下是我不断尝试后得出的结论,如果有错误还请指正。
准备
本次我将使用 SVProgressHUD
作为测试的第三方库。我下载了其 2.1.2 的代码,并手动分别打成了静态库 StaticSVProgressHUD
以及动态库 DynamicSVProgressHUD
这里强调一点:静态库无论是 .a 还是 .framework,都是无法把图片等资源文件打进去的(包括在工程中引入其他的 xcodeproj 最后也是生成一个 .a 的静态库)。如果有与静态库相匹配的资源文件,需要打一个 bundle 和静态库一起添加到工程中,然后就可以用代码到 mainBundle 中获取。动态库没有这个问题。
动态库引用静态库
创建
首先创建一个动态库 DynamicWithStatic
,然后链接刚才生成的 StaticSVProgressHUD
:
向其中添加自己的显示 HUD 的类 DWSShowPic
,并且将资源文件 SVProgressHUD.bundle
加入库中。之后设置好头文件就可以直接运行了。因为动态库引用静态库就相当于将代码直接添加在了动态库里,所以和普通动态库没有什么差别,具体可以看我的 Demo。记得一定要在测试工程中 Embedded Binaries
这个动态库。
引入 Cocoapods
现在模拟上面创建的库是工作中要引入的,然后你本身使用 Cocoapods 管理第三方库的,并且也用到了 SVProgressHUD
。那么这会产生什么冲突吗?我们实验一下。
我们将测试工程中引入 Cocoapods:
好的,运行项目,看看会发生什么:
运行时没有问题的,但是出现了一些警告。这是因为你在动态库中引入了静态库 SVProgressHUD
,并且又用 Cocoapods 引入了一遍静态库 SVProgressHUD
。这就产生了冲突,执行的时候会选择其中一个执行。我们需要对动态库中引入的静态库的文件进行改名,比如加上前缀。
如果在 Cocoapods 中引入的是动态库 SVProgressHUD
呢?结果也是一样的。会出现上面的警告,但是不影响运行。
动态库引用动态库
创建
创建一个动态库 DynamicWithDynamic
,然后链接之前的动态库 DynamicSVProgressHUD
:
和之前一样的,添加自己的显示类 DWDShowPic
。由于 DynamicSVProgressHUD
中自带了资源 bundle,所以这里就不需要再添加了。没有什么特别的直接运行生成动态库。现在放到测试工程里运行。同样的,要 Embedded Binaries
中添加这个动态库。
不出意外的,程序崩了:
这个问题上面看到过。是因为没有链接上动态库。不过这里不是没有链接上 DynamicWithDynamic
而是没有链接上 DynamicSVProgressHUD
。动态库不会被打包进动态库中,它只会创建一个引用,需要在项目中手动添加被引用的动态库。现在我们在测试工程的 Embedded Binaries
中添加所需的动态库:
可以顺利运行。
引入 Cocoapods
现在模拟的是你的工程里使用 Cocoapods 使用了某个动态库,比如 DynamicSVProgressHUD
,然后 SDK 提供者给你提供了一个使用 DynamicSVProgressHUD
的动态库 DynamicWithDynamic
。那么引入会有什么问题吗?
由于我之前用的都是对 SVProgressHUD
的封装 DynamicSVProgressHUD
,现在看来是多此一举了,因为我还得自己创建一个能被 Cocoapods 引入的 DynamicSVprogressHUD
。如果直接用 SVProgressHUD
就很开心了。现在修改 Podfile:
通过 use_frameworks
使 Cocoapods 创建动态库,然后自己创建了一个本地的 DynamicSVProgressHUD
。
现在我们把刚才手动加入的 DynamicSVProgressHUD
删去,然后 pod install
,运行。结果正常,运行没有问题。
如果我们 pod 加载的是个静态的 DynamicSVProgressHUD
,但是需要的是个动态的 DynamicSVProgressHUD
会这样?会产生和上面没有在 Embedded Binaries
中添加动态库相同原因的崩溃。
现在还有一个问题,如果我不将之前加入的 DynamicSVProgressHUD
删去呢?即工程中保留了两个同名的动态库 DynamicSVProgressHUD
。会发生什么样的情况?
运行,没有任何问题,也不会产生任何 warning。为了区分到底是哪个被执行了,我在 pod 添加的库的代码中添加了 log。运行时打印出了 log,证明运行的是 pod 的库。当然这不绝对,也有可能不执行 pod 的库的方法。问题的核心在于你在 Linked Frameworks and Libraries
时哪个库是先被添加的。上面的之所以总是执行 pod 的库,就是因为在 Linked Frameworks and Libraries
中,先添加的是 pod 的库。pod 的库执行时被加载后,手动添加的同名库就不会被加载了。
动态库引入动态库,不会将要引入的动态库打包到自身中,即只是 link 产生关联,而不是和静态库一样完全写入。 需要在外部使用该动态库时,手动 embed 动态库内要使用的动态库。这样的做法很麻烦,那么有什么意义呢?正如动态库本身的作用,如果工程本身也要用到该 SVProgressHUD
动态库时,那么仅需导入一份,就不会产生重复代码了。
静态库引用静态库
创建
现在来到静态库引用静态库的实验环节。我们还是先创建一个 StaticWithStatic
的静态库,在其中引入 StaticSVProgressHUD
。然后创建一个类 SWSShowPic
用来调用静态库。由于上面说到静态库的资源文件无法打包在静态库中,因此,资源文件将直接放到测试工程中去:
把生成的静态库放到测试工程中去执行。编译时产生错误:
这个错误说明静态库 StaticWithStatic
并没有把 StaticSVProgressHUD
嵌入其中,即静态库无法嵌入静态库中。动态库确实是把静态库打在了自己的库中,但是静态库无法做到这一点。我想这应该和资源文件无法打进静态库类似。
所以,为了解决这个问题。我们需要在测试工程中添加所需的静态库 StaticSVProgressHUD
:
现在再编译运行,一切正常。
引入 Cocoapods
同样用 Cocoapods 模拟一下通过 Cocoapods 是否可以为静态库提供静态库的链接。按照上面的方式,创建一个供静态库引入的 StaticSVProgressHUD
。注意要修改 Podfile,把 use_frameworks!
注释掉:
将原来手动引入的 StaticSVProgressHUD
删除,然后直接运行,没有任何问题。
那么再来思考一个问题,如果我不把手动引入的库删除,即有两个相同的静态库会怎样?
可以看到,编译的时候直接报 duplicate symbol
的错。其实就是两个静态库中,不能有相同的文件名,否则编译不通过。不过之前对于动态库包含静态库中的文件和工程中文件重名的情况就只报了个 warning。(特指不能有同名文件,和库同名没有关系 -.-)
静态库引用动态库
创建
最后我们试验一下静态库如何引用动态库。还是如法炮制。创建一个静态库 StaticWithDynamic
,然后在 Link FrameWorks and Libraries
中添加动态库 DynamicSVProgressHUD
。现在把生成的静态库拖到测试工程中,运行,编译时出错。还是那个由于没有链接动态库产生的 image not found
错误,我就不贴图片了。
解决方式也是上面说的,要把动态库 DynamicSVProgressHUD
单独添加到 EMbedded Binaries
中去。这样运行一切正常。
引入 Cocoapods
通过之前的学习,到这里,不用试也应该知道会发生什么了。运行正常,如果项目里有多个同名动态库,先添加的会被执行。
总结
到这里静态库动态库相互引用,以及配合 Cocoapods 的所有情况我都已经列举完毕了。累。应该所有接入 SDK 时候会遇到的情况都已经枚举过一遍了。
一些需要知道的点
debug与release
库分为 debug 和 release 两种版本。一般来说, 我们应该发布的是 release 版本。
- debug :调试版本, 系统本身也会有一些调试代码. 此版本体积会稍大, 运行会稍慢。
- release : 发布版本, 系统会去除调试代码, 体积变小, 运行速度变快. 对用户来说没有明显的感觉。
debug 与 release 的设置方式如下图:
对于别人给的库,貌似并不能区分是 debug 还是 release 版本的。
多架构编译
库不仅按 debug 和 release 分类,还会因为运行系统的不同而编译出不同框架的版本。上面的例子都是在真机下的编译,为 arm64 版本,在其他的框架下不能正确运行。
框架分类:
- 模拟器架构:
- i386 : 32位架构 4S ~ 5
- x86_64 : 64位架构 5S ~ 现在的机型
- 真机架构:
- arm7: 在最老的支持iOS7的设备上使用
- arm7s: 在iPhone5和5C上使用
- arm64: 运行于iPhone5S的64位 ARM 处理器 上
修改框架的方式如下图:
debug 项默认为 YES ,表示仅生成当前选择的框架的库; release 项默认为 NO ,表示生成支持所有模拟器或真机的库。生成的库将会保存在 products 目录下的不同分类目录内:
lipo
lipo 是个很有用的命令,主要用来查看库支持的架构以及合并拆分库。
-info
查看刚才编译的 Framework 库在 debug 和 release 下支持的框架:
可以看到正如上面所说 debug 下不是 fat file ,只支持 arm64 , release 下是 fat file , 支持 arm7 和 arm64。
-create
上面生成的库,要么是只支持模拟器的,要么是只支持真机的,那么如何才能又能兼顾真机和模拟器呢? -create 使用方式:
1 | lipo -create 库1 库2 -output 新库 |
使用结果如下图:
-thin
如果有一个 fat file 但是你不需要支持那么多框架,也可以通过拆分,为库瘦身, -thin 使用方式:
1 | lipo 旧库 -thin 需拆分框架 -output 新库 |
使用结果如下图:
参考链接
WWDC2014之iOS使用动态库
手把手教你使用CocoaPods打包静态库
iOS 静态库开发
使用CocoaPods开发并打包静态库
使用Cocoapods创建私有podspec
iOS静态库 【.a 和framework】【超详细】
创建一个 iOS Framework 项目
iOS开发——创建你自己的Framework
iOS中workspace与静态库
Cocoapods 应用第一部分 - Xcode 创建 .framework 相关
ios打包–打包静态库(五)
呼~总算把库的相关知识看完了,写成这第二篇。花了半个月才写完。期间各种问题,各种错误不知道怎么解决,真的累。以后还是研究些经常能用得到的东西吧。自己基本不需要打包库~