×

IOS-静态库(.a、framewrok)、动态库(.tbd、framework)、Bundle制作(适用于静态库的)研究

96
树下敲代码的超人
2017.09.27 17:11* 字数 4938

技 术 文 章 / 超 人


关于库思维导图:


概念

什么是库
库是共享程序代码的方式。库从本质上来说是一种可执行代码的二进制格式,可以被载入内存中执行。在开发过程中,一些核心技术或者常用框架,出于安全性和稳定性的考虑,不想被外界知道,所以会把核心代码打包成库,只暴露出头文件以供使用。库分静态库和动态库两种。
库的分类

  • 静态库
    对于静态库而言,类似于一个编译好的 .o的集合。在build的过程中,只会参与链接的操作,链接器会将静态库中被使用的部分合并到可执行文件中去,用函数的实际地址来代替函数引用。

存在 .a.framework两种形式。 .a要有.h文件以及资源文件配合
1. .a是一个纯二进制文件
2. .framework中除了有二进制文件之外还有资源文件。 .framework
文件可以直接使用。
总的来说.a + .h + sourceFile= .framework。所以创建静态库最好还是用.framework的形式。
链接流程如下图:

  • 动态库
    存在.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 直接跳转了。

  • 优劣
    1. 静态库,在链接时会被完整地复制到可执行文件中,被多次使用就有多份冗余拷贝,静态库还有一个较重要的缺点,多个静态库之间文件名和静态变量名字不能重复,否则编译会报错或者找错对象。好处很明显,编译完成之后,库文件实际上就没有作用了。目标程序没有外部依赖,直接就可以运行。当然其缺点也很明显,就是会使用目标程序的体积增大
    2. 动态库,与静态库相反,动态库在编译时并不会被拷贝到目标程序中,目标程序中只会存储指向动态库的引用。等到程序运行时,动态库才会被真正加载进来。系统的动态库不需要拷贝到目标程序中,自建的动态库可以由工程内的多个库共享,因此可以减小目标程序的体积。但是,由于其把静态链接做的事情都搬到运行时来做,程序的启动会变慢。

库的创建


.a静态库的创建

创建一个 .a
静态库项目,如下图所示:

image.png

静态库的文件列表如下,在 products 文件夹内的就是要生成的静态库。此刻是红色的,等到静态库编译成功就会变成黑色。


现在新建自己的类PrintString.h
,声明和实现一个第三方库的方法。


现在可以打包这个静态库了。由于模拟器和真机架构不同,需要选择该包将运行在哪个环境下,如下图所示,选择运行在真机上:


打包生成了静态库在 products 文件夹内:


打开 products 文件夹, 但是此时暴露出来的头文件并没有PrintString.h
。需要对暴露的头文件进行设置。


如下图,在 Build Phase ,的 Copy Files 目录下加入想要公开的头文件:


现在再 run 一次,就得到了正确的静态库。


现在,可以测试一下这个静态库。可以再创建一个工程,把库拖进去。不过更推荐如下图所示,新建一个 target :


先要在工程和库间建立关联。如下图所示,在 Link Binary With Libraries中添加库:

在 ViewController.m
中调用库的方法:


现在可以运行了,不过运行前要选择对 target :


可以在控制台看到库中的方法被调用了:



.framework的创建

动态framework
创建一个framework:

image.png

创建后的文件列表如下,可以看到只有一个 framework.h
头文件。通过注释,我们可以理解,这个头文件是所有 public 头文件的集合:


将前面创建的 PrintString.h 和 PrintString.m导入,并且 import 到 framework.h 中去:


设置需要暴露的头文件,头文件默认在 project header 中,将需要暴露出来的拖到 public header 中去。


我们可以看到此处有三种头文件,分别是 project header , public header , private header 。区别如下:

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.

生成的 framework 文件目录如下:


将生成的 framework 放入工程中测试,编译通过,运行时出现如下错误:


需要将动态库嵌入工程的 bundle 中。因此,需要在 General 中的 Embedded Binary 一项中加入相应动态库:



现在就可以正确运行了


动态framework

静态framework和动态framework创建的基本流程一致,唯一的区别需要设置 Mach-O Type 为 Static Library


静态库与动态库的引用
一个库的开发经常会需要用到其他的库(如 AFNetWorking
)的配合,因此,需要在库中嵌入其他的库。如何在自己的静态/动态库中集成第三方的静态/动态库是我比较困惑的点。
网上没有找到相关教程,以下是我不断尝试后得出的结论,如果有错误还请指正。
动态库引用静态库
创建一个动态库 DynamicWithStatic

