如何配置应用的环境变量

一般的项目中多个环境的切换,都是依靠修改代码。但是这样的做法不够优雅,我们其实可以通过两种更好的方式去实现。

使用 Build Configuration 配置

系统默认有两个 Configuration,一个是 Debug,一个是 Release。如果要多环境切换的话就需要我们自己创建 Build Configuration。

新建 Build Configuration

在 Project 的 Configuration 中选择 Duplicate "Debug" Cofiguration,添加一个 Bebug 模式的 copy。将其改名为 OnlineDebug,之后它将被用于线上环境的 debug:

复制 Debug

如果使用 Cocoapods,那么添加新的 Configuration 后必须执行 pod install。pod 会为新的 Configuration 生成 xcconfig 文件:

生成xcconfig

添加了这个 Configuration 后,在 Build Setting 中就多了一个 OnlineDebug 的配置:

OnlineDebug

修改 Build Setting

创建好了新的 Configuration,我们要用它干什么呢?我们可以在 Build Setting 中根据不同的 Configuration 设置不同的参数。比如我们可以针对不同的 Configuration 设置 target 的 Build Settings(注意是 target 不是 project,为什么后面再说) 中的 Product Bundle Indentifer 以及 Product Name 。这样安装的时候就会按照配置更换 Bundle ID 和 名字:

设置 ID 和 Name

不仅如此,我们还可以自定义一些设置。这次我们回到 project 的 Build Setting 中去,点击 Add User-Defined Setting:

自定义设置

之后就会在 Build Setting 底部的 User-SETTING 一栏添加了一个 NEW_SETTING。举个例子,我们将其改名为 CustomProductName,然后修改各个 Configuration 对应的值:

这样我们就可以在其他地方使用这个 CustomProductName 了。

虽然我们是在 Project 中修改的,但是打开 target 的 Build Setting,可以看到,在其中也添加了一个 CustomProductName:

target

修改 info.plist 文件

info.plist 文件中保存着许多应用的配置信息:

plist

比如上面的 Bundle Name 对应的值 $(PRODUCT_NAME),就表示 Build Setting 中的 Product Name 这一项。其实可以直接在 Build Setting 中设置 Product Name 的,但是由于上面我们是自定义了一个 CustomProductName,所以我们这里将其改为 ${CustomProductName}(用小括号和大括号是一样的,参考)。运行一下发现 App 的名称确实和 CustomProductName 配置的一样。

其实 info.plist 也是可以在 Build Setting 中设置的,我们可以自己创建诸如 myInfo.plist,yourInfo.plist 等文件,只要设置好其索引就行:

配置 plist

新建 Scheme

现在我们如果要切换 Configuration,需要在 Edit Scheme 中切换 Build Configuration:

切换

这样每次 Run 都需要多点几下比较麻烦。既然在一个 scheme 中修改比较麻烦,那么我们为什么不新建多个 Scheme 呢?

我们选择 New Scheme 后,会弹出如下对话框,选择正确的需要运行的 Target,然后给新的 Scheme 取一个名字 TestConfigurationNew:

新建Scheme

新建号 Scheme 后,我们选择 manage scheme 查看:

manage scheme

看到有两个勾选项,一个叫 show,一个叫 shared。show 选项勾上后,就可以在上图的位置显示以及能被选择运行,否则就隐藏起来。shared 选项表示是否将 scheme 共享给别人。如下图所示,如果我们不为刚刚创建的 TestConfigurationNew 勾上,那么该 scheme 生成的 TestConfigurationNew.xcscheme 文件将会保存在 zachary.xcuserdatad 文件夹下,别人的 xcode 是无法识别的。但是如果你勾上了 share,那么就会像下面那样显示在 xcshareddata 文件夹下。这也告诉我们,在写 gitignore 的时候,可以忽略 xcuserdata 文件夹内的文件,但是一定不能忽略 xcshareddata 内的文件。

文件位置

现在我们新建了 TestConfigurationNew 这个 scheme 后,就可以为每个 scheme 设置单独的 Build Configuration。每次 Run 的时候,切换 scheme,而不是切换 scheme 中的 Configuration。

分环境的比较好的方式

上面的方式是新建一个 scheme,然后在 debug 模式下对应的 Configuration 是 OnlineDebug,但是其 Archieve 对应的还是 release。那么如果想要针对 OnlineDebug 的环境打一个 release 包,该怎么办呢?我们需要再 Deplicate release Configuration,然后将其命名为 OnlineRelease:

新建 onlineRelease

然后,将 TestConfigurationNew 的 archieve 中的 Build Configuration 改为 OnlineRelease:

也就是说,每一个环境都应该有其对应用 debug 以及 release 的 Configuration:

环境配置

配置和获取环境变量

说了半天添加 Configuration,那么到底和区分环境有什么关系呢?有两种方式可以处理。

使用预编译宏

我们进入 Project 的 Build Setting,其中的 Preprocessor Macros 可以添加一些环境变量的宏来做标识符,比如添加 ONLINEOFFLINE 标识符,来区分线上和线下环境:

配置环境

现在,你就可以这样设置环境了:

1
2
3
4
5
6
7
8
9
10
#ifdef ONLINE
#define searchURL @"http://www.baidu.com"
#define sociaURL @"weibo.com"
#elif OFFLINE
#define searchURL @"http://www.bing.com"
#define sociaURL @"twitter.com"
#else
#define searchURL @"http://www.google.com"
#define sociaURL @"facebook.com"
#endif

使用 plist 文件动态配置

除了使用宏,还可以读取 plist 里的配置信息。我们可以为每一个 Configuration 提供一个 plist 文件,将配置信息分别写入。由于一个文件夹内不能有同名的文件,所以我们为每一个 Configuration 的 plist 都创建一个文件夹:

多个plist

然后在 target 的 build phase 中添加 New Run Script Phase,将其命名为 Copy Configuration:

添加 script

在其中添加如下 shell 脚本:

1
2
3
4
5
6
7
echo "CONFIGURATION -> ${CONFIGURATION}"
RESOURCE_PATH=${SRCROOT}/${PRODUCT_NAME}/config/${CONFIGURATION}

BUILD_APP_DIR=${BUILT_PRODUCTS_DIR}/${PRODUCT_NAME}.app

echo "Copying all files under ${RESOURCE_PATH} to ${BUILD_APP_DIR}"
cp -v "${RESOURCE_PATH}/"* "${BUILD_APP_DIR}/"

这个脚本的目的就是将根据 Configuration 复制不同 config 文件夹下的文件,到 xxx.app 的根目录下,也就是 mainBundle 下。

读取配置文件的时候,可以根据文件路径获取:

1
2
3
4
5
6
- (NSString *) readValueFromConfigurationFile {
NSBundle *bundle = [NSBundle mainBundle];
NSString *path = [bundle pathForResource:@"Configuration" ofType:@"plist"];
NSDictionary *config = [NSDictionary dictionaryWithContentsOfFile:path];
return config[@"serverURL"];
}

除了 plist,我们也可以用这种方法配置不同的文件。

上面提到的几个概念

上面一会儿修改 project 的 Build Setting,一会儿修改 target 的 Build Setting。一会儿添加 Configuration,一会儿又添加 Scheme 的。那么这些概念到底是什么意思呢?

Scheme,Configuration 以及 Target

这三者的关系,用 Edit Scheme 就可以表现的一目了然:

可以看到,Scheme,就是同意调配 Configuration 以及 Target 的,也就是让哪个 Target 对应于 哪个 Configuration。

.xcconfig 和 Configuration

我们可以看一下一个非官方的 xccofig 的教程(这个教程对于 xcconfig 介绍的非常好)中的一句话:

A xcconfig file is used as a supplemental file to a specific build configuration. A build configuration can have an associated xcconfig file, this allows for additional changes to the target’s build settings from outside the Xcode project editor.

意思是一个 Configuration 可以有一个与其相关联的 xcconfig。xcconfig 可以使我们能够不在 Xcode 的提供的编辑器中编辑 Build Setting

对于一个没有使用 Cocoapods 的项目,其 Configuration 下 Based on Configuration File 都是 None,因为我们直接修改 Build Setting 就可以了:

但是使用了 Cocoapods 的项目,在 pod install 后,会自动为每一个 Configuration 生成一个 xcconfig。因为 Pod 不能直接修改你的 Build Setting,所以只能借助于 xcconfig:

我们来看一下 Pod 生成的一个 xcconfig:

首先看第一句 GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1GCC_PREPROCESSOR_DEFINITIONS 是 GCC 预编译头参数,也就是我们之前设置过的 Preprocessor Macros$(inherited) 表示继承于之前的设置。我们打开 target 的 Build Setting,找到这一项:

