分享

一篇较为详细的 iOS动态静态库创建打包方法 总结

 quasiceo 2017-03-19

一篇较为详细的 iOS动态静态库创建打包方法 总结

144
作者 张仙森
2016.10.26 14:54* 字数 4948 阅读 1334评论 11

最近由于要接入第三方库,因此想要了解库的相关知识。网上查阅了许多资料,仍然比较疑惑。比如,静态库能否以及如何引入动态库?动态库能否以及如何引入静态库?自建动态库(非系统动态库)的好处体现在哪,怎么实现?等等。在一番探索之后,总结了一篇较为详尽全面的库的使用方法。

先来看一张思维导图:


概念

什么是库

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

库的分类

静态库

存在 .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 静态库项目,如下图所示:


lib_2


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


lib_3


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


lib_4

lib_5


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


lib_6


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


lib_7


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


lib_8


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


lib_9


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


lib_10


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


lib_11


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


lib_12


ViewController.m 中调用库的方法:


lib_13


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


lib_14


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


lib_15

.framework的创建

动态framework

创建一个framework:


frame_1


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


frame_2


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


frame_3


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


frame_4


我们可以看到此处有三种头文件,分别是 project headerpublic headerprivate 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 文件目录如下:


frame_5

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


frame_6

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


frame_7

现在就可以正确运行了

静态framework

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


frame_8

静态库与动态库的引用

一个库的开发经常会需要用到其他的库(如 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 ,如下图所示:


引入SVProgressHUD

现在 run 一下,就可以成功生成了。

下面在 FrameworkTest 中测试,使其 embed DynamicWithDynamic 。微调一下 ViewController.m 使其引入目标库,运行。

在运行中出现如下错误:


错误原因

这个错误上面也提到过了,是因为必要的动态库没有 embed 进工程中。但是明明已经将 DynamicWithDynamic embed 了啊。好吧,由于 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 头文件的。

一些需要知道的点

debug与release

库分为 debug 和 release 两种版本。一般来说, 我们应该发布的是 release 版本。

  • debug :调试版本, 系统本身也会有一些调试代码. 此版本体积会稍大, 运行会稍慢。
  • 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 使用方式:

lipo -create 库12 -output 新库

使用结果如下图:


合并

-thin

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

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

使用结果如下图:


拆分

bundle

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

一般的方法是创建一个 bundle :


创建bundle

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


添加图片

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


添加bundle

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

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

[UIImage imageNamed:@"Frameworks.bundle/Images/author.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      = <<-DESC
TODO: 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 表示依赖的第三方类库

其中要说明的是:

  1. source 可以填写远端 git 仓库,也可以是像我写的那样的本地 git 仓库。
  2. 依赖项不仅要包含你自己类库的依赖,还要包括所有第三方类库的依赖,只有这样当你的类库打包成 .a 或 .framework 时才能让其他项目正常使用。
  3. source_file 路径中出现的通配符 * 表示匹配任意字符, ** 表示匹配所有当前文件夹和子文件夹。
  4. 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

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

提交代码

  1. 使用 sourcetree 添加本地仓库
  2. 提交上面的所有改动
  3. 为改动添加 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 的官方文档,自行研究下。

参考链接

WWDC2014之iOS使用动态库
手把手教你使用CocoaPods打包静态库
iOS 静态库开发
使用CocoaPods开发并打包静态库
使用Cocoapods创建私有podspec
iOS静态库 【.a 和framework】【超详细】
创建一个 iOS Framework 项目
iOS开发——创建你自己的Framework
iOS中workspace与静态库
Cocoapods 应用第一部分 - Xcode 创建 .framework 相关
ios打包--打包静态库(五)

呼~总算把库的相关知识看完了,写成这第二篇。花了半个月才总结完。期间各种问题,各种错误不知道怎么解决,真的累。以后还是研究些经常能用得到的东西吧。自己基本不需要打包库~

我的demo地址,欢迎参考

IOS笔记

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!

后发表评论
Blank_佐毅
2楼 · 2016.10.26 16:34

图片哪去了

张仙森@Blank_佐毅 应该是加载比较慢吧。我没有把图片传到简书。放在github仓库里了。

2016.10.26 16:36 回复
Blank_佐毅
3楼 · 2016.10.27 20:49

图片不用放在git,直接拖进来就可以

张仙森@Blank_佐毅 主阵地在gitpage上,我只把认真写的几篇顺便搬到简书上了。

2016.10.27 21:18 回复
woniu
4楼 · 2016.11.02 16:08

楼主辛苦了!

让历史重演
5楼 · 2017.02.13 12:43

使用动态库提交到APPSTORE会不会被拒?

张仙森@让历史重演 不会被拒的。但是不要想着能动态更新。

2017.02.13 14:15 回复
brownfeng
6楼 · 2017.03.09 10:44

总结的很棒

张仙森@brownfeng 哈哈共同进步哈:smile:

2017.03.09 10:46 回复

huang_weijian
7楼 · 2017.03.10 14:50

兄弟,你github的网址怎么打不开的

张仙森@huang_weijian 我可以打开啊。。。就是myocdemo中的一个

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多