使用 cocoapods 的方式为动态库引入静态库。在工程目录下新建 podfile
,写入:

target 'DynamicWithStatic' do
pod 'SVProgressHUD'
end

这里引入SVProgressHUD
,因为调试起来比较简单。
在执行完 pod install
后,打开 Frameworks.xcworkspace
,在其中添加SVProgress
类。现在的工程目录如下:

SVProgress.h
中添加代码:

typedef void(^SVProgresshud)();
#import <Foundation/Foundation.h>
@interface SVProgress : NSObject
+ (SVProgresshud)getBlock;
@end

SVProgress.m
中添加代码:

#import "SVProgress.h"
#import "SVProgressHUD.h"
@implementation SVProgress
+(SVProgresshud)getBlock{
    return ^(){
        [SVProgressHUD showSuccessWithStatus:@"成功!"];   
   };
}@end

SVProgress
类的目的是提供一个 block 以供调用。将 SVProgress.h
头文件暴露出来后, run 生成动态库。
动态库内引用静态库相当于直接把代码写入动态库中,非常的简单。现在思考一个问题,如果工程中引用了这个动态库,并且工程本身也用到了 SVProgressHUD
库,那么会发生什么?尝试一下,在 Pod 中为 FrameworkTest
添加 SVProgressHUD
,会产生如下的警告:

重复实现

SVProgressHUD
库中的各个类在工程和库中都进行了实现,但是没有指明使用哪一个实现。不过这个警告,并不影响运行,非强迫症可以选择无视,那么对于强迫症患者,如何消除掉这些警告呢?修改库中 SVProgressHUD
中各个类的命名。有两种方式:手动和自动,将在下面介绍。
动态库引用动态库
这次被引用的动态库还是 SVProgressHUD库。先用 pod 下载 SVProgressHUD的源码,然后打包成动态库,这里就不多做说明了。将动态库 SVProgressHUD拖入工程中,新建 DynamicWithDynamic动态库。将上面的 SVProgress.hSVProgress.m拖入 DynamicWithDynamic中,设置暴露出 SVProgress.h
记得注意一定要在 DynamicWithDynamic
中引入 SVProgressHUD
,如下图所示:

现在 run 一下,就可以成功生成了。下面在 FrameworkTest中测试,使其 embed DynamicWithDynamic。微调下 ViewController.m使其引入目标库,运行。在运行中出现如下错误:

这个错误上面也提到过了,是因为必要的动态库没有 embed 进工程中。但是明明已经将 DynamicWithDynamicembed了啊。好吧,由于 SVProgressHUD现在是动态库了,还需要将 SVProgressHUD库 embed 进 FrameworkTest中。
动态库引入动态库,不会将要引入的动态库打包到自身中,即只是 link 产生关联,而不是 embed 嵌入。 需要在外部使用该动态库时,手动 embed 动态库内要使用的动态库。这样的做法很麻烦,那么有什么意义呢?正如动态库本身的作用,如果工程本身也要用到该 ·SVProgressHUD·
动态库时,那么仅需导入一份,就不会产生重复代码了。


静态库引用静态库

新建一个 target
命名为 StaticWithStatic,使用 cocoapods 管理 SVProgressHUD。基本方法和上面一样,唯一需要改变的地方是 Mach-O 需要改为 Static Library ,运行。
然而运行失败,报错如下:

大概意思是在对 ViewController.h
进行链接的时候没有找到 SVProgressHUD.m
。这就很奇怪了,明明已经在 StaticWithStatic
中通过 cocoapods 管理了,怎么会没有 SVProgressHUD.m
呢?我想可能是因为cocoapods只是起到 Link 作用,并没有把第三方库也打进去,那么为什么动态库引入静态库的时候是可行的呢?这就不太清楚了。虽然不知道原因,但是解决方法是有的。需要在工程里,即 FrameworkTest
cocoapods 中添加 SVProgressHUD

target 'DynamicWithStatic' do
pod 'SVProgressHUD'
end


target 'StaticWithStatic' do
pod 'SVProgressHUD'
end


target 'FrameworksTest' do   
 pod 'SVProgressHUD'
end


target 'DynamicWithDynamic' do
end

再次 build ,错误消除,可以使用。


