Skip to content

25自动化构建:解决大量重复性人力工作神器 ​

在上一讲我们讲述了如何使用 fastlane 来自动管理私钥、证书和 Provisioning Profile 文件,相信你已经体会到自动化的威力了。其实,我们可以自动化几乎所有的 iOS 任务,包括编译、检查代码风格、执行测试、打包和签名、发布到分发渠道、上传到 App Store、发送发布通知等。自动化是衡量一个团队成熟度的关键因素,也是推动项目工程化实践的基石,同时还有以下多个优点。

  • 自动化能提高 App 的质量。通过自动执行代码风格检查和单元测试,能保证合并到主分支的代码都符合团队的代码规范,并通过质量检测。

  • 自动化能保证工作流程的一致性。如果通过手工操作来打包和发布,一不小心就会丢三落四,例如忘记执行测试、使用错误的 Provisioning Profile 等。而自动化打包和发布就不会出现这样的问题,并且还能保证流程的一致性,因为所有操作和步骤都是由程序自动执行的,可以保证任何机器执行这些操作都能得到一模一样的结果。

  • 自动化能提高发布频率。由于手工操作需要耗费大量的人力和时间成本,所以最后很可能我们不得不为了节省时间而延长发布周期。但有了自动化以后,我们就不需要任何人手来参与打包和发布过程,能做到主分支上任何 commit 都可以自动打包和发布。同时,频繁发布也有利于产品的快速迭代。

  • 自动化能减低沟通成本。当测试人员和产品经理需要验证功能时,我们都可以随时自动打包和分发 App,这样能有效降低沟通成本,并提高团队的和谐性。

  • 自动化能方便知识的共享。因为自动化的脚本都是源代码,所以我们可以很方便地把自动化的配置代码共享到多个项目中,提高效率并降低维护成本。

  • 自动化能为持续集成打下基础。持续集成可以帮助我们把 GitHub 上的代码更新都自动打包和发布到 App Store。在持续集成过程中,CI 服务需要执行定义好的自动化脚本,可以说,没有自动化脚本就无法进行持续集成。

既然自动化这么重要,那实现自动化的难度很高吗?其实不然,因为我们可以使用 fastlane 来实现几乎所有的任务。接下来我们就以 Moments App 为例子看看如何使用 fastlane 开发自动化脚本吧。

在前面《05 | 自动化准备:如何使用 fastlane 管理自动化操作?》里面,我们讲过如何搭建统一的 fastlane 环境,并介绍了如何开发检查和格式化代码等操作。其实除此以外,我们还可以完成编译与执行测试、打包与签名,以及上传到发布渠道等操作,下面我们就分别讲述这些操作是如何实现的。

编译与执行测试 ​

首先,我们看看编译的具体实现,如下所示:

ruby
desc "Build development app"
lane :build_dev_app do
  puts("Build development app")
  gym(scheme: "Moments",
    workspace: "Moments.xcworkspace",
    export_method: "development",
    configuration: "Debug",
    xcargs: "-allowProvisioningUpdates")
end

我们调用了gymAction 来编译 Debug 版本的 App。这里需要注意:我们把development传递给export_method参数。在上一讲的思考题里面,我问大家为什么 Moments App 的 Debug Target 使用了 Automatically manage signing,其实是因为 Automatically manage signing 是由 Xcode 自动管理签名证书,开发者无须任何额外的配置就能在设备上进行 Debug 操作。在build_dev_appLane 里,我们把development传递给export_method,这样能让 fastlane 也使用 Automatically manage signing 来进行自动签名。

接着看一下如何执行测试,具体实现代码如下所示:

ruby
desc "Run unit tests"
lane :tests do
  puts("Run the tests")
  scan(
    scheme: "Moments", 
    output_directory: "./fastlane/dist", 
    output_types: "html", 
    buildlog_path: "./fastlane/dist")
end

我们可以调用scanAction 来执行测试,同时还可以指定 Log 和报告的路径,当测试失败时,可以打开报告进行查看。在上面的例子中,我们生成了格式为 HTML 的报告。假如你需要自动分析测试报告,可以生成 JSON 格式的报告,然后读取 JSON 文件里的内容进行分析。

