Xcode 打包实践

Xcode 打包实践

Posted by WTJ on September 10, 2022

目录

内容概览

  • 了解架构
  • xcodebuild 命令中打包相关的用法
  • ExportOptions.plist 详解

了解架构

芯片设计采用什么指令集架构,在其上运行的软件就要支持对应的指令集架构。常见的指令集架构有x86系列和arm系列。

x86 和 arm

x86 是复杂指令集架构,由Intel公司设计,主要应用在桌面计算机和服务器。有16 位(16-bit)版本、32 位(32-bit)版本、64 位(64-bit)版本等,早期的i386是 32 位的 x86 架构,显然x86_64就是 64 位的 x86 架构。 arm 是精简指令集架构,由Arm公司设计,主要用于移动设备,比 x86 架构更加省电。同样,arm 架构也有 32 位和 64 位之分。Arm 公司发布的第一代ARM1就已经是 32 位架构,后来的ARMv3、ARMv7依然是 32 位,直到 2011 年发布的ARMv8-A才开始支持 64 位。

armv7 和 arm64

armv7,即ARMv7-A架构,是 32 位 arm 架构之一,因此不能统称为 arm32。例如 Apple 最早的 A 系列芯片 A4 以及后面的 A5/A5X/A6/A6X 芯片均采用ARMv7-A架构。A4 芯片服务了 iPhone 4 和第一代 iPad,A6 服务了 iPhone 5/iPhone 5c,直到 A6X 服务完第四代 iPad,armv7 就结束了它的使命,从 iPhone 5s 搭载的 A7 芯片开始,都是 64 位 arm 的天下。

arm64 是 64 位 arm 架构的统称,即ARMv8-A及以后版本的 64 位系列架构,并非是某一个架构。例如 Apple 的 A7/A8/A8X/A9/A9X/A10/A10X 芯片采用ARMv8-A架构,A11芯片采用ARMv8.2-A架构,A12/A12X/A12Z 芯片采用ARMv8.3-A架构,而在最新的 iPhone 13 系列手机上搭载的 A15 芯片则采用ARMv8.5-A架构。M 系列芯片 M1/M1 Pro/M1 Max 均是支持 arm64 架构的芯片。

M1 和 Rosetta 2

这么多架构,我们并未针对每一种架构输出二进制,而是通常将 Build Settings 的ARCHS项设置为armv7 arm64来统一处理。而能够统一处理的前提,则是每一个版本的架构都兼容前一个版本的。ARCHS值默认由$(ARCHS_STANDARD) 决定,它由 Xcode 根据项目支持的平台和版本自动确定。例如在 M1 芯片的 Mac 上利用 Xcode 13.1 创建的 macOS 项目,$(ARCHS_STANDARD) 实际值是arm64,在 Intel 芯片的 Mac 上创建的 macOS 项目,其值是x86_64 arm64。而在 Intel 芯片的 Mac 上利用低版本的 Xcode 创建的 macOS 项目,其值是x86_64。

如果应用只支持 x86_64 架构,它是不能直接跑在 M1 芯片上的,那么 Mac AppStore 上海量的应用无法在 M1 设备上运行。缺失应用生态支持,直接导致 M1 设备几乎无人购买。而 M1 设备又是苹果打开自研芯片 Apple Silicon 市场的切入点,那么这条路注定很艰难。为了解决这个问题,Apple 创造了Rosetta 2工具,它支持 x86_64 应用运行在 arm64 架构的芯片上。这是一项伟大的技术,解决了前面的问题。但这种转换运行始终不可能和在 x86_64 架构的芯片上直接运行相媲美,最好还是开发者提供支持 arm64 架构的应用程序,这也是 Apple 大力呼吁开发者修改自己的应用程序以增加对 Apple Silicon 支持的原因。 现在不难理解为什么 iOS 应用可以在搭载 M1 的 MacBookPro 上运行了。因为芯片架构不同的鸿沟已经被填平,剩下软件层进行适配就相对容易了。

Universal binaries 和 Fat file

为了让一个应用程序在不同的架构上都可以运行,将支持不同架构的二进制打包在一起,就形成了通用二进制(Universal)文件,最终形成通用应用程序。作者使用的是 Intel 芯片的 Mac,导航到应用程序目录,在系统日历上右键显示简介,可以看到种类后标注有(通用),表明此应用是通用应用程序,可以同时在 Intel 芯片的 Mac 和 Apple Silicon 上运行。同样的方式查看Visual Studio,种类后标注的是(Intel),表明这个版本的 VS 只能在 Intel 芯片的 Mac 上运行。

