我们小组需要持续向其他公司或部门提供一套IM(即时通讯)SDK,为了隐藏源码实现,就需要将它打包为静态库(.a/.framework)的形式。该项目使用cocoapods管理,依赖许多私有库,并且私有库又依赖私有库、开源库等,这些私有库又会涉及频繁更新,每次打包静态库时都需要保证是最新的代码,并且还要解决各个库的依赖问题。在这种情况下,怎么才能方便快速的打包静态库呢?幸好 cocoapods 已经给我们提供了一个打包插件:Cocoapods-package,这让打包静态库的任务变得简单许多。
基础理论
xcode中的 workspace, project, target, scheme概念
wrokspace
A workspace is an Xcode document that groups projects and other documents so you can work on them together. A workspace can contain any number of Xcode projects, plus any other files you want to include. In addition to organizing all the files in each Xcode project, a workspace provides implicit and explicit relationships among the included projects and their targets.
workspace 是最大的集合,包含一个或多个 project,同时 workspace 可以管理它所包含的 project 之间隐式或显式的关系。workspace 是以 xcworkspace 的文件形式存在的(这点和 project 一致)。workspace 的存在是为了解决不同 project 之间引用和调用困难的问题.
By default, all the Xcode projects in a workspace are built in the same directory, referred to as the workspace build directory. Each workspace has its own build directory. Because all of the files in all of the projects in a workspace are in the same build directory, all of these files are visible to each project. Therefore, if two or more projects use the same libraries, you don’t need to copy them into each project folder separately.
同一个 workspace 下的所有 project 共用同一个编译路径。
project
An Xcode project is a repository for all the files, resources, and information required to build one or more software products. A project contains all the elements used to build your products and maintains the relationships between those elements. It contains one or more targets, which specify how to build products. A project defines default build settings for all the targets in the project (each target can also specify its own build settings, which override the project build settings).
一个 xcode project 包含了这个项目所有的文件、资源以及构建一个或多个 product 所需的信息。project 包含了构建你的应用所需要的所有元素,并维护这些元素之间的关系。project 包含一个或者多个 target,target 描述了怎样去构建 product。project 定义了其包含的所有 project 的 target 的默认编译设置,每个 target 都可以自定义编译选项,自定义配置会覆盖掉 project 的默认配置。
一个 project 可以是独立存在的,也可以被包含到 workspace 中。一个 xcode project 文件包含如下信息:
References to source files:
- Source code, including header files and implementation files
- Libraries and frameworks, internal and external
- Resource files
- Image files
- Interface Builder (nib) files
Groups used to organize the source files in the structure navigator
- Project-level build configurations.
You can specify more than one build configuration for a project; for example, you might have debug and release build settings for a project. - Targets, where each target specifies:
- A reference to one product built by the project
- References to the source files needed to build that product
- The build configurations that can be used to build that product, including dependencies on other targets and other settings; the project-level build settings are used when the targets’ build configurations do not override them
- The executable environments that can be used to debug or test the program, where each executable environment specifies:
- What executable to launch when you run or debug from Xcode
- Command-line arguments to be passed to the executable, if any
- Environmental variables to be set when the program runs, if any
You use Xcode schemes to specify which target, build configuration, and executable configuration is active at a given time.
target
A target specifies a product to build and contains the instructions for building the product from a set of files in a project or workspace. A target defines a single product; it organizes the inputs into the build system—the source files and instructions for processing those source files—required to build that product. Projects can contain one or more targets, each of which produces one product.
字面意思是“目标”,target 指定了如何编译 product ,它负责向编译系统提供”输入”(源文件以及编译这些源文件的配置说明)。
如果你现在有一个产品,你要做不同的环境出来,包括线上、预发、日常等等。这个时候你就可以来建立多个Target来实现。你先选中Targets里面的默认的第一个,然后右击弹出一个小列表:(Duplicate、Delete、Project Editor Help),顾名思义,Duplicate就是复制的意思,你可以选择一个Target进行复制,然后通过修改其General、Build Settings以及Build Phases来进行定制化修改,在Build Settings里面有一个Preprocessor Macros的选项,你可以直接设置定义宏的方式来对不同的Target进行区分。
scheme
An Xcode scheme defines a collection of targets to build, a configuration to use when building, and a collection of tests to execute.You can have as many schemes as you want, but only one can be active at a time. You can specify whether a scheme should be stored in a project—in which case it’s available in every workspace that includes that project, or in the workspace—in which case it’s available only in that workspace. When you select an active scheme, you also select a run destination (that is, the architecture of the hardware for which the products are built).
静态库和动态库
库(library)是共享程序代码的方式,一般分为静态库和动态库。
静态库: 即静态链接库,是一系列从源码编译得到的目标文件的集合,是你的源码的实现所对应的二进制。链接时,静态库会被完整地复制到目标程序中,被多次使用就有多份冗余拷贝。
在 iOS 8 之前,iOS 只支持以静态库的方式来使用第三方的代码。
静态库的优点是,编译完成之后,原始静态库实际上就没有作用了,应用程序没有外部依赖(因为依赖的静态库已经被完整的拷贝进来),直接就可以运行。当然其缺点也很明显,就是会使用应用程序的体积增大。
不同平台下的静态库文件格式如下表:
系统 | 静态库文件 |
---|---|
Windows | .lib |
Linux | .a |
MacOS/iOS | .a |
动态库: 一个没有main函数的可执行文件。动态库在链接时并不会被拷贝到目标程序中,目标程序中只会存储指向动态库的引用。等到程序运行时,动态库才会被真正加载进内存,这也是叫做动态库的原因。
动态库的优点是,不影响目标程序的体积,可以随时对库进行升级替换而不需要重新编译。
不同平台下的动态库文件格式如下表:
系统 | 动态库文件 |
---|---|
Windows | .dll |
Linux | .so |
MacOS/iOS | .dylib |
–
MacOS/iOS 里的 Framework
除了上面提到的 .a 和 .dylib/.tbd 之外,Mac OS/iOS 平台还可以使用 Framework。Framework 是一种特殊的文件夹,将库的二进制文件,头文件和有关的资源文件打包到一起,方便管理和分发。
系统的 framework 是存在于系统内部,而不会打包进 app 中。app 的启动的时候会检查所需要的动态框架是否已经加载。像 UIKit 之类的常用系统框架一般已经在内存中,就不需要再次加载,这可以保证 app 启动速度。相比静态库,framework 是自包含的,你不需要关心头文件位置等,使用起来很方便。
在 iOS 8 之前,iOS 平台不支持使用动态 Framework,开发者可以使用的 Framework 只有苹果自家的 UIKit.Framework,Foundation.Framework 等。这种限制可能是出于安全的考虑。换一个角度讲,因为 iOS 应用都是运行在沙盒当中,不同的程序之间不能共享代码,同时动态下载代码又是被苹果明令禁止的,没办法发挥出动态库的优势,实际上动态库也就没有存在的必要了。
iOS 8/Xcode 6 推出之后,iOS 平台添加了动态库的支持,同时 Xcode 6 也原生自带了 Framework 支持(动态和静态都可以)。为什么 iOS 8 要添加动态库的支持?唯一的理由大概就是 App Extension 的出现,可以为一个应用创建插件。Extension 和 App 是两个分开的可执行文件,同时需要共享代码,这种情况下动态库的支持就是必不可少的了。但是这种动态 Framework 和系统的 UIKit.Framework 还是有很大区别。系统的 Framework 不需要拷贝到目标程序中,我们自己做出来的 Framework 哪怕是动态的,最后也还是要拷贝到 App 沙盒中(App 和 App Extension 的 Bundle 是共享的),因此苹果又把这种 Framework 称为 Embedded Framework。
由于 iOS 的沙盒机制,自己创建的 Framework 和系统 Framework 不同,App 中使用的 Framework 运行在沙盒里,而不是系统中。每个 App 都只能用自己对应签名的动态库,做不到多个 App 使用一个动态库。也就是说,如果不同的 App 使用了同一个动态库 Framework,那该 Framework 会被分别签名、打包和加载。所以,iOS 上我们自己创建的动态库只能是私有的,无法将动态库放置在除了自身沙盒以外的地方。
swift 项目支持静态库
Xcode9 之前 Swift 项目不支持静态库。所以如果你使用的依赖中包含 Swift 代码,又想使用 CocoaPods 来管理的话,必须选择开启 user_frameworks!
。use_frameworks!
会把项目的依赖全部改为 framework。也就是说这是一个 none or all 的更改。你无法指定某几个框架编译为动态,某几个编译为静态。
终于在 Xcode 9 版本,swift 带来对静态库的原生支持。并且,CocoaPods 在前一段时间发布了 1.5 版本,其中有一个对于使用了 Swift 的项目非常重要的 feature,那就是支持了 Swift 的 Static Library。
With CocoaPods 1.5.0, developers are no longer restricted into specifying
use_frameworks!
in their Podfile in order to install pods that use Swift. Interop with Objective-C should just work. However, if your Swift pod depends on an Objective-C, pod you will need to enablemodular headers
(see below) for that Objective-C pod.As a pod author, you can add
DEFINES_MODULE => 'YES'
to your pod_target_xcconfig. Alternatively, in your Podfile you can adduse_modular_headers!
to enable the stricter search paths and module map generation for all of your pods, or you can add:modular_headers => true
to a single pod declaration to enable for only that pod.
以上补充了几点基础知识,能够让你对xcode文件结构和各个文件的作用有了比较清晰的了解,也知道了静态库、动态库,以及为什么之前需要在 podfile 中添加那句 use_frameworks!
。现在可以开始打包静态库的工作了。
打包静态库
创建一个 pod
如果当前已经存在一个 project,创建pod也非常简单:
1 | $ pod spec create [POD_NAME]] |
然而创建并配置工程这种费力的活交给 cocoapods 会更好。
Running pod lib create [pod name] will set you up with a well thought out library structure allowing you to easily include your files and get started quickly.
所以我们这里使用 pod lib create [pod name] 去创建一个pod,过程如下:
1 | $ pod lib create YJDemoSDK |
创建完成之后,会自动打开这个pod工程,文件结构如下图所示:
podspec
.podspec 文件描述了一个 pod 库的版本。它详细说明了这个 pod 库中源码应该从哪里取出、应用怎样的构建设置以及其他基本的信息,比如名称、版本、描述等。
podspec 文件的内容如下,并做了简要注释,不过还是强烈建议阅读下官方文档:
1 | Pod::Spec.new do |s| |
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.
Development Pods 是符号链接文件,所以对它的编辑也会改变原始文件。这样就能在xcode内开发你的pod库。当你向 Pod/Classes 或 Pod/Assets 添加新的/已经存在的文件,或者更新你的 .podspec 时,需要再次运行 pod install 或者 pod update。
配置完 podspec 之后,需要对它进行验证,没有错误和警告就能通过验证了。
1 | pod lib lint ***.podspec //仅本地验证 |
在实际使用中,我们的pod库依赖私有库,开源库,并且依赖静态库,所以验证时需要指定 sources ,私有Repo地址和cocoapods官方的 Repo 地址,并且默认是 master 分支。
1 | pod lib lint PODNAME.podspec --sources=git@*******:yangjie2/snowRepo.git,https://github.com/CocoaPods/Specs.git --use-libraries --allow-warnings |
–use-libraries表示依赖了静态库,–allow-warnings忽略警告。
验证过程也许会比较艰难,因为总会出现各种 errors,不过基本上 google 下都能解决,这里简单记录下我遇到的两个问题:
- pod本地缓存引起的 build error,明明 xcode 都能编译通过,并且远端服务器上的代码也同步了最新的,但就是执行 pod spec lint 时出现一堆
- ERROR | xcodebuild:
。仔细分析下发现,出错的地方依旧是是老版本的代码导致,所以确定是由 pod 缓存引起的。执行 pod cache clean 清除缓存,就可以了。 - 一个特别奇怪的 error,
error: cannot synthesize weak property in file using manual reference counting
. 这到底是什么鬼?分析了半天,终于找到原因,s.requires_arc = true
这句话,我把true
写成了'true'
,记得这里一定不要加单引号,否则 cocoapods 认为你的 pod 库是非 ARC 的。
刚才说到,pod lib lint 和 pod spec lint 的区别:
The difference between them is that pod lib lint does not access the network, whereas pod spec lint checks the external repo and associated tag.
上面使用的是 pod lib lint 验证本地pod,其实只要保证本地和远端服务器的代码是一致的,使用 pod spec lint 肯定可以验证通过。只不过有一点需要注意,在使用 pod spec lint 时,验证的是远端,也许你会遇到这种 error:1
2
3
4
5
6- ERROR | [iOS] unknown: Encountered an unknown error ([!] /usr/bin/git clone git@*********/WYAVTencentSDK.git /var/folders/8v/jks4fgp55897h3tpp65jp9680000gn/T/d20180808-84364-vuvr0v --template= --single-branch --depth 1 --branch 0.1.0
Cloning into '/var/folders/8v/jks4fgp55897h3tpp65jp9680000gn/T/d20180808-84364-vuvr0v'...
warning: Could not find remote branch 0.1.0 to clone.
fatal: Remote branch 0.1.0 not found in upstream origin
fatal: The remote end hung up unexpectedly
它提示你找不到 0.1.0 这个分支(因为在 podspec 文件中我们指定了 s.version = ‘0.1.0’),当然找不到了,因为确实没有这个分支。因为我们暂时还没有打 0.1.0 这个tag。当我打了 0.1.0 tag 后,再次使用 pod spec lint 验证,就顺利通过了。
到现在为止,我们完成了对 pod 库的配置,并且打了tag。接下来就开始打包静态库!
打包静态库
需要安装一个 CocoaPods 打包插件 cocoapods-packager
。终端执行安装命令:sudo gem install cocoapods-packager
,等待安装完成。
cocoapods-packager allows you to generate a framework or static library from a podspec.
执行以下命令,开始打包静态库
1 | pod package YJDemoSDK.podspec --library --force --no-mangle --spec-sources=http://*******/yangjie2/snowRepo.git,https://github.com/CocoaPods/Specs.git |
1 | --force |
打包完成,生成的静态库放在 pod 库路径下 PODNAME-0.1.0 文件夹中。若是 .a 类型的静态库,是没有头文件的,需要手动将头文件拷贝过来才能正常使用。而 framework 则可以直接放在项目中使用。
使用含有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 并指定路径:1
-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 时需要手动设置编译参数,会给第三方带来不好的使用体验。为了使用方便可定义一下宏:1
2
3
4
5
6
7
8
9
10
11#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 对象文件。
以上就是使用 cocoapods 打包静态库的所有内容了,也许你跟着这个步骤一步步做的时候,会出现意外的错误,不要灰心,用好 google 比什么都重要。
需要注意的是,使用 cocoapods-packager 打包静态库时,podspec 文件中依赖的其他库,会单独编译打包成对应的静态库,所以依赖的库不会打包进主静态库中。想要使用 cocoapods-packager 打包一个完整的静态库,也就是它所有依赖的库也都打包进去,是需要自己去实现的(使用 lipo、ar 等命令)。
Mac 下 lipo、ar、nm 命令的使用
(1) lipo 的使用
lipo 是创建和操作“通用文件”(多种CPU架构混合的二进制文件)的命令。它仅生成一个输出文件,并且不改变原始输入文件。
通俗的讲,lipo 的作用是,将多架构类型的文件拆分成单独某个架构类型的文件,或者将多个不同架构类型的文件合成为一个 fat 的通用文件(多架构类型文件)。所以 lipo 进行各种操作的本质是处理 “架构类型”,而不是操作文件内容。
1 | lipo [-info] [-detailed_info] [-arch arch_type input_file] ... [ input_file] |
它能做的事情包括:
- 列出通用文件所支持的全部架构类型
- 从多个/单个输入文件创建一个通用架构的fat文件
- 从一个通用fat文件,thin 分离出一个指定架构的文件
- 从输入文件抽取、替换、移除某个架构类型,从而创建出一个单独的新的通用输出文件。
除了 -arch, -arch_blank, -output, 以及 -segalign 这四个 option 可以与其他 option 结合使用以外,剩下的 option 都仅能使用一个。参数 input_file (输入文件) 是必选的,并且仅仅 -create 这个 option 允许有多个输入文件。除了 -info 和 -detailed_info-output 之外,-output 是必选的。
OPTIONS:
1 | -info |
(2)ar 创建/维护 library 归档
1 | # ar -t File.a //显示所有.o文件清单 |
(3)nm 查看文件符号表
nm用来列出目标文件的符号清单
参考文献
Pod Authors Guide to CocoaPods Frameworks