打包与签名 ​

只有进行过打包和签名的 App,才能安装到用户的设备上。那到底为什么需要签名呢?我们可以通过下面的示意图来看看原因。

苹果公司为了给所有的 iOS 用户提供安全和一致的体验,便把所有的 App 都放在沙盒(Sandbox)里面运行,这样能保证 App 运行在一个受限和安全的空间里面。通常情况下,App 只能访问沙盒里面的文件系统。当 App 需要访问系统资源的时候,必须通过权限管理模块的授权。常用的系统资源包括以下几类:

  • 照相机、麦克风以及传感器等硬件;

  • 网络访问;

  • 联系人、日程表和邮件等系统 App 的数据;

  • 文件 App 里的文件;

  • 地理位置信息、推送通知、HealthKit 以及 HomeKit 等服务。

我们以获取地理位置信息作为例子来看看权限管理系统的运作方式。当 App 想要获得后台地理位置信息时,权限管理系统会检查 Info.plist 文件是否提供了描述信息,并检查用户是否同意,最后检查 Background Modes 的 Entitlement 是否允许 Location updates。如果这些都通过了,权限管理系统就允许 App 在后台访问地理位置信息。任何一项不通过,App 都无法在后台访问地理位置信息。

当 App 需要访问各种资源的时候,iOS 系统会询问 App 一些重要的问题来判断是否能通过权限检查。那谁能提供这些信息呢?答案是 Provisioning Profile。可以这么说,Provisioning Profile 能回答下面的几大"哲学"问题。

  • 你是谁? Provisioning Profile 具有 Team ID 等信息,iOS 能知道这个 App 的开发者是谁。

  • 你要干吗? Provisioning Profile 关联的 Entitlement 能告诉 iOS 系统该 App 需要访问哪些系统资源。

  • 你要去哪里? Provisioning Profile 里的设备列表能告诉 iOS 系统能否安装该 App。

  • 我能相信你吗? 这涉及签名(Code Sign)的概念,通过签名,就能证明你是这个 App 的签名主体,并能证明这个 App 里面没有经过非法更改。

你可能已经发现 Provisioning Profile 文件是打包在 App 里面的,那么我们能不能偷偷地替换了它,让系统给我们所有的权限呢?这时候签名就能发挥作用。通过签名以后,App 就带有一个封印(Code Seal),该封印能帮助 iOS 系统快速地检查 App 是否经过非法更改。

到这里,你已经知道为什么需要为 App 进行签名了。

假如我们通过手工的方式进行打包和签名,那会怎么操作呢?首先要把证书和 Provisioning Profile 文件从苹果开发者网站上下载下来,并需要手工安装和管理。接着可以通过两种方式来完成打包操作:要么使用 Xcode 的 Archive 菜单进行打包,然后再使用 Validate App 功能来签名;要么使用xcodebuild archive命令来生成 .xcarchive 文件,然后调用xcodebuild -exportArchive命令来生成 IPA 文件。这两种办法都需要大量的手工操作,并且还十分容易出错。幸运的是 fastlane 能帮我们简化这些繁重的操作,下面我们一起看看如何使用 fastlane 来完成打包和签名吧。

首先看一下如何打包 Internal App,具体实现如下:

ruby
desc 'Creates an archive of the Internal app for testing'
lane :archive_internal do
  unlock_keychain(
    path: "TemporaryKeychain-db",
    password: "TemporaryKeychainPassword")
  update_code_signing_settings(
    use_automatic_signing: false,
    path: "Moments/Moments.xcodeproj",
    code_sign_identity: "iPhone Distribution",
    bundle_identifier: "com.ibanimatable.moments.internal",
    profile_name: "match AdHoc com.ibanimatable.moments.internal")
  puts("Create an archive for Internal testing")
  gym(scheme: "Moments-Internal",
    workspace: "Moments.xcworkspace",
    export_method: "ad-hoc",
    xcargs: "-allowProvisioningUpdates")
  update_code_signing_settings(
    use_automatic_signing: true,
    path: "Moments/Moments.xcodeproj")
end

我们定义了archive_internalLane 来打包和签名 Moments App 的 Internal 版本,具体分成以下四步。

  • 第一步是解锁 Keychain。因为签名所需的证书信息保存在 Keychain 里面,所以我们需要解锁 Keychain 来让 fastlane 进行访问。

  • 第二步是更新签名信息。我们使用"iPhone Distribution"作为签名主体,并使用"match AdHoc com.ibanimatable.moments.internal"作为 Provisioning Profile,这表示我们使用了 Ad Hoc 的 Provisioning Profile 来分发该 App。

  • 第三步是核心操作,调用gymAction 来进行打包和签名。gym帮我们封装了xcodebuild的实现细节,我们只需要调用一个 Action 就能完成打包和签名的操作。这里需要注意,为了生成用于测试的 Internal App,我们需要把export_method参数赋值为ad-hoc,这样我们就能实现内部分发。

  • 第四步是恢复回自动签名。因为在开发环境中,我们使用的是自动签名。为了方便本地开发,在完成打包后,我们得把签名方式进行重置。

下面再看一下如何为 App Store 版本的 App 进行打包和签名。

ruby
desc 'Creates an archive of the Production app with Appstore distribution'
lane :archive_appstore do
  unlock_keychain(
    path: "TemporaryKeychain-db",
    password: "TemporaryKeychainPassword")
  update_code_signing_settings(
    use_automatic_signing: false,
    path: "Moments/Moments.xcodeproj",
    code_sign_identity: "iPhone Distribution",
    bundle_identifier: "com.ibanimatable.moments",
    profile_name: "match AppStore com.ibanimatable.moments")
  puts("Create an archive for AppStore submission")
  gym(scheme: "Moments-AppStore",
    workspace: "Moments.xcworkspace",
    export_method: "app-store",
    xcargs: "-allowProvisioningUpdates")
  update_code_signing_settings(
    use_automatic_signing: true,
    path: "Moments/Moments.xcodeproj")
end

archive_appstore的实现基本上与archive_internal一致。不同的地方是在archive_appstore里面,我们指定的 Provisioning Profile 是 "match AppStore com.ibanimatable.moments",而且在调用gymAction 时传递了app-store给export_method参数,表示要生成上传到 App Store 的 App。

有了archive_internal和archive_appstore以后,再结合上一讲介绍的download_profiles,我们就可以十分方便地自动化打包和签名 App 了。命令执行完毕以后,在项目文件夹里面会出现一个 Moments.ipa 文件。IPA 文件也叫作 iOS App Store Package,该文件是一个包含了 iOS App 的存档(archive)文件。 为了查看 IPA 文件里面的内容,我们可以把后缀名修改成 .zip 文件并进行解压,其内容如下图所示:

在图中有一个名为 embedded.mobileprovision 的 Provisioning Profile 文件,你可以打开该文件来查看相关内容,如下图所示:

在该 Provisioning Profile 中,你可以看到用于定义访问系统资源权限的 Entitlement 信息、证书信息以及用于安装的设备列表信息。有了这些信息,iOS 系统就能对 App 进行权限管理。

上传到发布渠道 ​

经过打包和签名生成 IPA 文件后,下一步是把 App 上传到各个发布渠道。为了方便内部测试人员和产品经理进行测试和验证新功能,我们把 Internal 版本的 App 上传到 Firebase 上。

Firebase 提供了一个免费 App 分发服务,我们在本模块的后半部分会详细讲述 Firebase 的各种服务。这里我们就先看一下如何使用 fastlane 自动把 App 上传到 Firebase 的 App 分发服务,具体代码如下:

ruby
desc 'Deploy the Internal app to Firebase Distribution'
lane :deploy_internal do
  firebase_app_distribution(
      app: "1:374168413412:ios:912d89b30767d8e5a038f1",
      ipa_path: "Moments.ipa",
      groups: "internal-testers",
      release_notes: "A new build for the Internal App",
      firebase_cli_token: ENV["FIREBASE_API_TOKEN"]
  )
end