ea314d4a06f34f7282763cdd4c99259b~tplv-k3u1fbpfcp-zoom-in-crop-mark-3024-0-0-0 image

通过lipo命令查看系统日历中的二进制文件支持的架构:

$ lipo -info /System/Applications/Calendar.app/Contents/MacOS/Calendar
Architectures in the fat file: /System/Applications/Calendar.app/Contents/MacOS/Calendar are: x86_64 arm64e 

同样的方式查看Visual Studio:

$ lipo -info /Applications/Visual Studio.app/Contents/MacOS/VisualStudio 
Non-fat file: /Applications/Visual Studio.app/Contents/MacOS/VisualStudio is architecture: x86_64

从上面两个命令的输出中可以看到,日历的二进制包含x86_64 arm64e两个架构,是胖文件(Fat file),而Visual Studio的二进制只有x86_64一个架构,不是胖文件(Non-fat file)。

xcodebuild 命令中打包相关的用法

打包配置

  • -configuration NAME
    指定 Build Settings 的变体名称,Xcode 工程默认创建了 Debug 和 Release 两个变体。例如指定 Release 变体,则写法为-configuration Release。

  • -xcconfig PATH
    指定 Build Settings 的配置文件,所有在 Xcode -> Build Settings 面板中的配置,都可以在配置文件中指定。配置文件优先级最高,会覆盖 Build Settings 面板中的配置和命令行单独传入的配置。

  • -arch ARCH
    针对指定的架构进行构建。例如-arch arm64,将只构建 arm64 架构的二进制。

  • -sdk SDK
    指定 Base SDK 的规范名称(Canonical Name)或完整路径。通过xcodebuild -showsdks -json查看所有可用的 SDK 的完整信息。 示例,指定 Base SDK 的完整路径:

$ xcodebuild \
-project App.xcodeproj \
-scheme App \
-destination generic/platform=iOS \
-sdk /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS15.0.sdk

或者指定规范名称:-sdk iphoneos15.0

账号授权

  • -allowProvisioningUpdates
    允许 xcodebuild 和 Apple Developer 后台交互。对于自动签名的项目,xcodebuild 可以自动创建或更新证书、AppID 和描述文件,对于手动签名的项目,xcodebuild 会下载缺失的描述文件或更新过期的描述文件。 有两种方式可以搭配该选项使用:
  • 在 Xcode 偏好设置面板中登录开发者账号,后续无论是从 Xcode 构建还是利用 xcodebuild 命令构建,都将获得 Apple Developer 后台交互权限。
  • 不登录开发者账号,通过 API 密钥进行交互。API 密钥由-authenticationKeyPath、-authenticationKeyID和-authenticationKeyIssuerID三者共同决定。
  • -allowProvisioningDeviceRegistration
    允许 xcodebuild 将当前设备注册到 Apple Developer 后台。依赖-allowProvisioningUpdates。

  • -authenticationKeyPath PATH
    指定 API 密钥文件的绝对路径,不能是相对路径。密钥文件是.p8格式。导航到 App Store Connect -> 用户和访问 -> 密钥 -> App Store Connect API, 创建一个密钥,点 击下载 API 密钥按钮进行下载。

注意,创建密钥需要指定权限,权限较低无法操作发布证书。另外,密钥文件只能下载一次,请妥善保管。如果新创建的密钥没有下载入口,刷新一下网页就会显示。

  • -authenticationKeyID KEY_ID
    密钥 ID。导航到 App Store Connect API,选择一个密钥,点击拷贝密钥 ID即可。

  • -authenticationKeyIssuerID ISSUER_ID
    创建认证令牌的发放者。导航到 App Store Connect API,Issuer ID下面的字符串即是。

bb5a29a972f846bc8ee160f4f23300c4~tplv-k3u1fbpfcp-zoom-in-crop-mark-3024-0-0-0 image

打包操作

  • -archivePath PATH
    指定.xcarchive文件的路径。

  • -exportPath PATH
    指定导出目录,目录中可能包含AppStoreInfo.plist,DistributionSummary.plist,ExportOptions.plist,manifest.plist,OnDemandResources文件夹,Packaging.log,安装包文件等。后续会针对这些文件一一说明。

  • -exportOptionsPlist PATH
    提供导出操作需要的参数,决定导出行为。后文会详细说明。

  • archive
    将 Xcode 项目导出为.xcarchive归档文件,由-archivePath指定自定义路径,不指定则默认导出到~/Library/Developer/Xcode/Archives/路径的当前日期目录下。

  • -exportArchive
    将归档文件导出为安装包或上传至 AppStore。必须依赖-archivePath和-exportOptionsPlist,对于导出操作,还需指定-exportPath。

  • -exportNotarizedApp
    将公证过的归档文件导出为安装包。
  • -create-xcframework
    从已经编译好的Framework或Library创建.xcframework文件。.xcframework文件可以同时包含多个平台和架构的二进制,并直接嵌入 Xcode 项目,Xcode 会根据当前运行环境自动抽取匹配的二进制参与构建,无需再写脚本剥离动态库中的模拟器架构。

