Xcode 制作 Framework & XCFramework

带你逛逛编译原理及如何使用 Xcode 制作 .framework 和 .xcframework。

2020-06-14, evanxlh
字数: 3.9k, 阅读时间: 15 分钟
浏览次数:

使用 Xcode 制作 Framework 与 XCFramework

最近公司有个项目外包,我就负责提供离在线语音识别 SDK 和数据埋点 SDK 封装,在制作 Framework 的过程中,遇到了很多问题。所以在这篇文章里我们会主要介绍下 如何制作 Frameworks ,以及如何解决遇到的一些问题。

编译过程简述

在制作 Framework 之前,我想简单阐述下编译器的工作原理,这有助于我们理解静态库动态库的制作。如果想了解编译器的详细设计,请点这里

我们都知道,计算机没法直接理解我们人类用高级语言写的程序,所以编译器可以帮助我们将高级语言写的程序转换成计算机能懂的二进制。下面我们结合一个简单的 C 程序来讲解下具体的编译过程。

使用 GCC 编译源程序

GCC(GNU Compiler Collection,GNU编译器套件)是由GNU开发的编程语言译器。GNU编译器套件包括C、C++、 Objective-C、 Fortran、Java、Ada和Go语言前端,也包括了这些语言的库(如libstdc++,libgcj等。)

GCC 的初衷是为 GNU 操作系统专门编写的一款编译器。GNU 系统是彻底的自由软件,此处“自由”的含义是它尊重用户的自由。

编写源程序

我们先编写一个简单的 C 程序,然后存储为 hello.c 源文件。

#include <stdio.h>
int main()
{
printf("hello, world\n");
return 0;
}

GCC 编译

  • 编写好源程序后,在控制终端输入以下命令,gcc 自动会完成所有编译过程,最后输出可执行文件 hello

    gcc -o hello hello.c
  • 在终端运行可执行文件 hello,终端就会输出 hello, world

    ./hello

我们可以看到,一个 gcc 命令就将源程序转变成了可执行文件,它使用源程序编译变得非常简单。但我们还是得深究下,gcc 究竟做了些啥事。接下来,我们就简单介绍下编译器的四个阶段:预处理、编译、汇编,以及链接。

编译过程

预处理阶段

CPP,即 C Pre-Processor, 即 C 预处理器,是一个独立于C 编译器的小程序,预编译器并不理解 C 语言语法,它仅是在程序源文件被编译之前,实现文本替换的功能。

hello.c 源程序为例,CPP 会根据 # 开头的指令来修改源程序。比如 hello.c 中的第一行 #include 就是告诉预处理器去把系统头文件 stdio.h 内容读取出来,并直接插入到 hello.c 源程序中来,替换 #include

修改后的 C 程序一般另保存为 .i 后缀的文本文件(本例为 hello.i),输出的 hello.i 将用于下一个阶段。

编译阶段

接下来编译器 cc1hello.i 编译成汇编程序,并保存为 .s 后缀的汇编语言文本文件(本例为 hello.s)。

编译成汇编语言程序有个好处,就是对于不同的编译器,不同的高级语言,都会编译输出一样的汇编程序。

例如,C 编译器 和 Fortran 编译器,编译后都会输出同样的汇编程序。

汇编阶段

目标文件 (Object File)

在介绍汇编阶段前,我们先了了解下目标文件(Object File), 它其实有 3 种形式:

  • 可重定位目标文件 (Relocatable Object File)

    包含可与其它 relocatable object file 相结合的二进制代码和数据,由编译器和汇编器产生。

  • 可执行文件 (Executable Object File)

    包含可直接复制到内存并执行的二进制代码和数据,由链接器生成。

  • 共享目标文件 (Shared Object File)

    一种特殊的 relocatable object file,它可以被装载入内存,并且可以在装载或运行的时候动态地链接。

    由编译器和汇编器产生。

汇编阶段的工作内容

汇编阶段的工作就是将汇编文本程序翻译成机器指令,输出目标文件。

汇编器 (as) 将汇编程序 hello.s 翻译成机器指令,然后包装成可重定位目标程序(Relocatable Object Program),并将其结果保存在 hello.o 文件中。hello.o 为二进制文件。

链接阶段

