How to create a Framework for iOS (二)创建 Framework

Building a Framework

现在, 你可能会不耐烦地敲打你的脚趾并且想要知道 framework 到底什么时候才会开始。这可以理解,因为到目前为止你做了一大堆东西但还没有看到 framework。

好的,某些东西要开始变化了,马上就来了。到现在你还没有创建一个 framework 的原因是因为它就是一个静态库和头文件的集合 - 正是你之前所做的。

制作一个 framework 会有几点特别的地方:

  1. 目录结构。Frameworks 有着 Xcode 认可的特殊目录结构。你会创建一个构建任务,这将为你创建这种结构。

  2. 当你构建库的时候,它只会生成当前必须的架构,例如 i386,arm7,等等。为了让一个框架有作用,在构建的时候它需要包含所有需要运行的架构。你将会创建一个新的产品,它将构建必须的架构并把它们放到框架中。

在这个部分会有大量的神奇脚本,但我会讲慢点,它们不会很复杂。

Framework Structure

正如之前提到的,一个框架有着特殊的目录结构,看起来像是这样:

ios_framework_directory_structure

现在在静态库编译过程中要给它添加一个脚本。选择 RWUIControls 工程,并选择 RWUIControls 静态库目标。选择 Build Phases 标签并通过选择 Editor/Add Build Phase/Add Run Script Build Phase 来添加一个新的脚本。

ios_framework_framework_add_run_script_build_phase

在 Build Phases 部分创建了一个新的面板,这能让你在编译阶段的某个时刻运行一个任意的 Bash 脚本。如果你想在编译过程中改变脚本的运行时刻就在列表中拖动面板。对于框架工程来说,在最后运行脚本就行,因此你可以默认放置即可。

ios_framework_new_run_script_build_phase

Rename the script by double clicking on the panel title Run Script and replace it with Build Framework.
双击重命名面板标题为 Build Framework

ios_framework_rename_script

把下面的 Bash 脚本粘贴到脚本框中。

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 构建。

ios_framework_build_target_static_lib

右键 libRWUIControls.a 并在 Finder 中显示。

ios_framework_static_lib_view_in_finder

在构建目录中你可以访问到 RWUIControls.framework,并确认目录的结构显示的是正确的:

ios_framework_created_framework_directory_structure

在完成你框架的道路上这真是一个质的飞跃,但你会发现仍然没有一个静态库。这就是接下来要做的。

多架构构建

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_framework_add_target_button

选择 iOS/Other/Aggregate, 单击 Next 并命名目标为 Framework

ios_framework_select_aggregate_target

注意:为什么要使用 Aggregate 目标来构建一个 Framework?为什么不直接新建?因为 Frameworks 对 OS X 的支持更好,这个事实体现在 Xcode 为 OS X 应用提供了一个非常方便直接的 Cocoa Framework 构建目标。为了解决这个问题,你要使用 Aggregate 构建目标(target)来做为编译框架目录结构的 bash 脚本的钩子(hook)。你开始明白这里面疯狂的地方了吗?

无论何时创建一个新的 framework 目标(target)都必须确保添加了静态库依赖。选择 Framework 目标(target)和 Build Phases 标签。展开 Target Dependencies 面板并添加静态库依赖。

ios_framework_add_dependency_to_framework_target

这个目标的主要构建部分是多平台编译,你将会用到脚本来执行。正如你之前所做的,在 Build Phases 中创建一个 Run Script

ios_framework_framework_add_run_script_build_phase

双击,把名字命名为 MultiPlatform Build

粘贴下面的脚本到脚本框中:

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

接下来的脚本会设置些工程随后会用到的函数。把下面的代码添加到脚本框的底部:

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 - 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_PLATFORMRW_OTHER_BUILT_PRODUCTS_DIR 的值。

这四个 if 语句的详细解释:

  1. SDK_NAME 将会是 iphoneos7.0iphonesimulator6.1。这个正则表达式从字符串的开头处开始提取非数字字符。因此,它的结果是 iphoneos 或者 iphonesimulator
  2. 这个正则表达式从 SDK_NAME 变量取得数字版本号,例如 7.0 或 6.1 等等。
  3. 这是简单的 iphonesimulatoriphoneos 之间的字符串比较,反之亦然。
  4. 从产品构建目录路径的末尾处得到平台名称并用其他平台替换。这个确保其他平台的构建目录能被找到。当加入两个静态库的时候这至关重要。

现在你可以为其他平台编译了,随后会加入产生的静态库。

把下面的脚本添加到末尾处:

# Build the other platform.
build_static_library "${RW_OTHER_PLATFORM}${RW_SDK_VERSION}"
 
# If we're currently building for iphonesimulator, then need to rebuild
#   to ensure that we get both i386 and x86_64
if [ "$RW_SDK_PLATFORM" == "iphonesimulator" ]; then
    build_static_library "${SDK_NAME}"
fi
 
# Join the 2 static libs into 1 and push into the .framework
make_fat_library "${BUILT_PRODUCTS_DIR}/${RW_INPUT_STATIC_LIB}" \
                 "${RW_OTHER_BUILT_PRODUCTS_DIR}/${RW_INPUT_STATIC_LIB}" \
                 "${RW_FRAMEWORK_LOCATION}/Versions/A/${RW_FRAMEWORK_NAME}"
  • 首先通过之前定义好的函数来编译其他平台。
  • 如果你当前要为模拟器编译,那默认的 Xcode 只会为那个系统编译,例如 i386 或者 x86_64。为了编译所有的架构,第二部分调用 build_static_libraryiphonesimulator SDK 重新编译,来确保编译了所有架构。
  • 最后调用 make_fat_library 函数把当前构建目录的静态库和其他构建目录加到一起来制作完整的多架构静态库。这个会放到 framework 里面。

最后是个简单的拷贝命令的脚本。在末尾添加下面的脚本:

# Ensure that the framework is present in both platform's build directories
cp -a "${RW_FRAMEWORK_LOCATION}/Versions/A/${RW_FRAMEWORK_NAME}" \
      "${RW_OTHER_BUILT_PRODUCTS_DIR}/${RW_FRAMEWORK_NAME}.framework/Versions/A/${RW_FRAMEWORK_NAME}"
 
# Copy the framework to the user's desktop
ditto "${RW_FRAMEWORK_LOCATION}" "${HOME}/Desktop/${RW_FRAMEWORK_NAME}.framework"
  • 第一个命令保证 framework 出现在多平台的构建目录里。
  • 第二个部分拷贝完成的 framework 到用户的桌面。这是可选步骤,但我发现把 framework 放到某个容易访问的地方会非常友好。

选择 Framework 集合(aggregate) 方案,并按下 cmd+B 来编译框架。

ios_framework_select_framework_aggregate_scheme

编译完成后桌面会出现 RWUIControls.framework

ios_framework_built_framework_on_desktop

为了检查多平台时候正确编译,启动终端并执行以下操作:

$ cd ~/Desktop/RWUIControls.framework
$ RWUIControls.framework  xcrun lipo -info RWUIControls

第一行是切换到框架目录,第二行使用了 lipo 命令来得到关于 RWUIControls 库的相关信息。这会列出这个库里出现的所有部分。

ios_framework_architectures_in_fat_library

你能看到这儿有五个部分:i386, x86_64, arm7, arm7s and arm64,正好是你在编译的时候设置的。你之前运行过 lipo -info 命令,你会看到这几个部分的子集。

本教程最后一节: How to create a Framework for iOS (三)使用 Framework 与 Bundles