ExportOptions.plist 详解

ExportOptions.plist 描述了导出操作,内部的配置项和手动在 Xcode 中导出需要勾选或输入的内容相同。由于是命令行操作,不允许和 Xcode 界面交互,只能将这些内容通过文件形式先确定下来。在终端中执行xcodebuild -h会在输出的最后一部分显示所有可用的键。

分发方式

method

指定分发方式,例如分发到 AppStore 或沙盒测试。值是 String 类型,所有可用值:app-store,validation,package,ad-hoc,enterprise,development,developer-id,mac-application。归档文件类型不同,可用值也会相应变化。

destination

将 App 上传到 App Store Connect 还是导出到本地,默认是导出到本地。值是 String 类型,所有可用值:export,upload。上传操作需要和 App Store Connect 交互,参见上文的账号授权部分。

generateAppStoreInformation

是否生成AppStoreInfo.plist文件。值是 Bool 类型,YES 或 NO,默认为 NO。在 Linux 和 Windows 中利用iTMSTransporter上传 App 时必须要指定该文件,而在 macOS 中上传时不需要。

Bitcode

uploadBitcode

是否上传 bitcode 并允许 AppStore 从 bitcode 重新编译 App,仅限 AppStore 导出操作。值是 Bool 类型,YES 或 NO,默认是 YES。需要在 Xcode 中启用Enable Bitcode,并且归档文件内确实携带 bitcode。

如果项目中使用了任何一个不包含 bitcode 的三方库,会导致最终的 App 也无法支持 bitcode,所以这个步骤常常被开发者所忽略,但对 Apple 而言却是非常重要的。当有新的硬件或软件变更时,需要 App 进行更新适配而不是兼容运行,就很依赖开发者了,直接导致 Apple 受限于开发者变得被动,很难推进自我更新。但是如果留有 App 的 bitcode,Apple 可以在后台默默重新编译 App 以适配最新的软硬件,这样就摆脱开发者的限制了。

compileBitcode

是否需要 Xcode 以和 AppStore 相同的方式从 bitcode 重新编译 App,只对非 AppStore 导出操作有效。需要在 Xcode 中启用Enable Bitcode,并且归档文件内确实携带 bitcode。值是 Bool 类型,YES 或 NO,默认是 YES。

On-Demand Resources

onDemandResourcesAssetPacksBaseURL

指定按需下载资源(On-Demand Resources,以下简称 ODR)所在的服务器地址,只对非 AppStore 导出操作有效。值是 String 类型,具体为资源所在的服务器域名和目录。xcodebuild 会将它写入AssetPackManifest.plist的URL部分。AssetPackManifest.plist 文件可以在导出成功的 App 的 Main Bundle 内找到。如果是 AppStore 导出,ODR 会被 AppStore 所托管,因此该值无效。

embedOnDemandResourcesAssetPacksInBundle

是否将 ODR 嵌入包内,只对非 AppStore 导出操作有效。值是 String 类型,true 或 false。默认是 true,当指定了onDemandResourcesAssetPacksBaseURL时,默认为 false。如果选择嵌入包内,则 App 无需从服务器下载 ODR 即可直接使用。当运行与 ODR 无关的测试或 ODR 托管服务器不可用时,这个选项非常有用。如果不嵌入,会额外导出 OnDemandResources 文件夹,包含所有 ODR 资源和一个 AssetPackManifest.plist 文件。

Over-the-air Installation

manifest

从浏览器安装 App(Over-the-air Installation)时需要的配置,只针对非 AppStore 导出操作。值是字典(Dictionary)类型,需要指定字典项的三个键:software-package(App 包地址),display-image(5757像素的展示图片),full-size-image(512512像素的原始图片)。如果使用了 ODR,还需指定 ODR 的 AssetPackManifest.plist 文件地址:asset-pack-manifest。启用该项,会额外导出manifest.plist文件。

签名&证书&描述文件

signingStyle