在我们的 hello 程序中,它调用了C 标准库中 printf 方法,而 printf 位于事先编译好的独立目标文件 printf.o 中。所以为了能让 hello 程序运行起来,我们需要采取某种手段将 printf.o 合并入 hello.o。幸运的是,链接器(ld) 就是做这一工作。

经过链接器的合并操作后,输出 hello 可执行文件。在终端命令行中输入 ./hello 回车后, hello 可执行文件被装载入内存(通过加载器Loader来完成),并由系统执行,程序就跑起来了。

Library

我们在开发过程中,会把一些通用的函数制作成一个库,或者将一个功能模块制作成一个库 ,然后提供给 App 使用,可以达到共享复用的目的。

Library 可以理解成目标文件的集合,将相关的目标文件打包在一起,就成了一个 Library。我们按照 Library 是如何链接到 App 中的,可以把 Library 分成静态库和动态库。

静态库(Static Libraries)

下图为 App 用到了静态库的情况。App 自身的代码被编译成目标文件后,通过静态链接器将App的目标文件与静态库合并,并生成的可执行文件。这样,App 自身代码生成的目标文件与静态库都被拷贝到可执行文件中,从而静态库也成为了 App 可执行文件的一部分。这样的库呢,我们称之为 静态库,也称为 static archive libraries, 或 static linked shared libraries.

Link Static Libraries

存在形式

静态库主要以 .a.lib 的形式存在,在苹果生态系统中,还可以是 .framework.xcframework

特点

  • App 启动的时候就全部载入内存空间,所以在 App 运行过程中,需要使用依赖库的时候不需要额外从外部加载,速度快,但也增加了 App 的启动时间。

  • App 的可执行文件变大,占用内存也会相应增多。因为App依赖的所有静态库都会被静态链接器链接并拷贝到将要生成的 app 可执行文件中。

  • 当静态库需要修改时,必须得重新编译和发布静态库,所以不便于维护。

动态库(Dynamic Library)

动态库又 dynamic shared libraries, shared objects, or dynamically linked libraries。我们以 OS X 为例,当 App 启动时,操作系统内核会将 App 代码和数据载入新进程(也就是操作系统为 App 创建的新进程)的地址空间。与此同时呢,操作系统内核也会把动态加载器(Dynamic Loader) 载入进程,由动态加载器来完成加载 App 依赖的动态库。不过在启动阶段,动态加载器只会根据静态链接器中记录的 App 已链接的依赖库的名字,然后使用依赖库的 install name 来查找它们是否在文件系统中存在。如果不存在或不兼容,App 启动过程会中断。动态库被完全载入内存,是在代码里使用它的时候。所以相对静态库来说,使用动态库链接的 App 启动过程会更快。

Dynamic Link

存在形式

动态库主要以 .dylib.sodll 的形式存在,在苹果生态系统中,还可以是 .framework.xcframework

iOS App 的动态库存放在 .app bundle 下的 Frameworks 文件夹。

特点

  • App 按需装载,可以加速 App 的启动。

  • 动态库不会被拷贝到 App 的可执行文件中,所以可以动态按需加载。

  • 动态库的维护和更新很方便,只要 APIs 不变,依赖动态库的 App 就不用重新编译 。因为动态库并不是 App 可执行文件的一部分,是独立的,可动态加载的。

Apple FrameWorks

Framework

Framework 可以通俗的理解为封装了共享资源的具有层次结构的文件夹。共享资源可以是 nib文件、国际化字符串文件、头文件、库文件等等。它同时也是个 Bundle,里面的内容可以通过 Bundle 相关 API 来访问。Framework 可以是 static frameworkdynamic framework 在 iOS App 打包完成后,如果 Framework 包含了模拟器指令集(x86_64 或 i386),那么用 Xcode 发布 App 的时候,会报 unsupported architectures 的错误,所以需要我们手动或脚本去移除。

XCFramework

XCFramework 是由 Xcode 创建的一个可分发的二进制包,它包含了 framework 或 library 的一个或多个变体,因此可以在多个平台(iOS、macOS、tvOS、watchOS) 上使用,包括模拟器。XCFramework 可以是静态的,也可以是动态的。xcframework 的好处就是用 Xcode 发布的时候,Xcode 会自动选用正确的指令集 Frameworks,省去了手动移除动态库中的模拟器指令集的工作。不过值得注意的是,Xcode 11 才引入 XCFramework 。

