iOS开发中xconfig和script脚本的使用

利用Xcode进行开发时需要进行很多build setting的设置以便能让项目按照设置的进行编译,同时有时候需要在编译时利用script脚本进行一些设置,本文主要介绍xconfig文件和script脚本在Xcode开发中使用。

Xcode编译

在使用xconfig时有几个关于Xcode的概念是需要理解的,这里我进行通俗简单的说明,同时需要知道Xcode在编译的过程中具体帮我们做了那几件事情。

Xcode target

在实际开发中一个Xcode创建的项目是可以有多个taget的,比如我们创建一个widgetXcode会自动新建一个target对应这个widget,也可以自己新建,同一个项目有多个target可以满足不同的测试场景,比如在前期开发阶段使用一个target,到UAT阶段使用另外一个target。一个target对应一个product,也就是编译后安装到手机上的项目,target定义了生成的唯一 product, 它将构建该product 所需的文件和处理这些文件所需的指令集整合进 build system 中,这些指令以 build settingbuild phases的形式存在,我们用xconfig文件来设置 build setting,同时将script脚本添加到build phases 中。

新建target

Xcode project

Xcode project 是一个仓库,该仓库包含了所有的文件,资源和用于生成一个或者多个software products 的信息,它包含一个或者多个targets,其中的每一个 target指明了如何生成 productsproject为其拥有的所有 targets定义了默认的build settings,例如project中默认包含debugrelease 两种build settings 当然,每一个 target能够制定其自己的 build settings,且targetbuild settings 会重写projectbuild settings

Xcode scheme

一个project可以有多个target,但是当前的target只能有一个,scheme就是用来确定当前的target的,并制定当前的target使用哪种configuration

新建configuration

打开项目编辑栏选择上面的progect同时选择info栏,可以看到Xcode默认添加了二个DebugReleaseconfiguration,点击做下角的+号按钮选择复制Debug或者Release其中一个configuration来新建并命名一个自己想取的名字,我这里命名为Mamba

Configuration文件的使用

平时手动的在Xcode中进行项目的一些build setting设置还是比较麻烦的,一个是需要在Xcode中进行搜索,另外一个是不好管理,例如需要在debug或者release下进行不同的设置的话就比较麻烦。利用Configuration文件来代替手动设置则更加的方便,直接新建Configuration Setting file类型文件,如下图所示:

利用Configuration设置不同的项目名

Configuration文件是可以继承的,一般先建立一个Common Configuration文件用来作为父类,为此新建一个名为CommonConfiguration文件,并加入如下代码:

APP_NAME = TestDemo

然后分别新建名为debugMambareleaseConfiguration文件,并加入如下代码:

  • 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会发现多了一个上文我们添加的名为MambaConfiguration

点击左边的小三角箭头展开每个Configuration后可以设置项目的project级别的Configuration Filetarget级别的Configuration File,当然也可以默认不设置。分别设置三个Configuration下的project级别的Configuration FileBasetarget级别的Configuration File则为对应的Configuration File,如下图所示:

查看是否设置成功

点击TARGETS导航栏,选择Build Settings并选中AllLevels滑到最下面可看见APP_NAME的值设置如下:


这里需要解释一下几个设置的级别:

  • Resolved: 最后生效的值
  • Target: 显示在Target级别生效的值,Target级别的优先级是高于Project的,并且默认继承Project设置的值。
  • Project: 显示在Project级别生效的值,往常在XcodeGeneral设置的值就是这一级别的。
  • 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.plistBundle display name的值为 $(APP_NAME)

测试是否生效

Scheme页面分别选择debugreleasemamba三中不同的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.xconfigMamba.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中第一个keyGCC_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编译阶段就能运行脚本,我们需要将脚本插入到XcodeBuild 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.plistBundle 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中的CFBundleShortVersionStringCFBundleVersion变量,并根据CONFIGURATION配置的是Release还是Debug来修改对应的BundleVersion,至此每当build时都会改变相应的BuildVersion值。

总结

本文主要介绍了利用xconfig文件如何进行项目的动态配置,并进行了实际的演示,同时介绍了scriptXcode中编译的基本使用,并配合xconfig文件能让Xcode在编译前做更多有意义的事情。

你可能感兴趣的:(iOS开发中xconfig和script脚本的使用)