导出 App 时使用的签名方式,手动签名或自动签名。值是 String 类型,所有可用值:manual,automatic。如果使用自动签名并导出了归档文件,那么导出 App 可以是手动签名也可以是自动签名。如果使用手动签名导出归档文件,就只能使用手动签名方式导出 App,此时该值会被忽略。

provisioningProfiles

指定 App 使用的所有描述文件,仅用于手动签名导出操作。值是字典类型,字典项的键是 BundleID,值是描述文件名或其 UUID。如果 App 使用了一个应用扩展(Application Extension),则这个字典会有两项,一项是 App 的,另一项是应用扩展的。

应用扩展和主 App 一样需要在开发者后台创建 BundlelD、描述文件,并且应用扩展的 BundleID 要以主 App 的 BundleID 作为前缀。

signingCertificate

指定签名使用的证书,仅用于手动签名导出操作。值是 String 类型,可以是证书名称、SHA-1 值或自动选择器。证书名称、SHA-1 值都可以从系统的钥匙串访问中获取。自动选择器允许 Xcode 选择类型匹配且最新的证书,可用值有: Mac App Distribution,iOS Distribution,iOS Developer,Developer ID Application,Apple Distribution,Mac Developer,Apple Development。

installerSigningCertificate

指定对安装器进行签名使用的证书,仅用于手动签名导出操作。值是 String 类型,可以是证书名称、SHA-1 值或自动选择器。自动选择器的可用值有:Developer ID Installer,Mac Installer Distribution。仅对 macOS App 有效。

teamID

开发者账号的 TeamID。

App 瘦身

thinning

指定瘦身类型,只对非 AppStore 导出操作有效。值是 String 类型。可用值有:(不瘦身,仅导出一个通用 App),(针对支持的所有设备进行瘦身,导出一个通用 App 和所有瘦身 App),或者是特定设备的型号标识符(例如 iPhone7,1,仅导出适用于 iPhone7,1 的瘦身 App)。

App Store Connect 只接受 Universal App,因为它要利用 Universal App 导出支持所有设备的瘦身 App 并存储,当用户从 AppStore 下载时,AppStore 会根据用户的设备型号下载对应的瘦身 App。瘦身 App 只能在指定型号的设备上安装。 如果你想从 AppStore 下载某家公司的游戏并尝试修改,提供给特殊玩家使用前,最好留意一下是否适合这些玩家的设备。

d2c59554e03d4eba961e4064dac4bf2e~tplv-k3u1fbpfcp-zoom-in-crop-mark-3024-0-0-0 image

stripSwiftSymbols

移除 Swift 标准库中的符号,以减小包体积。值是 Bool 类型,YES 或 NO。目前我们打出的 Swift 包,会将 Swift 标准库直接嵌入到 App 的 Frameworks 目录内,其携带的各种符号如果不再需要,可以移除。

uploadSymbols

是否上传 App 的符号文件,仅限 AppStore 导出操作。值是 Bool 类型,YES 或NO,默认是 YES。将符号文件上传给 App Store Connect 后,用户设备上的崩溃日志会被符号化和可视化,可以直接在 Xcode -> Organizer -> Reports -> Crashes 中查看崩溃详情。

其他

iCloudContainerEnvironment

指定iCloud Container的环境,仅限启用了CloudKit的应用。值是 String 类型,可用值有:Development,Production。一般根据描述文件的类型自动确定,也可以特别指定。开发环境可以增、改、删 Container 中的数据,而生产环境只能增、改数据。

manageAppVersionAndBuildNumber

是否由 Xcode 来管理 App 的版本和构建版本。值是 Bool 类型,YES 或 NO,默认是 YES。这个操作会将 App 内所有内容的版本和构建版本都更改为主 App 的版本和构建版本。例如,App 的版本信息为 7.3.5(506745),App 项目依赖一个第三方动态库 FBSDKCoreKit.framework,版本信息为 1.0(9.0.0),又依赖一个自建的动态库项目 DebugConsole.xcodeproj,版本信息为 3.0.1(14),还包含一个应用扩展 NotificationService.appex,版本信息为 1.5.0(8),如果启用该选项,那么上述三个依赖项的版本信息都会被改为 7.3.5(506745),可以在打出的包内的Frameworks和PlugIns目录查看。请慎重考虑是否启用。 如果应用扩展的版本信息和主 App 不一致,在通过Transporter上传时会有警告。

distributionBundleIdentifier

以指定的 BundleID 重新格式化归档文件。据使用fastlane和bitrise的用户反馈,他们在导出带有App Clip的应用时会报错,添加该项即可解决。