fastlane 通过插件的方式为我们提供了firebase_app_distributionAction,用于把 App 上传到 Firebase 的 App 分发服务上。要使用这个 Action,我们需要执行bundle exec fastlane add_plugin fastlane-plugin-firebase_app_distribution命令来安装 Firebase App 分发的插件。同时,为了能完全自动化执行该操作,我们需要把正确的参数传递给它。其中,app参数接收 Firebase 的 App ID,我们可以在 Firebase 的网站上找到,如下图所示:

ipa_path参数接收的是 IPA 文件的路径。当我们执行完上面的archive_internal命令以后,根目录会生成一个名叫 Moments.ipa 的文件,我们把该文件名传递给ipa_path参数即可。

groups参数用于指定测试组。在 Firebase 网站上打开 App Distribution -> Testers and Groups 就可以看到测试组。在 Moments App 项目里,我们配置了一个名叫 internal-testers 的测试组,如下图所示:

我们也可以使用不同测试组来管理不同的测试者,例如把测试人员和产品经理放置在不同的分组下。在上面的例子中,当我们把 internal-testers 传递给groups参数来分发 App 时,该测试组下的所有用户都会接收到 App 的更新通知。

release_notes参数用于传递发布公告信息。我们可以把 Git 的历史信息传递给该参数,这样测试人员就能看到该版本更新了哪些内容。

firebase_cli_token参数是访问 Firebase 的 API token,我们需要下载 Firebase CLI 来生成这个 Token,执行的命令如下:

java
$> curl -sL https://firebase.tools | bash
$> firebase login:ci

第一行命令用于安装 Firebase CLI,第二行命令用于生成一个 API Token,在执行过程中,我们需要在浏览器上登录并授权,其执行效果如下图所示:

我们可以把这个 Token 信息也放到 local.keys 文件里面,这样就能通过环境变量FIREBASE_API_TOKEN来提供给 fastlane 使用了。

至此,我们已经完成了上传到 Firebase App 分发服务的所有配置。请注意,这些配置只需要执行一次,以后任何开发者或者 CI 都可以方便地执行deploy_internal命令来完成上传操作。当上传完毕后,我们可以在 Firebase 网站上看到各个版本,如下图所示:

同时,测试者也能在手机上看到最新的版本,如下图所示:

最后,我们看看如何上传到 App Store,具体实现如下:

ruby
desc 'Deploy the Production app to TestFlight and App Store'
lane :deploy_appstore do
  api_key = get_app_store_connect_api_key
  upload_to_app_store(
    api_key: api_key,
    app_identifier: "com.ibanimatable.moments",
    skip_metadata: true,
    skip_screenshots: true,
    precheck_include_in_app_purchases: false,
  )
end

我们可以调用upload_to_app_storeAction 来把生产版本的 IPA 上传到 App Store,这里面使用了我们上一讲提到的私有 Laneget_app_store_connect_api_key,我们把取出的 API Key 赋值给api_key参数,然后把 com.ibanimatable.moments 赋值给app_identifier。其实,fastlane 还能帮助我们截图并自动上传到 App Store,但是为了给终端用户提供更准确的截图和描述信息,我们在上传的过程忽略了这两步。

好了,到这里任何开发者或者 CI 都可以执行deploy_appstore命令把 App 上传到 App Store 了。

总结 ​

在这一讲中,我们主要讲解了如何使用 fastlane 来开发编译、执行测试、打包和签名,以及发布到 Firebase 和 App Store 等各个自动化操作。

我非常建议你投资一些时间来开发这些自动化操作,这是一件事半功倍的事情。自动化能帮我们节省大量的时间,方便共享知识并减少错误的发生。除此之外,自动化也是搭建 CI 的基础,没有这些自动化操作,就无法搭建 CI。

思考题

这一讲我留一个操作题:请使用 fastlane 开发一个自动截图的 Lane。

可以把你的答案写到留言区,或者提交一个 PR 哦。下一讲我将介绍如何搭建 CI 来实现无需人手的快速交付。

源码地址

Fastfile 文件地址:https://github.com/lagoueduCol/iOS-linyongjian/blob/main/fastlane/Fastfile#L211-L261