在上一篇教程里,你学到了如何创建一个可复用的把手控件。然而,还不是很容易让其他开发者方便的复用这个控件。
一种共享它的方式就是直接提供源码文件。然而,这不是特别优雅。有可能你不想共享代码的实现细节。此外,开发者可能不想看见所有的东西,他们只是想继承一部分代码到自己的代码库里。
另一种方式是把你的代码编译成静态库来让开发者添加到他们的项目中去。然而这要求你来提供公共头文件,这样显得非常的笨拙。
你需要有一种简洁的方式来编译你的代码,并且它还要方便的共享和在多个项目间复用。你需要的是只是将静态库和它的头文件打包在一个文件里,然后只需要把这个文件添加到工程里就能立即开始使用。
好消息是这篇教程就是围绕这展开的。通过制作 framework,能够帮你解决这些迫在眉睫的问题。OS X 对制作 framework 有着良好的支持, Xcode 提供了一个工程模板,它包含有一个默认的构建目标还可以容纳资源文件,例如图片,音频和字体。你能够为 iOS 创建一个 framework,但这有点棘手,如果你跟着我一步一步来,你将会学到如何解决这些阻碍。
完成这篇教程,你将:
- 用 Xcode 构建一个基本静态库工程
- 构建一个依赖与这个静态库的 app
- 探索如何把静态库转换成一个完整的 framework
- 最后,你将会看到如何把一个图片文件打包到 framework 中的资源包中
现在开始
这篇教程的主要目的是解释如何在 iOS 项目中创建一个可复用的 framework,因此不会像本站中其他教程一样,只会有少量的 Objective-C 代码,用来阐述所涉及的概念。
在这里可以先下载好 RWKnobControl
资源文件。当你在 创建静态库项目
这个部分里创建第一个工程的过程中,你会看到如何使用它们。
你要创建的所有的这些代码和工程文件在 Github 上都能访问到,并且还有每个构建部分的独立提交。
什么是 Framework?
Framework 是一种资源集合,它把一个静态库和它的头文件汇集成一个单一结构,这样 Xcode 能够很容易的将其集成到你的工程中去。
在 OS X 中允许我们创建动态链接库。通过动态链接,framework 能够显式的实时更新而无需应用程序重新链接它们。在运行时,一份静态库的副本被所有进程共享,这能够显著减少内存使用提升系统性能。如你所见,这是个强大的东西。
在 iOS 中你不能用这种方式给系统添加自定义的 framework,因此只能使用 Apple 提供的动态库。
然而,这不意味着 framework 跟 iOS 就毫无相关了。对于在不同 app 中进行代码复用,静态链接库仍然是一个便捷的打包方式。
既然 framework 本质上是对静态库的一站式购物,那这篇教程中首要事情你了解如何创建和使用静态库。当这篇教程进展到构建 framework 时,你会知道接下来会发生什么。
创建静态库工程
打开 Xcode 并且通过点击 FileNewProject
和 iOSFramework and LibraryCocoa Touch Static Library
来创建一个新的静态库工程。
把工程命名为 RWUIControls
并且保存工程到一个空目录。
一个静态库工程由头文件和实现文件组成,它们由工程自己创建编译。
为了让开发者更方便的使用你的库和框架,你需要导入一个头文件来访问所有的你希望公开的类,好让他们只需访问这个头文件就行。
当创建静态库工程的时候,Xcode 添加了 RWUIControls.h
和 RWUIControls.m
。你不需要实现文件,因此右键 RWUIControls.m
选择删除,按提示把它移到垃圾箱中。
打开 RWUIControls.h
并且用下面的代码替换文件内容:
1 |
#import <UIKit/UIKit.h> |
这句代码导入了 UIKit
的伞型头文件,它包含有自身所需要的库。当你创建不同的组件类时,你要把它们添加到这个文件里,这样能够确保它们让这个库的使用者能访问。
你构建这个工程时会依赖 UIKit
,但 Xcode 静态库工程没有默认的链接到 UIkit
。为了修正这个问题,要添加 UIKit
作为一个依赖。选择工程的导航器,并且在主面板选择 RWUIControls
目标。
单击 Build Phases
然后展开 Link Binary With Libraries
部分。单击 +
来添加一个新的框架,查找 UIKit.framework
,单击 add
添加。
如果不绑定到头文件的话,静态包是没有用的。这些编译好的类和方法是包含在二进制文件中。你创建的类,有一些你可以在外部使用,另一个则只能在包内使用。
接下来,你需要在构件时添加引用,把公开的头文件放到编译者能使用的地方。最后,你要复制这些东西到框架里。
当你在Xcode里看到Build Phases 时,选择 EditorAdd Build PhaseAdd Copy Headers Build Phase.
注意:如果你发现选项变灰了,试试点击下方的空白区域看看,然后再尝试一遍。
把 RWUIControls.h
从导航器拖到面板的 Public
部分。这确保这个头文件对任何使用你库的用户都可用。
注意:这可能有点多此一举,但把包含有你工程所有公开类头文件的头文件放到公有部分非常重要。否则,开发者在企图使用这个库的时候会发生编译错误。这对任何人都不是开玩笑的,当 Xcode 读取公有头的时又不能读取你忘记添加的公有文件。
创建一个 UI 控件
现在你已经设置好了你的工程,是时候给库添加些功能了。既然这个教程的目的是讲诉如何构建一个 framework,而不是如何构建一个 UI 控件,那你会借用些上篇教程的一些代码。在你之前下载的 zip 文件你会找到 RWKnobControl 目录。把它拖到 Xcode 的 RWUIControls
组别。
选择 Copy items into destination group’s folder
并确保要拷贝的新文件勾选了响应的单选框。
这会同时把实现文件添加到编译列表,默认的头文件在 Project group
。这意味着它们都是私有的。
注意:这三个部分的命名如果不拆分开理解会有点令人误解。Public
如你所预料的。Private
头仍然会暴露你的头文件,这有点让人困惑。Project
头是你工程用到的特定私有文件,这有点讽刺。因此,你会慢慢发现要么头文件是放到 Public
要么放在 Project
部分。
现在你需要分享主控件头RWKnobControl.h ,有如下几步要做。首先从Project 组中拖拽Copy Headers 到Public 组。
另一种方式,当你编辑文件的时候会发现更改 Target Membership
面板中的值会更方便。当你开发库继续添加文件的时候这会非常方便。
注意:在你往库中添加新的类时,记得保持成员是最新的。尽可能减少公有的头文件,并确保其余的在 Project
组。
用控件的头文件做的另一件事就是把 RWUIControls.h
它添加到库的主头文件中。这样开发者使用你的库时只需要像下面这样包含这一个文件就行,而不是一堆。
1 |
#import <RWUIControls/RWUIControls.h> |
因此,把下面的代码添加到 RWUIControls.h
1 2 |
// Knob Control #import <RWUIControls/RWKnobControl.h> |
配置 Build 设置
现在你非常接近这个工程的编译部分了。然而,有几个确保库尽可能对用户友好的设置需要配置。
首先,你需要提供一个目录名给你公有头文件将要拷贝到那里去。这确保当你使用静态库的时候能定位到相关的头文件。
单击工程导航栏的工程,然后选择 RWUIControls
静态库目标。选择 Build Setting
标签,然后搜索 public header。双击 Public Header Folder Path
设置并输入下面的路径:
1 |
include/$(PROJECT_NAME) |
之后你会看到这个目录。
现在你需要改变一些其他的设置,尤其是那些保留在二进制库中的。编译器给了你移除无用代码的选项,指那些从不会访问到的代码。并且你还能移除 debug 符号,例如函数名和其他 debug 时相关的细节。
既然你创建 framework 给其他人使用,那最好把它们都禁用了然后让用户自行选择最适合他们工程的配置。要做这些的话,跟之前一样使用搜索就行,更新下面的设置:
Dead Code Stripping
– 设为 NOStrip Debug Symbols During Copy
– 设为 NO for all configurationsStrip Style – 设为 Non-Global Symbols
构建运行。你仍然什么东西看没看到,但这仍然是件好事,这足以说明工程成功的构建的并且没有警告和错误。
要构建的话,选择构建目标为 iOS Device
并按下 cmd+B
来执行构建。一旦完成,项目导航器的 Products 组别里的 libRWUIControls.a
会从红色变为黑色,这表示文件已生成。右键 libRWUIControls.a
并且选择 Show in Finder
。
在这个目录中你能看到生成的静态库,libRWUIControls.a
,并且公有头文件单独放在 include/RWUIControls
。
创建一个依赖开发项目
当你不能亲眼看到你在做什么的时候,为 iOS 开发一个 UI 控件库极其的困难,现在似乎就是这样。
没人要你盲目的工作,因此在这个部分你将会创建一个新的 Xcode 工程,它会用到你刚创建的库。这能让你通过一个示例 app 来开发 framework。自然地,这个 app 的代码会完全的与库本身的代码分离开来,这样一来会让结构更清晰。
关闭静态库工程。然后创建一个新的工程。选择 iOS/Application/Single View Application
,并取名为 UIControlDevApp
。设置类前缀为 RW
并指定仅 iPhone 可用。最后保存到 RWUIControls
相同的目录。
把 RWUIControls.xcodeproj
拖到 UIControlDevApp
组别来把 RWUIControls
作为一个依赖项。
注意:你不能在两个不同的窗口中打开同一个工程。如果你发现你不能切换到库工程,请检查你没有在另一个 Xcode 窗口中打开它。
你可以简单的拷贝代码而不是重新创建上一篇教程的 app。首先选择 Main.storyboard
,RWViewController.h
和 RWViewController.m
然后删除它们。接着拷贝 DevApp
文件夹到 UIControlDevApp
组别。
现在添加静态库作为示例 app 的依赖构建:
- * 在工程中选择
UIControlDevApp
工程。 - * 导航至
UIControlDevApp
目标的Build Phases
标签。 - * 打开
Target Dependencies
面板并单击 + 来显示选择器。 - * 找到
RWUIControls
静态库,单击Add
来添加。这个动作表示当构建示例 app 的时候,Xcode 会检查是否静态库需要重新构建。
为了链接静态库,展开 Link Binary With Libraries
面板并再次点击 +。选择 libRWUIControls.a
单击添加。
这个行为会让 Xcode 把示例 app 与静态库链接起来,就像链接系统 framework 一样比如 UIKit
。
构建运行。你会看到跟上一 篇教程中熟悉的画面。
嵌套工程的好处就是你能够在不离开示例 app 工程的情况下继续开发静态库,正如你在不同的部位维护代码一样。你每次构建项目的时候,你也要同时检查 public/project 头成员是否正确设置。如果丢失了任何必须的头文件那么示例 app 将不会成功构建。
创建 Framework
现在, 你可能会不耐烦地敲打你的脚趾并且想要知道 framework 到底什么时候才会开始。这可以理解,因为到目前为止你做了一大堆东西但还没有看到 framework。
好的,某些东西要开始变化了,马上就来了。到现在你还没有创建一个 framework 的原因是因为它就是一个静态库和头文件的集合 – 正是你之前所做的。
制作一个 framework 会有几点特别的地方:
- 目录结构。Frameworks 有着 Xcode 认可的特殊目录结构。你会创建一个构建任务,这将为你创建这种结构。
- 当你构建库的时候,它只会生成当前必须的架构,例如 i386,arm7,等等。为了让一个框架有效,在构建的时候它需要包含所有需要运行的架构。你将会创建一个新的产品,它将构建必须的架构并把它们放到框架中。
在这个部分会有大量的神奇脚本,但我会讲慢点,它们不会很复杂。
框架结构
正如之前提到的,一个框架有着特殊的目录结构,看起来像是这样:
现在在静态库编译过程中要给它添加一个脚本。选择 RWUIControls
工程,并选择 RWUIControls
静态库目标。选择 Build Phases
标签并通过选择 Editor/Add Build Phase/Add Run Script Build Phase
来添加一个新的脚本。
在 Build Phases 部分创建了一个新的面板,这能让你在编译阶段的某个时刻运行一个任意的 Bash 脚本。如果你想在编译过程中改变脚本的运行时刻就在列表中拖动面板。对于框架工程来说,在最后运行脚本就行,因此你可以默认放置即可。
双击重命名面板标题为 Build Framework
。
把下面的 Bash 脚本粘贴到脚本框中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
set -e export FRAMEWORK_LOCN="${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.framework" # Create the path to the real Headers die mkdir -p "${FRAMEWORK_LOCN}/Versions/A/Headers" # Create the required symlinks /bin/ln -sfh A "${FRAMEWORK_LOCN}/Versions/Current" /bin/ln -sfh Versions/Current/Headers "${FRAMEWORK_LOCN}/Headers" /bin/ln -sfh "Versions/Current/${PRODUCT_NAME}" \ "${FRAMEWORK_LOCN}/${PRODUCT_NAME}" # Copy the public headers into the framework /bin/cp -a "${TARGET_BUILD_DIR}/${PUBLIC_HEADERS_FOLDER_PATH}/" \ "${FRAMEWORK_LOCN}/Versions/A/Headers" |
这段脚本首先创建了 RWUIControls.framework/Versions/A/Headers
目录,然后创建了一个框架所必须的三个语法链接
Versions/Current => A
Headers => Versions/Current/Headers
RWUIControls => Versions/Current/RWUIControls
最后,公有头文件从你之前指定的公有头文件路径拷贝到 Versions/A/Headers
目录。-a
参数确保了在拷贝的时候编辑时间不会改变,从而防止不必要的重新构建。
现在,选择 RWUIControls
静态库方案和 iOS Device
构建目标,然后通过 cmd+B
构建。
右键 libRWUIControls.a
并在 Finder
中显示。
在构建目录中你可以访问到 RWUIControls.framework
,并确认目录的结构显示的是正确的:
在完成你框架的道路上这真是一个质的飞跃,但你会发现仍然没有一个静态库。这就是接下来要做的。
多架构构建
iOS app 需要在不同的架构上运行:
- arm7: 用于 iOS 7 所支持的最老的设备
- arm7s: 用于 iPhone 5 和 5C
- arm64: 用于 iPhone 5S 和 iPhone 6 等 64-bit ARM 处理器
- i386: 用于 32-bit 模拟器
- x86_64: 用于 64-bit 模拟器
每种架构都需要不同的二进制文件,并且当你构建一个 app 的时候,无论你当前是何种设备 Xcode 都会正确的构建相应的架构。
这意味着构建会很快。当你归档 app 或构建 release 模式的 app 时,Xcode 会构建所有的三种 ARM 架构,从而让 app 运行到大部分设备上。那其他的版本呢?
自然地,当你构建框架时,你想要开发者能够尽可能使用所有的架构,对吗?如果是这样那表示你会得到同行的尊敬与敬佩。
因此你需要让 Xcode 构建所有的五种架构。这个过程会创建一个所谓的臃肿的库,它包含有每个架构部分。啊哈!
注意:其实这里强调的另一个原因是要创建一个依赖静态库的示例 app:这个库只为示例 app 需要的架构构建,并只会在某些东西改变的时候才重新编译。为什么这会令你异常兴奋?因为这会让开发周期尽可能的缩短。
单击 RWUIControls 工程,创建一个新的目标(target)。
选择 iOS/Other/Aggregate
, 单击 Next
并命名目标为 Framework
。
注意:为什么要使用 Aggregate
目标来构建一个 Framework 为什么不直接新建?因为 Frameworks 对 OS X 的支持更好,这个事实体现在 Xcode 为 OS X 应用提供了一个非常方便直接的 Cocoa Framework 构建目标。为了解决这个问题,你要使用 Aggregate
构建目标(target)来做为编译框架目录结构的 bash 脚本的钩子(hook)。你开始明白这里面疯狂的地方了吗?
无论何时创建一个新的 framework 目标(target)都必须确保添加了静态库依赖。选择 Framework 目标(target)和 Build Phases
标签。展开 Target Dependencies
面板并添加静态库依赖。
这个目标的主要构建部分是多平台编译,你将会用到脚本来执行。正如你之前所做的,在 Build Phases
中创建一个 Run Script
。
双击,把名字命名为 MultiPlatform Build
。
粘贴下面的脚本到脚本框中:
1 2 3 4 5 6 7 8 9 10 11 |
set -e # If we're already inside this script then die if [ -n "$RW_MULTIPLATFORM_BUILD_IN_PROGRESS" ]; then exit 0 fi export RW_MULTIPLATFORM_BUILD_IN_PROGRESS=1 RW_FRAMEWORK_NAME=${PROJECT_NAME} RW_INPUT_STATIC_LIB="lib${PROJECT_NAME}.a" RW_FRAMEWORK_LOCATION="${BUILT_PRODUCTS_DIR}/${RW_FRAMEWORK_NAME}.framework" |
set -e
确保如果脚本的某部分失败了那就让整个脚本都失败。这能帮你避免生成不完全的 framework。- 接下来,
RW_MULTIPLATFORM_BUILD_IN_PROGRESS
变量决定是否脚本有被递归的调用。如果有,那就退出执行。 - 然后就是设置一些变量。框架的名字将会跟工程名字一样,例如
RWUIControls
,还有静态库是libRWUIControls.a
。
接下来的脚本会设置些工程随后会用到的函数。把下面的代码添加到脚本框的底部:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
function build_static_library { # Will rebuild the static library as specified # build_static_library sdk xcrun xcodebuild -project "${PROJECT_FILE_PATH}" \ -target "${TARGET_NAME}" \ -configuration "${CONFIGURATION}" \ -sdk "${1}" \ ONLY_ACTIVE_ARCH=NO \ BUILD_DIR="${BUILD_DIR}" \ OBJROOT="${OBJROOT}" \ BUILD_ROOT="${BUILD_ROOT}" \ SYMROOT="${SYMROOT}" $ACTION } function make_fat_library { # Will smash 2 static libs together # make_fat_library in1 in2 out xcrun lipo -create "${1}" "${2}" -output "${3}" } |
build_static_library
需要SDK
作为参数,例如iphoneos7.0
,然后会构建相应的静态库。大部分参数都是直接从当前的构建任务中传进来,但不同的地方在于ONLY_ACTIVE_ARCH
是用来确保为当前的 SDK 构建所有的架构。make_fat_library
使用lipo
把两个静态库变成一个。它的参数是两个输入库后面紧跟着输出位置。点击来了解更多关于 lilp 的信息。
下个部分的脚本确定了更多变量,为了你能使用上面两个方法。你需要知道其他的 SDK 是什么,例如 iphoneos7.0
应该跳转到 iphonesimulator7.0
反之亦然,还要定位 SDK 的构建目录。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
# 1 - Extract the platform (iphoneos/iphonesimulator) from the SDK name if [[ "$SDK_NAME" =~ ([A-Za-z]+) ]]; then RW_SDK_PLATFORM=${BASH_REMATCH[1]} else echo "Could not find platform name from SDK_NAME: $SDK_NAME" exit 1 fi # 2 - Extract the version from the SDK if [[ "$SDK_NAME" =~ ([0-9]+.*$) ]]; then RW_SDK_VERSION=${BASH_REMATCH[1]} else echo "Could not find sdk version from SDK_NAME: $SDK_NAME" exit 1 fi # 3 - Determine the other platform if [ "$RW_SDK_PLATFORM" == "iphoneos" ]; then RW_OTHER_PLATFORM=iphonesimulator else RW_OTHER_PLATFORM=iphoneos fi # 4 - Find the build directory if [[ "$BUILT_PRODUCTS_DIR" =~ (.*)$RW_SDK_PLATFORM$ ]]; then RW_OTHER_BUILT_PRODUCTS_DIR="${BASH_REMATCH[1]}${RW_OTHER_PLATFORM}" else echo "Could not find other platform build directory." exit 1 fi |
这四个语句看起来都非常相似,它们使用字符串比较和正则表达式来确定 RW_OTHER_PLATFORM
和 RW_OTHER_BUILT_PRODUCTS_DIR
的值。
这四个 if
语句的详细解释:
- SDK_NAME将会是
iphoneos7.0
或iphonesimulator6.1
。这个正则表达式从字符串的开头处开始提取非数字字符。因此,它的结果是iphoneos
或者iphonesimulator
。 - 这个正则表达式从
SDK_NAME
变量取得数字版本号,例如 7.0 或 6.1 等等。 - 这是简单的
iphonesimulator
和iphoneos
之间的字符串比较,反之亦然。 - 从产品构建目录路径的末尾处得到平台名称并用其他平台替换。这个确保其他平台的构建目录能被找到。当加入两个静态库的时候这至关重要。
现在你可以为其他平台编译了,随后会加入产生的静态库。
把下面的脚本添加到末尾处: