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-storeexport_method参数,表示要生成上传到 App Store 的 App。

有了archive_internalarchive_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