如何搭建多环境支持,为 App CI作准备

前言

在开始之前,先问几个问题,在测试的时候,App 一般需要连接测试服务器,那么在上架后,还需要连生产服务器吗?在发布前,App 需要通过 Ad-hoc 分发给内部测试组吗?在发布到 App Store 的时候,App 需要同时支持免费版和收费版吗?

如果回答是“是”,那么App 就需要搭建多环境支持,优化开发的工作流程。多环境提供很多好处,比如能基于同一套源代码自动构建出有差异功能的 App;能支持多个团队并行开发,也能分离测试和生产环境,提高产品的迭代速度,保证上架的 App 通过严格测试和功能验证。

在 App 项目中,一般使用了三个不同的环境,分别是开发环境,测试环境和生产环境。它们到底有什么区别呢?

  • 开发环境,用于日常的开发,一般有未完成的功能模块。编译时,也不进行任何优化,可以打印更多的日志,帮助开发者快速定位问题。
  • 测试环境,主要是用于测试,以及为产品经理进行功能验证,包括部分完成的功能模块,也提供一些隐藏功能,方便进行开发和迭代,例如快速切换用户,清理 Cache,连接到不同后台服务器等等。
  • 生产环境,只包含通过了测试并验证过的功能模块,它是最终提交到 App Store 供终端用户使用的版本。

多环境支持需要用到 Xcode 的构建配置,下面会一一阐述。

Xcode 构建基础概念

一般在构建一个 iOS App 的时候,需要用到 Xcode Project,Xcode Target,Build Settings,Build Configuration 和 Xcode Scheme 等构建配置。它们各有什么用呢?

Xcode Project

Xcode Project用于组织源代码文件和资源文件。一个 Project 可以包含多个 Target,例如当我们新建一个 Xcode Project 的时候,它会自动生成 App 的主 Target,Unit Test Target 和 UI Test Target。

例如在 Moments App 项目中,主 Target 就是 Moments,Unit Test Target 是 MomentsTests, UI Test Target 就是 MomentsUITests。

Xcode Target

Xcode Target用来定义如何构建出一个产品(例如 App, Extension 或者 Framework),Target 可以指定需要编译的源代码文件和需要打包的资源文件,以及构建过程中的步骤。

例如在 Moments App 项目中,负责单元测试的MomentsTestsTarget 就指定了 14 个测试文件需要构建(见下图的 Compile Sources),并且该 Target 依赖了主 App TargetMoments(见下图的 Dependencies)。


有了 Target 的定义,构建系统就可以读取相关的源代码文件进行编译,然后把相关的资源文件进行打包,并严格按照 Target 所指定的设置和步骤执行。那么 Target 所指定的设置哪里来的呢?来自 Build Settings。

Build Settings

Build Setting保存了构建过程中需要用到的信息,它以一个变量的形式而存在,例如所支持的设备平台,或者支持操作系统的最低版本等。

通常,一条 Build Setting 信息由两部分组成:名字和值。比如下面是一条 Setting 信息,iOS Development Target是名字,而iOS 14.0是值。

有了这些基础以后,接下来就结合 Moments App 来介绍如何进行多环境配置,从而生成不同环境版本的 App。

Moments App 构建配置

一般用 Xcode 编译出不同环境版本的 App 有多种办法,例如拷贝复制所有源代码,建立多个 Target 来包含不同的源码文件等等。不过,推荐使用 Build Configuration 和 Xcode Scheme 来管理多环境,进而构建出不同环境版本的 App。为什么?因为这两个是目前管理成本最低的办法。一一介绍下。

Build Configuration

当在 Xcode 上新建一个项目的时候,Xcode 会自动生成两个 Configuration:Debug和Release。Debug 用于日常的本地开发,Release 用于构建和分发 App。在 Moments App 项目中,有三个 configuration:Debug,Internal 和 AppStore。它们分别用于构建开发环境、测试环境和生产环境。 其中 Internal 和 AppStore 是从自动生成的 Release 拷贝而来的。

那什么是 Build Configuration 呢?

Build Configuration就是一组 Build Setting。 我们可以通过 Build Configuration 来分组和管理不同组合的 Build Setting 集合,然后传递给 Xcode 构建系统进行编译。

