多环境参数的配置

一、设计目的

1、根据需求切换开发、测试、生产的环境,快速响应差异化;
2、统一团队配置,优化开发流程,实现环境配置的统一管理。

二、应用环境

1、Dev
日常业务或者组件开发,打印日志,查看页面元素、内存、定位问题等,最常使用环境;
2、Test
测试环境(预发布),测试及验证,包含隐藏功能,比如内部token等信息的查看、日志信息查看等;
3、Production
生产环境,预备发布到App Store的最终版本,此版本调试功能不可用。

三、项目结构体系

先看一下项目的结构配置示意图:

备注:
这是一张关于项目的具体配置体系的结构图,包括各个部分的关系。
其中的红线代表构建一个完整的app经过的流程步骤。

四、配置加载过程

编译加载过程流程图:

通过该加载流程我们可以知道进行动态处理”动手术”的地方就是红色区域标识的位置。

五、配置可选方案

明晰了项目配置的结构,那么首先这里Project是排除的,因为它并不与我们的环境设置挂钩。但是也需要先理解一下各个部分的定义:

Project:
组织源代码和资源文件,可包含多个Scheme,也可包含多个Target目标对象,包含整体项目资源和配置的集合体。

Target:
定义构建产品(App、Extension、Framework),指定需要编译的源代码和打包的资源文件和构建的过程步骤。
根据不同的环境设置多个target,意味着设置三个target,对应开发、测试、生产,配合设置不同宏定义参数进行内部设置的区分,比如三方库appId,网络请求头参数设置等。

演示图1:

这里打开可以看到对应三个target,也就是可以切换不同的target来确定构建目标,不过一般来说,不会在这里切换,会对应环境构建Scheme来进行切换。

Scheme:
定义完整构建过程,指定Target进行对应的构建流程,允许创建多个,但是同一时刻只能允许一个生效,整合目标configuration编译环境。

演示图2:

优点:
切换快速,一个target对应两套Build
Settings
(release/debug),如果代码变动不大,使用起来很方便.

缺点:
1.每个target需要构建不同的bundleId,签名证书重新生成,增加到3个
2.如果后台业务与bundleId有绑定,则所有的逻辑也需要变更修改(后台视频业务、IM服务)
3.代码同步问题,一份代码三个target都需要引用勾选,漏掉后果很严重,如果存在环境的影响,还需要维护环境宏变量

Configurations
先确定涉及到的定义:

Build Configuration:
可以把Build Settings当作是一个集体设置族参数,而BuildConfiguration则是用来管理和分组不同族BuildSettings的管理器,一般管理的BuildSettings族有Debug和Release两种,意思是这里有两套BuildSettings的配置集合,用于构建不同性质的App.

Build Settings:
保存构建需要的设置信息,例如系统版本、异常支持、架构支持、三方库路径、编译顺序等.

本身是配合xcconfig进行配置设置的Build
Configuration
,该方案是无xcconfig的设置,结果就是Build
Settings
里面需要对新增加的Configurations除了以前旧有的debug/release设置以外还需要提供修改支持。还有创建scheme打到多环境,每个Schem对应一个Configurations环境,形成一一对应的关系,便于直接通过切换Scheme变更环境,构建不同的步骤。

如果需要自定义参数配置,要么使用不同Configurations的环境定义宏,内部宏区分处理,同Duplicate方案

也可以User-Defined根据不同Configurations定义不同参数,同时在info.plist里面同key配置引入即可

示意图:

优点:
根据env环境配置单一的环境,切换就可完成
缺点:
1.完全一一对应env环境,不区分当前编译类型debug/release/test
2.完全一一对应env环境,具体编译的debug/release根据具体业务确定
3.纯粹手动BuildSettings配置,如果出现丢失或者删除,难以对比跟踪,带来编译错误也查找困难
4.不适合统一管理,成员各个修改,给配置合并带来问题

实例:

Configuration+xcconfig
xcconfig(Build Configuration File,构建配置文件)

纯文本文件,是作用于Project或者Target的BuildSettings,也就是说它可以代替Build
Settings
,由手工修改变成了”外界配置”,再按照上述定义来看,我们可以确定,我们需要的是一BuildSettings,那么也就是一组xcconfig文件,由于是文件,那么可以用git进行统一管理.里面的内容格式类似于:

BUILD_SETTING_NAME = VALUE
SWIFT_VERSION = 5.0