静态库引用动态库
静态库貌似不能引用动态库。如果尝试一下,会发现,即使将动态库 Link Binary With Libraries 入静态库,也是找不到 SVProgressHUD.h头文件的。一些需要知道的点debugrelease

  • 库分为 debugrelease 两种版本。一般来说, 我们应该发布的是 release 版本。
  • debug:调试版本, 系统本身也会有一些调试代码. 此版本体积会稍大, 运行会稍慢。
  • release : 发布版本, 系统会去除调试代码, 体积变小, 运行速度变快. 对用户来说没有明显的感觉。

debugrelease 的设置方式如下图:

对于别人给的库,貌似并不能区分是 debug 还是 release 版本的。


多架构编译
库不仅按 debugrelease 分类,还会因为运行系统的不同而编译出不同框架的版本。上面的例子都是在真机下的编译,为 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 下支持的框架:
[图片上传中。。。(33)]查看库

可以看到正如上面所说 debug 下不是 fat file ,只支持 arm64release 下是 fat file , 支持 arm7arm64


-create
上面生成的库,要么是只支持模拟器的,要么是只支持真机的,那么如何才能又能兼顾真机和模拟器呢? -create 使用方式:

lipo -create 库1 库2 -output 新库

使用结果如下图:



-thin
如果有一个 fat file 但是你不需要支持那么多框架,也可以通过拆分,为库瘦身, -thin 使用方式:

lipo 旧库 -thin 需拆分框架 -output 新库

使用结果如下图:



bundle

framework 中使用 storyboard/xib 创建的页面,可以直接访问 framework 中图片资源。但是 framework 中通过imageNamed:
方式加载的照片都会丢失。这是因为 imageNamed:
的方法默认是从 mainBundle
中查找资源的,而 framework 中的照片是从 framework 内部加载的,这是的 bundle 并不是 mainBundle
,而是存在于主程序的 docment 文件中的 framework 包,图片加载的路径发生了变化,自然找不到图片资源,所以需要修改加载图片的方法!
一般的方法是创建一个 bundle :

image.png

bundle 一般和库命名相同。需要注意的是, bundle 并不会被打包进库中的,而是添加要单独添加到工程中,和 framework 相独立的两部分。向 bundle 中直接添加图片:


先在主工程的 Copy Bundle Resources 中添加 bundle ,如下图所示:


现在就可以通过[NSBundle mainBundle]
获取图片,一下两种方式皆可:

[UIImage imageNamed:[[[NSBundle mainBundle] pathForResource:@"Frameworks" ofType:@"bundle"] stringByAppendingString:@"/Images/author.png"]];
[UIImage imageNamed:@"Frameworks.bundle/Images/author.png"];

修改bundle Setting

  • 修改Architectures中的Base SDK为latest IOS
9109394.png
  • 修改Build OptionsEnable BitcodeNO
image.png
  • 修改signingCode signing identityDon't Code Sign (在提交苹果审核时,bundle如果有任何签名都会被拒)
image.png
  • 修改User-DefinedCOMBINE_HIDPI_IMAGES为NO(iOS 创建Bundle时放入的图片资源(.png)在默认配置下会被转为.tiff格式,使用的时候找不到。因为在iOS中创建bundle时会用一个“hack”,为了使所有的运行需要更改一个配置。)
image.png

-在静态库中的Build PhasesTarget Dependencies添加Bundle。这样以后编译静态库时,bundle会自动编译。无需单独再编译

Target Dependencies Target依赖,某些Target可能依赖某个Target输出的值,这里设置依赖
Copy Bundle Resources 是指生成的product的.app内将包含哪些资源文件
Compile Sources 是指将有哪些源代码被编译
Link Binary With Libraries 是指编译过程中会引用哪些库文件

image.png

cocoapods打包库

好了,终于到最后一部分了。前面已经介绍了手动创建库的方式,那么如何自动创建一个库?另外一点,前面提到过,在动态库内引入静态库,会和项目本身由 cocoapods 引入的库同名冲突,如何消除这一冲突?以上问题都可以通过 cocoapods 打包库实现

创建工程
只需要输入 pod 的 lib
命令即可完成初始项目的搭建:

pod lib create StaticWithCocoapods

输出指令后,会提示确认五个问题,按需求回答即可:


稍等片刻,就会自动生成一个工程。
配置信息
在项目目录下有一个 xxx.podspec
配置文件,需要进行修改,摘录如下:

Pod::Spec.new do |s|  
s.name             = 'StaticWithCocoapods'

s.version          = '0.1.0'  

s.summary          = 'A short description of StaticWithCocoapods.'