有了 Build Configuration 以后,我们就能为同一个 Build Setting 设置不同的值。例如Build Active Architecture Only在 Debug configuration 是Yes,而在 Internal 和 AppStore configuration 则是No。这样就能做到同一份源代码通过使用不同的 Build Configuration 来构建出功能不一样的 App 了。

那么,在构建过程中怎样才能选择不同的 Build Configuration 呢?答案是使用 Xcode Scheme。

Xcode Scheme

Xcode Scheme用于定义一个完整的构建过程,其包括指定哪些 Target 需要进行构建,构建过程中使用了哪个 Build Configuration ,以及需要执行哪些测试案例等等。在项目新建的时候只有一个 Scheme,但可以为同一个项目建立多个 Scheme。不过这么多 Scheme 中,同一时刻只能有一个 Scheme 生效。

下图是 Moments App 项目的 Scheme 。 Moments App 项目有三个 Scheme 来分别代表三个环境,Moments Scheme 用于开发环境,Moments-Internal Scheme 用于测试环境,而 Moments-AppStore Scheme 用于生产环境。

下面是MomentsScheme 的配置。

左边是该 Scheme 的各个操作,如当前选择了 Build 操作;右边是对应该操作的配置,比如 Build 对应的 Scheme 可以构建三个不同的 Targets。不同的 Scheme 所构建的 Target 数量可以不一样,例如下面是Moments-InternalScheme 的配置。

该 Scheme 只构建主 App TargetMoments,而不能构建其他两个测试 Target。

当选择 Run、Test、Profile、 Analyze 和 Archive 等操作时,在右栏有一个很关键的配置是叫作 Build Configuration,可以通过下拉框来选择 Moments App 项目里面三个 Configuration (Debug,Internal 和 AppStore) 中的其中一个。

为了方便管理,通常的做法是,一个 Scheme 对应一个 Configuration。有了这三个 Scheme 以后,我们就可以很方便地构建出 Moments α(开发环境),Moments β(测试环境)和 Moments(生产环境)三个功能差异的 App。

这三个 App 的名字都不一样,怎么做到的呢?实际上是为不同的 Configuration 设置了不一样的 Build Setting。其中决定 App 名字的 Build Setting 叫作PRODUCT_BUNDLE_NAME,然后在 Info.plist 文件里面为 Bundle name 赋值,就能构建出名字不一样的 App。

QQ20210608-104945@2x.png

为了构建出不同环境版本的 App,需要经常为各个 Build Configuration 下的 Build Setting 设置不一样的值。 在这其中,使用好 xcconfig 配置文件就显得非常重要。

xcconfig 配置文件

xcconfig 会起到什么作用呢?

一般修改 Build Setting 的办法是在 Xcode 的 Build Settings 界面上进行。 例如下面的例子中修改 Suppress Warnings。

这样做有一些不好的地方,首先是手工修改很容易出错,例如有时候很难看出来修改的 Setting 到底是 Project 级别的还是 Target 级别的。其次,最关键的是每次修改完毕以后都会修改了 xcodeproj 项目文档 (如下图所示),导致 Git 历史很难查看和对比。

所以Xcode 提供了一个统一管理这些 Build Setting 的便利方法,那就是使用 xcconfig 配置文件来管理。

xcconfig 概念及其作用

xcconfig也叫作 Build configuration file(构建配置文件),可以使用它来为 Project 或 Target 定义一组 Build Setting。由于它是一个纯文本文件,可以使用 Xcode 以外的其他文本编辑器来修改,而且可以保存到 Git 进行统一管理。 这样远比在 Xcode 的 Build Settings 界面上手工修改要方便很多,而且还不容易出错。

在 xcconfig 文件里面的每一条 Setting 都是下面的格式:

BUILD_SETTING_NAME = value

其中,BUILD_SETTING_NAME表示 Build Setting 的名字,而value是该 Setting 的值。下面是一个例子。

SWIFT_VERSION = 5.0

SWIFT_VERSION是用于定义 Swift 语言版本的 Build Setting,其值是5.0。Setting 的名字都是由大写字母,数值和下划线组成。这种命名法我们一般成为蛇型命名法,例如SNAKE_CASE_NAME。

如何获取Build Setting 的键值呢,在build setting选项上command+c就可以直接复制获取,delete的话会恢复xcode初始默认值。