制作 Frameworks

关于如何用 Xcode 一步一步创建 Framework 工程的话,我就不多说了,网上一大把教程,您也可以参考 Building Cross Platform Universal FrameworksSwift Cross Platform Framework。我重点讲如何用脚本来制作各种类型的 frameworks。为什么要介绍脚本呢,网上不是很多脚本制作 frameworks 吗? 刚开始我也是直接用网上的脚本,可总会有这样那样的问题,各种错误,所以决定在参考大神们文章的同时,自己重新整理下。

到这里,我假设您的 framework 代码都已经写好了,打包的 Aggregation Target 也创建好了。接下来,我将直接讲用脚本来制作 frameworks 。至于是制作 static framework 还是 dynamic framework 可以在 framework targetBuild Settings 中的 Mach-O Type 选择 framework 的类型,一般选用 Dynamic Library 或者 Static Library 就行。

开发环境

本文中使用的开发环境为:

  • macOS Catalina 10.15.4
  • Xcode 11.5

.xcarchive 目录结构

在制作 universal frameworkxcframework,我们都会用到 .xcarchive 包,所以我们先来看下它的目录结构。

xcarchive contents

制作 Universal Framework 脚本

你可以在这里直接获取制作 universal framwork 的完整脚本

编译单个平台的函数

# 制作完 framework 后,是否在 Finder 中打开
REVEAL_FRAMEWORK_IN_FINDER=true
# Framework 的名字
FREAMEWORK_NAME="${PROJECT_NAME}"
# 制作好的 framework 会输出到这个文件夹下面
FREAMEWORK_OUTPUT_DIR="${PROJECT_DIR}/Distribution"
# Device Archive 生成的 .xcarchive 存放路径。在工程的根目录下生成 Build 文件夹。
ARCHIVE_PATH_IOS_DEVICE="./Build/ios_device.xcarchive"
# Simulator Archive 生成的 .xcarchive 存放路径。
ARCHIVE_PATH_IOS_SIMULATOR="./Build/ios_simulator.xcarchive"

# 我们可以编译更多平台的 xcarchive
# ARCHIVE_PATH_MACOS="./build/macos.xcarchive"

# 生成单个平台的 .xcarchive. 接收4个参数, scheme, destination, archivePath,指令集.
# xcpretty 可以删除,这里用来使 Xcode 输出的日志更加人性化。
function archiveOnePlatform {
echo "▸ Starts archiving the scheme: ${1} for destination: ${2};\n▸ Archive path: ${3}"

xcodebuild archive \
-scheme "${1}" \
-destination "${2}" \
-archivePath "${3}" \
VALID_ARCHS="${4}" \
SKIP_INSTALL=NO \
BUILD_LIBRARY_FOR_DISTRIBUTION=YES | xcpretty
# BUILD_LIBRARY_FOR_DISTRIBUTION=YES

# sudo gem install -n /usr/local/bin xcpretty
# xcpretty makes xcode compile information much more readable.
}

编译所有平台的函数

以下方法可以编译并生成 iOS device, simulator 两个平台的 .xcarchive,此方法接收一个参数:scheme, 即 对应 app targetscheme。通常情况下,schemeframework name 是相同的。

function archiveAllPlatforms {

# https://www.mokacoding.com/blog/xcodebuild-destination-options/

# Platform Destination
# iOS generic/platform=iOS
# iOS Simulator generic/platform=iOS Simulator
# iPadOS generic/platform=iPadOS
# iPadOS Simulator generic/platform=iPadOS Simulator
# macOS generic/platform=macOS
# tvOS generic/platform=tvOS
# watchOS generic/platform=watchOS
# watchOS Simulator generic/platform=watchOS Simulator
# carPlayOS generic/platform=carPlayOS
# carPlayOS Simulator generic/platform=carPlayOS Simulator

SCHEME=${1}

archiveOnePlatform $SCHEME "generic/platform=iOS Simulator" ${ARCHIVE_PATH_IOS_SIMULATOR} "x86_64"
archiveOnePlatform $SCHEME "generic/platform=iOS" ${ARCHIVE_PATH_IOS_DEVICE} "armv7 arm64"
# archiveOnePlatform $SCHEME "generic/platform=macOS" ${ARCHIVE_PATH_MACOS}
}

