利用
Xcode
进行开发时需要进行很多build setting
的设置以便能让项目按照设置的进行编译,同时有时候需要在编译时利用script
脚本进行一些设置,本文主要介绍xconfig
文件和script
脚本在Xcode
开发中使用。
Xcode编译
在使用xconfig
时有几个关于Xcode
的概念是需要理解的,这里我进行通俗简单的说明,同时需要知道Xcode
在编译的过程中具体帮我们做了那几件事情。
Xcode target
在实际开发中一个Xcode
创建的项目是可以有多个taget
的,比如我们创建一个widget
时Xcode
会自动新建一个target
对应这个widget
,也可以自己新建,同一个项目有多个target
可以满足不同的测试场景,比如在前期开发阶段使用一个target
,到UAT
阶段使用另外一个target
。一个target
对应一个product
,也就是编译后安装到手机上的项目,target
定义了生成的唯一 product
, 它将构建该product
所需的文件和处理这些文件所需的指令集整合进 build system
中,这些指令以 build setting
和 build phases
的形式存在,我们用xconfig
文件来设置 build setting
,同时将script
脚本添加到build phases
中。
Xcode project
Xcode project
是一个仓库,该仓库包含了所有的文件,资源和用于生成一个或者多个software products
的信息,它包含一个或者多个targets
,其中的每一个 target
指明了如何生成 products
。project
为其拥有的所有 targets
定义了默认的build settings
,例如project
中默认包含debug
和release
两种build settings
当然,每一个 target
能够制定其自己的 build settings
,且target
的build settings
会重写project
的 build settings
。
Xcode scheme
一个project
可以有多个target
,但是当前的target
只能有一个,scheme
就是用来确定当前的target
的,并制定当前的target
使用哪种configuration
。
新建configuration
打开项目编辑栏选择上面的progect
同时选择info
栏,可以看到Xcode
默认添加了二个Debug
和Release
的configuration
,点击做下角的+
号按钮选择复制Debug
或者Release
其中一个configuration
来新建并命名一个自己想取的名字,我这里命名为Mamba
。
Configuration文件的使用
平时手动的在Xcode
中进行项目的一些build setting
设置还是比较麻烦的,一个是需要在Xcode
中进行搜索,另外一个是不好管理,例如需要在debug
或者release
下进行不同的设置的话就比较麻烦。利用Configuration
文件来代替手动设置则更加的方便,直接新建Configuration Setting file
类型文件,如下图所示:
利用Configuration设置不同的项目名
Configuration
文件是可以继承的,一般先建立一个Common Configuration
文件用来作为父类,为此新建一个名为Common
的Configuration
文件,并加入如下代码:
APP_NAME = TestDemo
然后分别新建名为debug
,Mamba
和release
的Configuration
文件,并加入如下代码:
- debug
#include "Common.xcconfig"
APP_NAME = $(inherited)Debug
- Mamba
#include "Common.xcconfig"
APP_NAME = $(inherited)Mamba
- release
```Swift
#include "Common.xcconfig"
APP_NAME = $(inherited)Release
上面利用#include
进行导入依赖的Configuration
文件,并利用$(inherited)
来引用依赖的Configuration
文件中的变量。
Configuration
文件中的语法一般是SETTING_NAME = VALUE
,具体等式二边设置的值可见苹果官网.
设置Configuration
点击PROJECT
导航栏并选择Info
会发现多了一个上文我们添加的名为Mamba
的Configuration
。
点击左边的小三角箭头展开每个Configuration
后可以设置项目的project
级别的Configuration File
和target
级别的Configuration File
,当然也可以默认不设置。分别设置三个Configuration
下的project
级别的Configuration File
为Base
,target
级别的Configuration File
则为对应的Configuration File
,如下图所示:
查看是否设置成功
点击TARGETS
导航栏,选择Build Settings
并选中All
和Levels
滑到最下面可看见APP_NAME
的值设置如下:
这里需要解释一下几个设置的级别:
-
Resolved
: 最后生效的值 -
Target
: 显示在Target
级别生效的值,Target
级别的优先级是高于Project
的,并且默认继承Project
设置的值。 -
Project
: 显示在Project
级别生效的值,往常在Xcode
的General
设置的值就是这一级别的。 -
iOS Default
: 显示iOS
默认设置的值。
加上Configuration File
后优先级顺序从低到高如下:
- Platform defaults
- Project.xcconfig file
- Project file build settings
- Target .xcconfig file
- Target build settings
设置Info.plist
最后为了通过Configuration File
来控制APP
运行时名字的显示,需要在Info.plist
中链接Bundle display name
属性(没有的话需要新增)到我们上面设置的user-defined setting(APP_NAME)
上,为此修改Info.plist
中Bundle display name
的值为 $(APP_NAME)
。
测试是否生效
在Scheme
页面分别选择debug
,release
和mamba
三中不同的Configuration
环境运行APP
成功的根据不同的Configtation
设置不同的项目运行名字。
利用xconfig文件实现OC条件编译
在开发中经常需要进行条件编译,在OC
中可以利用pch
文件配合宏来实现,例如如下:
#ifdef DEBUG
#define BaseURL @"192.168.1.1:8080/appname/api"
#define PublicKEY @"QWE3R23WR09WURI220WR3TTY5ET3CR2X"
#else
#define BaseURL @"http://api.appname.com"
#define PublicKEY @"32GDG4575UB5M97O7M2X32RFH53QWT43"
#endif
通过在pch
文件中利用条件编译定义不用的宏来实现项目的动态切换配置,上述宏定义一般定义在.pch
中,通常.pch
文件中定义的宏都比较杂乱,希望能单独放在一个独立的文件中,可以通过新建一个头文件env.h
, 把上述宏定义放到env.h
中,在需要使用的时候导入头文件即可,把环境参数单独放在一个独立的头文件中,更加简洁,职能更加专一,也便于维护但是这种做法还不是最好的,因为还需要手动导入头文件,而且生产环境参数和开发环境参数是放在同一个文件中而是不是独立分开的,要想独立分开并且使用时又不用导入头文件可以通过Xcode
中的Configurations Setting Fil(.xcconfig)
来解决,这应该是最优的实现方式。
xconfig文件的设置
在上面的Debug.xconfig
和Mamba.xconfig
文件中分别加入如下代码:
- Debug.xconfig
WEBSERVICE_URL = @"www.baidu.com"
- Mamba.xconfig
WEBSERVICE_URL = @"www.jd.com"
这样只是自定义了一个Build Setting
变量,不能代码里像使用宏那样使用,Xcode
是支持利用GCC_PREPROCESSOR_DEFINITIONS
在定义宏的,在Common.xconfig
文件中加入如下代码:
GCC_PREPROCESSOR_DEFINITIONS = $(inherited) WEBSERVICE_URL='$(WEBSERVICE_URL)'
在TARGET
导航栏中Preprocessor Macros
即可看见我们定义的宏。
代码使用
可以在代码中直接使用定义的宏,当切换Configuration
时则会根据.xconfig
文件输入不同的打印。
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"-----------%@-------------",WEBSERVICE_URL);
}
使用
#include
语法来包含其他配置文件,如#include "Common.xcconfig"
, 最好是放在文件的最后面,放在文件的开头也可以。Common.xconfig
中第一个键的配置必须有:GCC_PREPROCESSOR_DEFINITIONS = $(inherited)
,没有Xcode
会报错,暴露自定义键时的语法:宏名='$(key)'
,在代码或其他地方使用宏名来引用,'$(key)'
:通过key
来指定每个模式下的对应的自定义键的名字,通常将宏的名字和key
的名字保持一致, 注意 等号前后一定不能有空,Common.xconfig
中第一个key
是GCC_PREPROCESSOR_DEFINITIONS = $(inherited)
后面跟自定义的key
,注意在第一个key
后面跟上自己定义的key
的时候一定不要回车换行,敲一个空格,然后在同一行后面追加就行了,换行会编译错误, 不能换行,不能换行,不能换行!
Swift中条件编译的实现
在Swift
中是不支持通过GCC_PREPROCESSOR_DEFINITIONS
来定义宏的,但是可以通过定义Custom Flags
进行定义,这里介绍另外一种方法,还是通过.xconfig文件
进行获取我们需要的宏。前面我们通过info.plist
获取到了.xconfig
文件中自定义的变量,再次我们同样通过info.plist
来获取自定义的变量的值来当做宏使用,首先在info.plist
中新建一个WEBSERVICE_URL
变量,并设置值为'$(WEBSERVICE_URL)'
,由于需要解析info.plist
中的变量,再次封装一个config.swift
的类用来解析:
import Foundation
enum Config {
static func stringValue(forKey key: String) -> String {
guard let value = Bundle.main.object(forInfoDictionaryKey: key) as? String else {
fatalError("Invalid value or undefined key")
}
return value
}
}
代码使用
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
print(Config.stringValue(forKey:"WEBSERVICE_URL"))
}
}
相比较于
OC
版本的是不能直接定义宏,需要通过在info.plist
定义后并通过方法取出值后才能使用,稍微麻烦了一点。
script的使用
上文我们已经知道xconfig
文件的使用,其实在编译之前不只是变量的自定义或者获取项目的一些默认参数,还可以在获取这些参数的基础上,将这些参数作为script
脚本的变量来做一些更有意义的事情,Xcode
是支持在编译之前链接script
脚本的。
script的初步认识
脚本一般来说就是可执行的二进制文件,下面先制作一个简单的脚本加深认知(实例代码采用Swift
),首先新建一个名为HelloXcode.swift
文件,加入如下代码:
import Foundation
@main
enum MyScript {
static func main() {
print("Hello Xcode")
}
}
下面我们用终端来编译上面的HelloXcode.swift
文件,cd
到文件所在的目录执行以下代码:
xcrun --sdk macosx swiftc -parse-as-library HelloXcode.swift -o CompiledScript
利用macOS SDK
来编译HelloXcode.swift
并输出名为CompiledScript
的二进制脚本文件,此时可以直接在当前目录利用./CompiledScript
执行该脚本文件,会直接输出打印HelloXcode
。为了在Xcode
编译阶段就能运行脚本,我们需要将脚本插入到Xcode
的Build Phases
中,首先我们先新建一个Build Phases
如下所示:
Xcode
中的Build Phases
选项卡是Xcode build
项目的中心,Xcode
在编译项目时其实帮我们做了如下几件事情:
- 确定项目的一些依赖并编译
- 编译项目的代码
- 链接上面编译的依赖文件
- 复制资源文件例如图片等到项目bundle中
这里我们是要在项目编译开始之前就运行脚本,所以需要调整新增加的Build Phases
的顺序,直接拖到Denpencies
下面,如下图所示:
点击刚刚新加的
Build Phases
可以重命名,展开后加入如下代码:
下面的
Input Files
可以理解为脚本的变量,这里将HelloXcode.swift
相对工程文件所在的路$SCRIPT_INPUT_FILE_0
进行引用,$(SRCROOT)
代表工程文件所在的目录,运行项目在build log
(不是打印台)会看见如下输出:
script的实际运用
利用script
来实现每当build的时候改变 Info.plist
中Bundle version
或者Bundle version string (short)
的值,新建一个IncBuildNumber.swift
文件,加入如下代码:
import Foundation
@main
enum IncBuildNumber {
static func main() {
guard let infoFile = ProcessInfo.processInfo
.environment["INFOPLIST_FILE"]
else {
return
}
guard let projectDir = ProcessInfo.processInfo.environment["SRCROOT"] else {
return
}
if var dict = NSDictionary(contentsOfFile:
projectDir + "/" + infoFile) as? [String: Any] {
guard
let currentVersionString = dict["CFBundleShortVersionString"]
as? String,
let currentBuildNumberString = dict["CFBundleVersion"] as? String,
let currentBuildNumber = Int(currentBuildNumberString)
else {
return
}
dict["CFBundleVersion"] = "\(currentBuildNumber + 1)"
if ProcessInfo.processInfo.environment["CONFIGURATION"] == "Release" {
var versionComponents = currentVersionString
.components(separatedBy: ".")
let lastComponent = (Int(versionComponents.last ?? "1") ?? 1)
versionComponents[versionComponents.endIndex - 1] =
"\(lastComponent + 1)"
dict["CFBundleShortVersionString"] = versionComponents
.joined(separator: ".")
}
(dict as NSDictionary).write(
toFile: projectDir + "/" + infoFile,
atomically: true)
}
}
}
当Xcode
在执行run script phase
时会通过环境变量environment variables
来共享build settings
,可以将环境变量在这里理解为全局变量,这里通过环境变量拿到了info.plist
中的CFBundleShortVersionString
和CFBundleVersion
变量,并根据CONFIGURATION
配置的是Release
还是Debug
来修改对应的BundleVersion
,至此每当build时
都会改变相应的BuildVersion
值。
总结
本文主要介绍了利用xconfig
文件如何进行项目的动态配置,并进行了实际的演示,同时介绍了script
在Xcode
中编译的基本使用,并配合xconfig
文件能让Xcode
在编译前做更多有意义的事情。