使之生效,则需要在BuildSettings最前面加上${inherited},表示不覆盖并且保留具有相同key值的设置.

xcconfig文件的修改实际上是修改BuildSettings中的参数,这里包含3种层次的类型文件:Project xcconfigFile、Target xcconfig File、Pod xcconfig File(一般会自动生成,需要进行include引用)

一般项目中是不包含Project xcconfig File、Target xcconfig
File
文件的,所以需要自己创建这里需要注意一下项目最终生效配置过程,特别是存在同名配置的情况下,这里有一个优先级的步骤。

优先级:

如果想继承保留,使用value时,在前面添加${inherited},后面跟着具体设置的值或者在后面添加${inherited},前面设置具体值

例如:
SWIFT_ACTIVE_COMPILATION_CONDITIONS = $(inherited) RELEASE
或者
SWIFT_ACTIVE_COMPILATION_CONDITIONS = RELEASE $(inherited)

使用示意图:


由此我们就完成了设置,剩下的就是补上xcconfig File文件的内容.

注意一下xcode项目里定义的BuildSettings参数,可以从project.pbxproj里面去找到,如图所示:

其中一部分对应:



所以这里如果我们使用同名key值,就可能会造成覆盖,可以根据自己的需求来处理。

优点:

1.在2方案的基础上逐级下沉细节配置,按照业务需求可非手动动态添加或者修改配置,可以代BuildSettings里面的设置,可以看作是2方案的加强版
2.将手动配置变成了动态注入配置,剥离环境影响,解耦合,可扩展性更强大
3.无需对项目scheme、target等”动手术”.
4.统一参数配置更灵活,直接文件写好即可,层次清晰

缺点:
1.需要了解xcconfig的配置语法和加载机制
2.维护成本,同一个设置多个文件,优先级区分处理

4、结论
1、通过以上三个方案的比较,Duplicate最先放弃,因为起码需要构建不同bundleId的设置和对证书请求、业务方面造成的影响范围太广,工程量巨大,性价比最低.
2、Configurations方案最直接简单,需要手动设置好参数,对新增的参数的设置是间接处理,但是不管是漏掉设置还是删除都会造成严重的后果,跟踪机制薄弱,不适合统一管理.
3、Configuration+xcconfig细化了配置流程,将新增参数独立解耦出来,而且由于是文件,可以通过git统一管理跟踪,而且就算BuildSettings漏掉或者删掉,也会有动态设置补充,灵活且稳定性最好.
所以,这里我们选择第3个方案,实现Configuration+xcconfig的结合处理.

六、整体设计

配置结构示意图:

七、配置变量

常用编译配置变量:

上面的参数可以从官网里面获取,具体地址为:
[官方网址:][https://xcodebuildsettings.com/]
注意:这里是官方配置参数字段说明,如果需要覆盖掉buildsetting的参数,那么可以使用同名key字段,同时删除xcode中 buildsetting的参数;

如果需要另外定义,则可以取名与系统定义不一致,如果需要让buildsetting使用,部分非选择项可以直接将自定义字段名填入buildsetting中,也可以生效,但不建议,因为这样做的后果是xcconfig中配置的参数名不能改动,不能删除,否则会影响编译配置效果。

目前初版参数对照表如下:

上述不是完整配置字段,但是变更环境后会造成上述参数变换的效果

具体变量设置:
公共变量xcconfig
由于project build setting与target buildsetting两者的设置基本一致,那么我们只是从业务层面去区分公共设置,这里常用的配置包括以下:

Project xcconfig
本项目梳理无,只包含一个基础xcconfig对公共变量xcconfig引入。

Target xcconfig File

八、应用参数

B端当前参数梳理:

应用参数存储:
1.确认当前参数是否保密参数,如果是非保密,则放在${configuration}Configurations.xcconfig中,主要是为了直观处理。

根据环境填充即可,目前通过脚本会自动构建生成对应的plist文件,用于使用和查找。

附上脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
echo "CONFIGURATION -> \${CONFIGURATION}\"
echo "SRCROOT -> ${SRCROOT}"
/usr/libexec/PlistBuddy -c "Clear" ${SRCROOT}/buildsettings.plist
/usr/libexec/PlistBuddy -c "Clear" ${SRCROOT}/buildConfiguration.plist
xcodebuild -project kanfangriji.xcodeproj -target kanfangriji -configuration ${CONFIGURATION} -showBuildSettings | grep "=" > ${SRCROOT}/buildsettings.txt

cat ${SRCROOT}/buildsettings.txt | while read line;
do
k=${line%=*}
v=${line##*=}
/usr/libexec/PlistBuddy -c "Add $k string $v" ${SRCROOT}/buildsettings.plist
done

cat ${SRCROOT}/kanfangriji/ShareConfigurations.xcconfig | grep "=" | while read line;
do
k=${line%=*}
v=${line##*=}
/usr/libexec/PlistBuddy -c "Add $k string $v" ${SRCROOT}/buildConfiguration.plist
done

cat ${SRCROOT}/kanfangriji/${CONFIGURATION}Project.xcconfig | grep "=" | while read line;
do
k=${line%=*}
v=${line##*=}
/usr/libexec/PlistBuddy -c "Add $k string $v" ${SRCROOT}/buildConfiguration.plist
done

cat ${SRCROOT}/kanfangriji/${CONFIGURATION}Target.xcconfig | grep "=" | while read line;
do
k=${line%=*}
v=${line##*=}
/usr/libexec/PlistBuddy -c "Add $k string $v" ${SRCROOT}/buildConfiguration.plist
done

cat ${SRCROOT}/kanfangriji/${CONFIGURATION}Configurations.xcconfig | grep "=" | while read line;
do
k=${line%=*}
v=${line##*=}
/usr/libexec/PlistBuddy -c "Add $k string $v" ${SRCROOT}/buildConfiguration.plist
done
rm -rf ${SRCROOT}/buildsettings.txt

效果如下:

2.保密参数,则使用私有外联参数定义,隐藏内部参数


内部需要根据环境进行划分,使用时直接使用SKM_ServerHost就可以进行实现

新增配置项整体流程:

九、任务执行流程

具体步骤:

十、应用及测试

步骤:
1.增加配置项后,这里区分,如果是私密应用参数,则忽略;
2.如果是其他的,启动编译;
3.工具类分别读取脚本生成的结果配置表和手动配置表自动进行对比;
4.无输出信息,则成功;否则,输出不一致项信息,进行弹窗提醒
5.如果有提示信息,则排错及纠正后重复2开始的步骤;
6.切换不同scheme环境,重复2开始的步骤;
7.成功,则配置项正确通过
8.结束流程

附上自动匹配代码段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
+ (NSString *)matchBuildSettings {
    // 个人配置
    NSString *buildPath = [[NSBundle mainBundle] pathForResource:@"buildConfiguration" ofType:@"plist"];
    // 最终结果
    NSString *resultPath = [[NSBundle mainBundle] pathForResource:@"buildsettings" ofType:@"plist"];
    NSMutableString *muStr = [NSMutableString new];
    if (!buildPath) {
        [muStr appendString:@"buildConfiguration.plist文件缺失"];
        return muStr.copy;
    }
    if (!resultPath) {
        [muStr appendString:@"buildsettings.plist文件缺失"];
        return muStr.copy;
    }
    NSDictionary *buildData = [NSDictionary dictionaryWithContentsOfFile:buildPath];
    NSDictionary *resultData = [NSDictionary dictionaryWithContentsOfFile:resultPath];
    NSArray *buildKeys = [buildData allKeys];
    @autoreleasepool {
        for (NSString *key in buildKeys) {
            NSString *buildValue = buildData[key];
            NSString *resultValue = resultData[key];
            if (buildValue.length == 0) {
                [muStr appendFormat:@"%@为空\n",key];
            }
            if (buildValue.length > 0 && resultValue.length > 0) {
                if ([buildValue containsString:@"$(inherited)"]) {
                    NSString *temp = [buildValue stringByReplacingOccurrencesOfString:@"$(inherited)" withString:@""];
                    if (![resultValue containsString:temp]) {
                        [muStr appendFormat:@"%@=%@配置有误\n",key,buildValue];
                    }
                }else if(![buildValue isEqualToString:resultValue]){
                    [muStr appendFormat:@"%@=%@配置有误\n",key,buildValue];
                }
            }
        }
    }
    NSString *resultStr = muStr.copy;
    if (resultStr.length) {
        return resultStr;
    }
    return nil;
}

十一、其他

问题1:
统一管理后如何不小心在配置文件中更改了怎么办?
直接清空Build Settings那一栏,因为xcconfig文件内容会根据环境自动覆盖

问题2:
对打包脚本的影响?
不影响,只要不修改configuration的的name值,不会影响。