当使用 xcconfig 时,Xcode 构建系统会按照下面的优先级来计算出 Build Setting 的最后生效值:

  • Platform Defaults (平台默认值)
  • Xcode Project xcconfig File(Project 级别的 xcconfig 文件)
  • Xcode Project File Build Settings(Project 级别的手工配置的 Build Setting)
  • Target xcconfig File (Target 级别的 xcconfig 文件)
  • Target Build Settings(Target 级别的手工配置的 Build Setting)

Xcode 构建系统会按照上述列表从上而下读取 Build Setting,如果发现同样的 Setting ,就会把下面的 Setting 覆盖掉上面的,越往下优先级别越高。

例如在 Project 级别的 xcconfig 文件配置了SWIFT_VERSION = 5.0而在Target 级别的 xcconfig 文件配置了SWIFT_VERSION = 5.1,那么Target 级别的 Build Setting 会覆盖 Project 级别的SWIFT_VERSION设置,最终SWIFT_VERSION生效的值是5.1。

那么,要怎样做才能做到不覆盖原有的 Build Setting 呢?可以使用下面例子中的$(inherited)来实现。

BUILD_SETTING_NAME = $(inherited) additional value

可以保留原先的 Setting,然后把新的值添加到后面去。比如:

FRAMEWORK_SEARCH_PATHS = $(inherited) ./Moments/Pods

其中的FRAMEWORK_SEARCH_PATHS会保留原有的值,然后加上./Moments/Pods作为新值。
在配置 Build Setting 时,还可以引用其他已定义的 Build Setting。

例如下面的例子中,FRAMEWORK_SEARCH_PATHS使用了另外一个 Build Setting PROJECT_DIR。

FRAMEWORK_SEARCH_PATHS = $(inherited) $(PROJECT_DIR)

为了重用,可以通过#include引入其他 xcconfig 文件。

#include "path/to/OtherFile.xcconfig"

Moments App xcconfig 配置文件

下面是 Moments App 项目如何管理 xcconfig 配置文件。

把所有 xcconfig 文件分成三大类:Shared、 Project 和 Targets。

其中 Shared 文件夹用于保存分享到整个 App 的 Build Setting,例如 Swift 的版本号、App 所支持的 iOS 版本号等各种共享的基础信息。 下面是 SDKAndDeviceSupport.xcconfig 文件里面所包含的信息:

TARGETED_DEVICE_FAMILY = 1
IPHONEOS_DEPLOYMENT_TARGET = 14.0

TARGETED_DEVICE_FAMILY表示支持的设备,1表示 iPhone。而IPHONEOS_DEPLOYMENT_TARGET表示支持 iOS 的最低版本,我们的 Moments App 所支持的最低版本是 iOS 14.0。

Project 文件夹用于保存 Xcode Project 级别的 Build Setting,其中 BaseProject.xcconfig 会引入 Shared 文件夹下所有的 xcconfig 配置文件,如下所示:

#include "CompilerAndLanguage.xcconfig"
#include "SDKAndDeviceSupport.xcconfig"
#include "BaseConfigurations.xcconfig"

然后根据三个不同的环境分别建了三个xcconfig 配置文件,如下:

  • DebugProject.xcconfig 文件
#include "BaseProject.xcconfig"
SWIFT_ACTIVE_COMPILATION_CONDITIONS = $(inherited) DEBUG
  • InternalProject.xcconfig 文件
#include "BaseProject.xcconfig"
SWIFT_ACTIVE_COMPILATION_CONDITIONS = $(inherited) INTERNAL
  • AppStoreProject.xcconfig 文件
#include "BaseProject.xcconfig"
SWIFT_ACTIVE_COMPILATION_CONDITIONS = $(inherited) PRODUCTION

它们的共同点是都引入了用于共享的 BaseProject.xcconfig 文件,然后分别定义了 Swift 编译条件配置SWIFT_ACTIVE_COMPILATION_CONDITIONS。其中$(inherited)表示继承原有的配置,$(inherited)后面的DEBUG或者INTERNAL表示在原有配置的基础上后面添加了一个新条件。有了这些编译条件,就可以在代码中这样使用

#if DEBUG
    print("Debug Environment")
#endif

该段代码只在开发环境执行,因为只有开发环境的SWIFT_ACTIVE_COMPILATION_CONDITIONS才有DEBUG的定义。这样做能有效分离各个环境,保证同一份代码构建出对应不同环境的 App。

Targets 文件夹用于保存 Xcode Target 级别的 Build Setting,也是由一个 BaseTarget.xcconfig 文件来共享所有 Target 都需要使用的信息。