这个方法执行完后,在本例中会得到以下 Build 文件夹内的内容:

Generated xcarchives

生成 Universal Framework的函数

function makeUniversalFramework {
# xcarchive 包中的 Frameworks 目录相对路径
FRAMEWORK_RELATIVE_PATH="Products/Library/Frameworks"

# 接下来的三个路径分别是模拟器平台的framework路径,真机平台的framework路径,以及输出的universal framework路径
SIMULATOR_FRAMEWORK="${ARCHIVE_PATH_IOS_SIMULATOR}/${FRAMEWORK_RELATIVE_PATH}/${FREAMEWORK_NAME}.framework"
DEVICE_FRAMEWORK="${ARCHIVE_PATH_IOS_DEVICE}/${FRAMEWORK_RELATIVE_PATH}/${FREAMEWORK_NAME}.framework"
OUTPUT_FRAMEWORK="${FREAMEWORK_OUTPUT_DIR}/${FREAMEWORK_NAME}.framework"

mkdir -p "${OUTPUT_FRAMEWORK}"

# Copy all the contents of iphoneos framework to output framework dir.
cp -rf "${DEVICE_FRAMEWORK}/." "${OUTPUT_FRAMEWORK}"

lipo "${SIMULATOR_FRAMEWORK}/${FREAMEWORK_NAME}" "${DEVICE_FRAMEWORK}/${FREAMEWORK_NAME}" \
-create -output "${OUTPUT_FRAMEWORK}/${FREAMEWORK_NAME}"

# For Swift framework, Swiftmodule needs to be copied in the universal framework
if [ -d "${SIMULATOR_FRAMEWORK}/Modules/${FRAMEWORK_NAME}.swiftmodule/" ]; then
cp -f "${SIMULATOR_FRAMEWORK}/Modules/${FRAMEWORK_NAME}.swiftmodule/*" "${OUTPUT_FRAMEWORK}/Modules/${FRAMEWORK_NAME}.swiftmodule/" | echo
fi
if [ -d "${DEVICE_FRAMEWORK}/Modules/${FRAMEWORK_NAME}.swiftmodule/" ]; then
cp -f "${DEVICE_FRAMEWORK}/Modules/${FRAMEWORK_NAME}.swiftmodule/*" "${OUTPUT_FRAMEWORK}/Modules/${FRAMEWORK_NAME}.swiftmodule/" | echo
fi
}

开始制作

echo "#####################"
echo "▸ Cleaning Framework output dir: ${FREAMEWORK_OUTPUT_DIR}"
rm -rf "$FREAMEWORK_OUTPUT_DIR"

echo "▸ Archive framework: ${FREAMEWORK_NAME}"
archiveAllPlatforms "$FREAMEWORK_NAME"

echo "▸ Make universal framework: ${FREAMEWORK_NAME}.framework"
makeUniversalFramework

# Clean Build
rm -rf "./Build"

if [ ${REVEAL_FRAMEWORK_IN_FINDER} = true ]; then
open "${FREAMEWORK_OUTPUT_DIR}/"
fi

去除动态库中的模拟器指令集

正如之前提到的,App 打包过程中,需要将 App 依赖的动态库中的模拟器指令集去除,这里是完整的脚本, 如何使用,请点 Stripping unwanted architectures from dynamic libraries。在 app targetBuild Phase 下新建 Run Script,并放到 Embed Frameworks 下面,然后将脚本复制进去就行。

#!/bin/sh

APP_PATH="${TARGET_BUILD_DIR}/${WRAPPER_NAME}"

echo $APP_PATH

# This script loops through the frameworks embedded in the application and
# removes unused architectures.
find "$APP_PATH" -name '*.framework' -type d | while read -r FRAMEWORK
do
FRAMEWORK_EXECUTABLE_NAME=$(defaults read "$FRAMEWORK/Info.plist" CFBundleExecutable)
FRAMEWORK_EXECUTABLE_PATH="$FRAMEWORK/$FRAMEWORK_EXECUTABLE_NAME"
echo "Executable is $FRAMEWORK_EXECUTABLE_PATH"

EXTRACTED_ARCHS=()