# This description is used to generate tags and improve search results.
#   * Think: What does it do? Why did you write it? What is the focus?#   * Try to keep it short, snappy and to the point.
#   * Write the description between the DESC delimiters below.
#   * Finally, don't worry about the indent, CocoaPods strips it!  

s.description      = <<-DESCTODO: Add long description of the pod here.                       DESC  

s.homepage         = 'https://github.com/zhang759740844'
  
s.license          = { :type => 'MIT', :file => 'LICENSE' }  

s.author           = { 'Zachary' => '759740844@qq.com' }  

s.source           = { :git => '/Users/zachary/Desktop/StaticWithCocoapods', :tag => s.version.to_s }  

s.ios.deployment_target = '8.0'  

s.source_files = 'StaticWithCocoapods/Classes/**/*' 

s.resource_bundles = {    'StaticWithCocoapods' => ['StaticWithCocoapods/Assets/*.png']  }  

s.public_header_files = 'StaticWithCocoapods/Classes/**/*.h' 

s.frameworks = 'UIKit', 'MapKit'  s.dependency 'AFNetworking', '~> 2.3'  

s.denpendency 'SVProgressHUD'end

s.version 表示的是当前类库的版本号
s.source 表示当前类库源
s.sources_files 表示类库的源文件存放目录
s.resource_bundles 表示资源文件存放目录
s.frameworks 表示类库依赖的framework
s.dependency 表示依赖的第三方类库

其中要说明的是:

  • source 可以填写远端 git 仓库,也可以是像我写的那样的本地 git 仓库。
  • 依赖项不仅要包含你自己类库的依赖,还要包括所有第三方类库的依赖,只有这样当你的类库打包成 .a 或 .framework 时才能让其他项目正常使用。
  • source_file 路径中出现的通配符 *表示匹配任意字符, **表示匹配所有当前文件夹和子文件夹。
  • source_bundles 中花括号内的 'StaticWithCocoapods'
    就表示一个 StaticWithCocoapods
    bundle。

添加文件
sources_filespublic_header_files 以及 resource_bundle 中添加图片和类文件。在 demo 的文件夹下执行 pod install
。现在打开 demo 工程,可以看到创建的 StaticWithCocoapods
库的文件结构如下图:

到这里一切正常,也可以使用 SVProgressHUD
,但是当我想用 [UIImage imageNamed:[[[NSBundle mainBundle] pathForResource:@"StaticWithCocoapods" ofType:@"bundle"] stringByAppendingString:@"/author.png"]];
加载图片资源文件时,一直返回 nil

好吧。虽然网上cocoapods打包教程不少,但是真正试过添加图片的人应该不多,最后在 Stackoverflow 的一个评论里总算找到了解决方法[Cocoapods]:Resource Bundle not accessbile

原先 demo 中 profile 的内容如下:

use_frameworks

!target 'StaticWithCocoapods_Example' do
  pod 'StaticW', :path => '../'  

target 'StaticWithCocoapods_Tests' do    
inherit! :search_paths    
pod 'FBSnapshotTestCase'  
end

end

现在要删除 use_frameworks!
以及其相关内容,变成这样:

target 'StaticWithCocoapods_Example' do  
pod 'StaticWithCocoapods', :path => ‘../‘
end

再次尝试加载图片,可以得到正确结果. _


提交代码

  • 使用 sourcetree 添加本地仓库
  • 提交上面的所有改动
  • 为改动添加 tag 为 0.1.0

设置好后如图:


这里要注意,tag 一定要打上,版本控制的时候就是以 tag 来辨别的。


验证类库
开发完成静态类库之后,需要运行pod lib lint验证一下类库是否符合pod的要求。添加 --allow-warnings
忽略警告:


打包库

打包库需要一个 cocoapods 的插件 cocoapods-packager来完成类库的打包。
在终端执行以下命令,安装插件:

sudo gem install cocoapods-packager

在目标文件夹内执行以下命令,完成打包:

pod package StaticWithCocoapods.podspec --force

打包成 .framework,也可以用--library打包成 .a。不过 cocoapods 之前有过打包成 .a但是没有暴露出头文件的bug,懒得试有没有修复了,所以还是都打成 .framework
吧。

现在在目标文件夹下就会多出一个 StaticWithCocoapods-0.2.0
目录,里面是打包好的 framework 。至于如何将打包好的库添加到 cocoapods的官方库内,这个就没有研究了,因为,在很长的一段时间内我都不会用到。有兴趣的可以看看 cocoapods 的官方文档,自行研究下。

日记本
Web note ad 1