PRODUCT_BUNDLE_NAME = Moments

这里的PRODUCT_BUNDLE_NAME是 App 的名字。
下面是三个不同环境的 Target xcconfig 文件。

  • DebugTarget.xcconfig
#include "../Pods/Target Support Files/Pods-Moments/Pods-Moments.debug.xcconfig"
#include "BaseTarget.xcconfig"
PRODUCT_BUNDLE_NAME = $(inherited) α
PRODUCT_BUNDLE_IDENTIFIER = com.ibanimatable.moments.development
  • InternalTarget.xcconfig
#include "../Pods/Target Support Files/Pods-Moments/Pods-Moments.internal.xcconfig"
#include "BaseTarget.xcconfig"
PRODUCT_BUNDLE_NAME = $(inherited) β
PRODUCT_BUNDLE_IDENTIFIER = com.ibanimatable.moments.internal
  • AppStoreTarget.xcconfig
#include "../Pods/Target Support Files/Pods-Moments/Pods-Moments.appstore.xcconfig"
#include "BaseTarget.xcconfig"
PRODUCT_BUNDLE_NAME = $(inherited)
PRODUCT_BUNDLE_IDENTIFIER = com.ibanimatable.moments

它们都需要引入 CocoaPods 所生成的 xcconfig 和共享的 BaseTarget.xcconfig 文件,然后根据需要改写 App 的名字。例如DebugTarget 覆盖了PRODUCT_BUNDLE_NAME的值为Moments α*, 其所构建的 App 叫作Moments α。

一般在 App Store 上所有 App 的标识符都必须是唯一的。如果你的项目通过 Configuration 和 Scheme 来生成免费版和收费版的 App,那么,你必须在两个 Configuration 中分别为PRODUCT_BUNDLE_IDENTIFIER配置对应的标识符,例如com.company.free和com.company.paid。

在 Moments App 中,也为各个环境下的 App 使用了不同的标识符,以方便通过 CI 自动构建,并分发到内部测试组或者 App Store。同时,这也能为各个环境版本的 App 分离用户行为数据,方便统计分析。

一旦有了这些 xcconfig 配置文件,今后就可以在 Xcode 的 Project Info 页面里的 Configurations 上引用它们。

下面是所有 Configurations 所引用的 xcconfig 文件

在配置好所有 xcconfig 文件的引用以后,可以在 Build Settings 页面查看某个 Build Setting 的生效值。我们以IPHONEOS_DEPLOYMENT_TARGET为例,一起看看。

当选择All和Levels时,可以看到所有配置信息分成了不同的列。这些列分别代表前面的 Build Settng 优先级:

  • 平台默认值
  • Project 级别的 xcconfig 文件
  • Xcode 项目文件中的 Project 级别配置
  • Target 级别的 xcconfig 文件
  • Xcode 项目文件中的 Target 级别配置

Build Settng 的优先级是从左到右排序的。越是左边优先级就越高。例如,在 Project 级别的 xcconfig 文件里面定义了IPHONEOS_DEPLOYMENT_TARGET的值为14.0,那么Project 级别的 xcconfig 文件(Project Config File) 一列上就会显示iOS 14.0,它覆盖了系统的默认值 (iOS Default)iOS 14.2。这就是因为 Project 级别的 xcconfig 文件,它的优先级高于系统默认值,因此最后生效的值是iOS 14.0。

总结

在使用 xcconfig 配置时,需要注意以下两点:

  • 首先,我们必须把所有 Build Setting 都配置在 xcconfig 文件里面,并通过 Git 进行统一管理;

  • 其次,我们千万不要在 Xcode 的 Build Settings 页面修改任何 Setting,否则该配置会覆盖 xcconfig 文件里面的配置。如果你不小心修改了,可以通过点击delete键把页面设置的配置删掉。

  • 最后,如何获取Build Setting 的键值呢,在build setting选项上command+c就可以直接复制获取,delete的话 会恢复xcode初始默认值。

  • 最后,如何获取Build Setting 的键值呢,在build setting选项上command+c就可以直接复制获取,delete的话会恢复xcode初始默认值。

  • 最后,如何获取Build Setting 的键值呢,在build setting选项上command+c就可以直接复制获取,delete的话会恢复xcode初始默认值。

  • 重要的事情说三遍😄

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335