for ARCH in $ARCHS
do
echo "Extracting $ARCH from $FRAMEWORK_EXECUTABLE_NAME"
lipo -extract "$ARCH" "$FRAMEWORK_EXECUTABLE_PATH" -o "$FRAMEWORK_EXECUTABLE_PATH-$ARCH"
EXTRACTED_ARCHS+=("$FRAMEWORK_EXECUTABLE_PATH-$ARCH")
done

echo "Merging extracted architectures: ${ARCHS}"
lipo -o "$FRAMEWORK_EXECUTABLE_PATH-merged" -create "${EXTRACTED_ARCHS[@]}"
rm "${EXTRACTED_ARCHS[@]}"

echo "Replacing original executable with thinned version"
rm "$FRAMEWORK_EXECUTABLE_PATH"
mv "$FRAMEWORK_EXECUTABLE_PATH-merged" "$FRAMEWORK_EXECUTABLE_PATH"
done

制作 XCFramework 脚本

生成 Universal Framework的函数

你可以在这里直接获取制作 xcframwork 的完整脚本。编译单个平台和全部平台的函数跟制作 Framework 一样,所以这里直接列出制作 xcframework 的函数。制作 XCFramewrok 的时候,需要将 Build Settings 中的 Build Libraries for Distribution 设置为 YES。不过即使不设置,脚本也会将其设置为 YES。

function makeXCFramework {

FRAMEWORK_RELATIVE_PATH="Products/Library/Frameworks"
OUTPUT_DIR="${FREAMEWORK_OUTPUT_DIR}/DynamicFramework"

mkdir -p "${OUTPUT_DIR}"

xcodebuild -create-xcframework \
-framework "${ARCHIVE_PATH_IOS_DEVICE}/${FRAMEWORK_RELATIVE_PATH}/${FREAMEWORK_NAME}.framework" \
-framework "${ARCHIVE_PATH_IOS_SIMULATOR}/${FRAMEWORK_RELATIVE_PATH}/${FREAMEWORK_NAME}.framework" \
-output "${OUTPUT_DIR}/${FREAMEWORK_NAME}.xcframework"
}

开始制作

echo "#####################"
echo "▸ Cleaning XCFramework output dir: ${FREAMEWORK_OUTPUT_DIR}"
rm -rf $FREAMEWORK_OUTPUT_DIR

#### Make XCFramework

echo "▸ Archive framework: ${FREAMEWORK_NAME}"
archiveAllPlatforms $FREAMEWORK_NAME

echo "▸ Make framework: ${FREAMEWORK_NAME}.xcframework"
makeXCFramework

# Clean Build
rm -rf "./Build"

if [ ${REVEAL_XCFRAMEWORK_IN_FINDER} = true ]; then
open "${FREAMEWORK_OUTPUT_DIR}/"
fi

最后生成的 xcframework 长这个样子:

Generated xcarchives

小技巧

  • 查看 framework 包含的指令集

    lipo -info /path/to/xxx.framework/xxx
  • 查看 dynamic framework 是否支持 bitcode

    otool -arch armv7 -l /path/to/xxx.framework/xxx | grep __bundle

    如果包含 bitcode, 你会看到 sectname __bundle 信息。如果不包含,Terminal 输出为空。

  • 查看 static framework 是否支持 bitcode

    otool -arch armv7 -l /path/to/xxx.framework/xxx | grep __bitcode

    如果包含 bitcode, 你会看到 sectname __bitcode 信息。如果不包含,Terminal 输出为空。

参考

非常感谢以下文章的贡献者,使我对编译原理相关知识,以及对静态库、动态库、framework 有了更深刻的认识。

GCC

GCC 百科

GCC, the GNU Compiler Collection

C Pre-Processor

Compiler Design Overview

Apple Frameworks

Dynamic Library Programming Topics

Mach-O Programming Topics

Code Loading Programming Topics

Framework Programming Guide

Stripping Unwanted Architectures From Dynamic Libraries In Xcode

XcodeBuild Destination

Xcode Build Settings


如果您喜欢这篇文章,或觉得对您有用的话,欢迎发表评论,您也可以随意的分享或转载这篇文章。如果文章中的图片有侵犯您的版本,请联系作者将期删除。谢谢您的支持!

...

...

00:00
00:00