之前整理过一种打包静态库的方法: 打包静态库(源码中包含其他静态库以及开源库)。不过这种方式的缺点很明显:1. 需要自己使用Xcode创建静态库工程;2. 需要手动处理代码中依赖的第三方开源库;3. 如果静态库工程依赖私有库,当这些私有库有更新时,就得把他们重新拷贝一份到静态库工程,不够灵活。
因为公司需要持续性向别人提供一套即时通讯SDK,按照之前的方式打包静态库真是太痛苦,SDK依赖的一些私有库会有频繁的更新,依赖的第三方库也是错综复杂。我迫切需要找到一种更方便的打包静态库的方式,既能随时更新私有库,也能解决开源库的冲突问题(比如你的SDK包含了AFNetworking
,别人项目中本身也含有AFNetworking
,就会产生冲),那就是使用cocoapods。
假设你已经有一堆写好的源码,并且它们依赖一堆私有库和第三方库,也许,这些依赖并非都是源码,可能也包含了静态库(.a
或者.framework
)。没关系,先放在那吧。
1. 创建你的 project
打开终端,cd 到你喜欢的某个文件路径,输入 pod lib create YOUR_POD_LIBARY_NAME
创建并初始化一个工程。
然后回答几个问题,就自动创建出一个project:
我们新建的工程以及目录如下:
其中重要文件夹我都已经展开,可以看到里面包含的内容,其他没有展开的文件夹不用管它。
从上向下看,最重要的一个就是YJDemoSDK.podspec
文件,*.podspec
是关于pod库的描述文件,它详细说明了在这个pod library
中源码应该从哪里取出、应用怎样的构建设置以及其他基本的信息,比如名称、版本、描述等。下面插播一段关于.podspec
文件的植入性广告:
podspec文件的内容如下所示,每一项是什么意思都做了简要解释,不明白可以去看官方文档:
Pod::Spec.new do |s|
s.name = 'YJDemoSDK' #项目名
s.version = '0.1.0' #相应的版本号
s.summary = 'A short description of YJDemoSDK.' #简述
s.description = <<‐ DESC #详细描述
TODO: Add long description of the pod here.
DESC
s.homepage = 'https://github.com/yangjie2/YJDemoSDK' #项目主页
s.license = { :type => 'MIT', :file => 'LICENSE' } #开源协议
s.author = { 'yangjie2' => 'yangjie2@guahao.com' } #作者
s.platform = :ios, '8.0' #支持的平台
s.requires_arc = true #arc和mrc选项
s.libraries = 'z', 'sqlite3' #表示依赖的系统类库,比如libz.dylib等
s.frameworks = 'UIKit','AVFoundation' #表示依赖系统的框架
s.ios.vendored_frameworks = 'YJKit/YJKit.framework' # 依赖的第三方/自己的framework
s.vendored_libraries = 'Library/Classes/libWeChatSDK.a' #表示依赖第三方/自己的静态库(比如libWeChatSDK.a)
#依赖的第三方的或者自己的静态库文件必须以lib为前缀进行命名,否则会出现找不到的情况,这一点非常重要
#平台信息
s.platform = :ios, '7.0'
s.ios.deployment_target = '7.0'
#文件配置项
s.source = { :git => 'https://github.com/yangjie2/YJDemoSDK.git', :tag => s.version.to_s }
#配置项目的目标路径,如果不是本地开发,pod init/update会从这个路去拉去代码
s.source_files = 'YJDemoSDK/Classes/**/*.{h,m}' #你的源码位置
s.resources = ['YJDemoSDK/Assets/*.png'] #资源,比如图片,音频文件等
s.public_header_files = 'YJDemoSDK/Classes/YJDemoSDK.h' #需要对外开放的头文件
#依赖的项目内容 可以多个
s.dependency 'YYModel'
s.dependency 'AFNetworking' '2.3'
明白了.podspec
文件是什么之后,继续往下看我们的工程目录,有个文件夹 Development Pods ,这里就是放置我们的源码和图片等资源文件的地方,要与YJDemoSDK.podspec
文件中描述的一致。当你向YOUR_POD_LIBARY_NAME/Classes
、YOUR_POD_LIBARY_NAME/Assets
添加新的/已经存在的文件,或者更新你的.podspec
时,需要运行pod install
或者pod update
。
Development Pods
Development Pods are different from normal CocoaPods in that they are symlinked files, so making edits to them will change the original files, so you can work on your library from inside Xcode. Your demo & tests will need to include references to headers using the#import <MyLib/XYZ.h> format.
Note: Due to a Development Pods implementation detail, when you add new/existing files to Pod/Classes or Pod/Assets or update your podspec, you should run pod install or pod update.
2. 配置podspec文件,添加自己的代码
上面创建了一个pod library项目YJDemoSDK
,可以看到项目的文件目录结构,并对几个重要的地方进行了单独说明。下面就开始设置它。
向Classes中添加源码替换掉ReplaceMe.m
文件(为了简单,这里添加YJDemoSDK.h
和 YJDemoSDK.m
)。YJDemoSDK.podspec
文件配置结果如下图:
配置完之后,切到终端,使用 pod lib lint ***.podspec
验证是否有效,必须没有错误,没有警告才可以通过验证。
由于YJDemoSDK依赖私有库YJHelloDog
,并且这个私有库中包含静态库(.a)文件,所以在验证时,需要键入如下命令:
pod lib lint --sources=git@git.guahao-inc.com:yangjie2/snowRepo.git,https://github.com/CocoaPods/Specs.git --use-libraries --allow-warnings
其中,--sources=git@git.guahao-inc.com:yangjie2/snowRepo.git,https://github.com/CocoaPods/Specs.git
指定了我们的私有Repo地址和cocoapods官方的Repo,并且是master分支。--use-libraries
表示依赖了静态库,--allow-warnings
忽略警告。这样就可以验证通过了。
然后cd到YJDemoSDK的Example目录,执行
pod install。
幽怨的发现,出错了,找不到YJDemoSDK依赖的YJHelloDog这个仓库。
不过问题也很显然, YJHelloDog 是私有库。在podfile文件中,添加上我自己的Repo 地址就好。
再次执行 pod install,就没问题了。
这时候再看下pod项目的目录,发现已经添加好了源码和依赖库:
3. 提交,打tag
YJDemoSDK 项目配置完成,添加了自己的源码,也添加了依赖的私有库、第三方开源库。提交所有的更新,打tag,push到托管服务器(比如gitHub等)。
打开终端,重新 cd 到 YJDemoSDK 路径,执行以下命令进行提交、打tag,推送tag到远端托管服务器。
git add .
git tag -a 0.1.0 -m 'version 0.1.0'
git push origin 0.1.0
4. 打包静态库
前面的准备工作完成,最后一步就是打包静态库了。这里需要安装一个 CocoaPods 打包插件 cocoapods-packager
。终端执行安装命令:sudo gem install cocoapods-packager
,等待安装完成。
该插件通过对引用的三方库进行重命名很好的解决了类库命名冲突的问题。
终端cd到项目所在目录下,执行以下命令即开始打包静态库:
pod package YJDemoSDK.podspec --library --force --no-mangle --spec-sources=http://git.guahao-inc.com/yangjie2/snowRepo.git,https://github.com/CocoaPods/Specs.git
# --library 表示打包成.a文件。--force 表示强制覆盖之前存在的文件
pod package YJDemoSDK.podspec --force --no-mangle --spec-sources=http://git.guahao-inc.com/yangjie2/snowRepo.git,https://github.com/CocoaPods/Specs.git
# 没有--library,则打包成.framework文件
上面的命令中,有一个是--no-mangle
,表示Do not mangle symbols of depedendant Pods
,当你的项目依赖包含静态库时,不加上这句,就会打包失败:
到此为止,.a 形式的静态库打包成功!刚才打包好的静态库就在我的项目路径下YJDemoSDK-0.1.0文件夹中,里面的文件如下图所示:
蓦然发现,没有头文件。。。我希望公开被
别人调用的头文件跑哪里去了?这样打出来静态库也没办法用。google了下,找到一种不是答案的答案,这是cocoapods package 的一个bug,不知道为啥没被修复。或者哪位老铁有了解决方法还请不吝分享下。
这样只能打包成framework了,它是有头文件的。目录如下图:
使用时,直接拖到项目中,然后 #import <YJDemoSDK/YJDemoSDK.h>
,使用#import "YJDemoSDK/YJDemoSDK.h"
形式是找不到文件的,要使用尖括号。拖到你的项目中,编译时会出错:
原因是找不到
YJHelloDog.a
文件,这个文件是我们的静态库依赖的另一个静态库,它是不会被打包进framework的,需要你手动添加进项目,所以再把依赖的这个静态库 YJHelloDog.a
拖进项目就好了,运行成功,显示toast提示:hello dog. 说明方法调用成功。至此才算结束了。
使用含有category的静态库时, selector not recognized
的解决方案
在 iOS/Mac 平台下,包含 Category 的静态库无法被正常加载,原因在于 Category 是 Objective-C 语言的特性,编译器并不会为它生成链接符号,在链接过程中便无法找到该对象文件的引用关系,链接器将会直接忽略掉 Category 对应的对象文件,从而在运行时无法找到相应的 selector。解决该问题的目标就是让链接器加载 Category 对应的对象文件,一种方法是添加编译参数让编译器加载所有的对象文件或是加载指定的对象文件;另一种方法是在 Category 的对象文件中添加 Fake symbol ,当 Fake symbol 被加载时 Category 的对象文件便一同被加载。
解决方法:
- 在编译选项 Other Linker Flags 中添加 -all_load,用于会告诉编译器 对于所有静态库中的所有对象文件,不管里面的符号有没有被用到,全部都载入,这种方法可以解决问题,但是会产生比较大的二进制文件。
- 在编译选项 Other Linker Flags 中添加 -force_load 并指定路径:
-force_load $(BUILT_PRODUCTS_DIR)/<library_name.a>`
这种方法和 -all_load 类似,不同的是它只载入指定的静态库。
- 在编译选项 Other Linker Flags 中添加 -ObjC,这个标识告诉编译器 如果在静态库的对象文件中发现了 Objective-C 代码,就把它载入,Category 中肯定会存在 Objective-C 代码。该方法与前两张类似,只是将加载的范围减少了。
- 另一种解决方法是新版本 Xcode 里 build setting 中的 Perform Single-Object PreLink,如果启用这个选项,所有的对象文件都会被合并成一个单文件(这不是真正的链接,所以叫做预链接),这个对象文件(有时被称做主对象文件 master object file)被添加到静态库中。现在如果主对象文件中的任何符号被认为是在使用,整个主对象文件都会被认为在使用,这样它里面的 Objective-C 部分就会被载入了,当然也包括 Category 对应的对象文件。
- 最后一种解决方法是在 Category 的源文件里添加 Fake symbol,并确保以某种方法在编译时引用了该 Fake symbol,这会使得 Fake symbol 对象文件被加载时它里面 Category 代码也会被载入。该方法可以控制哪些 Category 可以被正常加载,同时也不需要添加编译参数做特殊处理。
建议使用第五种方法解决问题,因为前 4 种都会增加二进制文件的体积,在第三方集成你的 SDK 时需要手动设置编译参数,会给第三方带来不好的使用体验。为了使用方便可定义一下宏:
#define FIX_CATEGORY_BUG_H(name) \
@interface FIX_CATEGORY_BUG_##name : NSObject \
+(void)print; \
@end
#define FIX_CATEGORY_BUG_M(name) \
@implementation FIX_CATEGORY_BUG_##name \
+ (void)print {} \
@end
#define ENABLE_CATEGORY(name) [FIX_CATEGORY_BUG_##name print]
在 Category 的头文件中使用 FIX_CATEGORY_BUG_H() 宏来声明一个 Fake symbol ,在 Category 的实现文件中使用 FIX_CATEGORY_BUG_M() 宏来实现该 Fake symbol。最后在找一处运行 ENABLE_CATEGORY() 宏,可以是初始化方法中,也可以是其他任何地方,只要确保它能被正常调用,目的在于该 Fake symbol 确保编译器能正常加载它。
在 64 位的 Mac 系统或者 iOS 系统下,链接器有一个 bug,会导致只包含有 Category 的静态库无法使用 -ObjC 标志来加载 Objective-C 对象文件。