可以看到,这一项和 xcconfig 中设置的一样。因此,我们可以猜想:在 project 的 Configuration 这一项中,将 target 和xcconfig 关联了起来,xcconfig 中的每一项,如果 target 的 Build Setting 中原来不存在,那么就是新建的 User-Defined,如果 target 的 Build Setting 中存在,那么就是重写。上面提及的非官方教程中也有详细说明。

比如上面 xcconfig 中提及的 PODS_ROOT 一项是 Build Setting 中原本没有的,那我们就到 target 的 User-Defined 中去查看,确实被新建了出来。

再比如,我们修改 xcconfig 中的 GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1 hello=1,增加了一个 hello=1。我们来看 Build Setting,确实增加了 hello 这一项。再次验证了猜想:

每一个 target 都可以设置一个相关的 xcconfig,来配置其 Build Setting,没有就是 None,Cocoapods 就是基于这个原理。我们也可以自己创建 xcconfig 来配置:

这里盗用了一张别人的图,他为每一个 Configuration 都创建了一个 xcconfig,并将其与 target 关联:

然后在其中添加自己的设置。注意,如果使用了 pod 要在其中引入 pod 的 xcconfig:

所以,对于我们来说,自定义 xcconfig 其实完全等价于直接处理 Build Setting。因此,这也是一种修改环境变量的方式。

Target 以及 Project 的 Build Setting

之前我们时而设置 Target 的 Build Setting,时而设置 Project 的 Build Setting,那么到底两者有什么区别呢。其实之前说的那篇教程中也有说明。

简单来说,Build Setting 有一个继承关系,我们可以通过下面一张图了解:

我们在 Build Setting 中选择 Levels,这样整个继承关系就平铺了下来。继承级别从左向右依次降低。最右边的是 iOS Default,提供一个默认的设置,我们可以在 Project 的 Build Setting 中修改它。然后是 target 的 xcconfig,这里就是 pod 生成的 xcconfig,它可以修改 Build Setting 的选项。最后是 target 的 Build Setting,修改级别最高。最终的结果就是 Resolved 一栏显示的。

这也就解释了之前为什么时而用 Project 的 Build Setting,时而用 Target 的 Build Setting。当你想要修改的东西要作用于所有 target 的时候,就在 Project 中修改。如果只是想针对某一个 target,那么就修改 target 的 Build Setting。

一个小问题

这里在看到一个关于新建 Configuration 会产生的一个问题:新建的 Configuration 在默认情况下无法调试:

bug

可见官方文档

使用 Target 配置

在创建了项目后,项目本身包含一个 target,我们可以以这个 target 为基础,创建出测试环境,线上环境的 target。

我们先 Duplicate 一个 target:

将会生成一个 TestConfiguration copy,以及一个 TestConfiguration copy-Info.plist 文件:

我们将其名字修改为 TestConfiguration_Test 以及 TestConfiguration_Test-Info.plist。改了 plist 的名称后,对应的,在 target 的 Build Setting 中 Info.plist File 也要改成对应的名称:

除此之外,新建了 target 后,还会自动创建一个 scheme,我们同样要修改 scheme 的名字。记得要勾上 shared:

之后配置和获取环境变量的方式和之前的 Configuration 一模一样。两者的不同在于 Configuration 修改的是同一个 Build Setting;复制 Target,则相当于创建了一个 Build Setting 的副本。

如果维护一套代码,以后这些app如果需求有不同怎么办??比如要进入不同界面,跳转不同界面,页面也显示不同怎么办??这个问题其实很简单。在Targets里面的Compile Sources里面是可以给每个不同的Targets添加不同的编译代码的。只需要在每个不同的Targets里面加入不同界面的代码进行编译就可以了,在跳转的那个界面加上宏,来控制不同的app跳转到相应界面。这样本地还是维护的一套代码,只不过每个Targets编译的代码就是这套代码的子集了。这样维护起来还是很方便。也实现了不同app不同界面,不同需求了。

如何选择

那么什么时候选择 Configuration 什么时候选择 Target 呢?如果只上架一个 app,但是需要多个环境,并且不同环境的功能基本相同,那么用 Configuration 就可以了。如果要上架多个类似的 app,那么一定要用 target,因为不同 app 所需的证书不同,archive的时候还会有各种问题。