手把手教你給一個iOS app配置多個環境變量

前言

談到多環境,相信如今大多公司都至少有2-3個app環境了,好比Test環境,UAT(User Acceptance Test)用戶驗收測試環境,Release環境等等。當須要開發打多個包的時候,通常常見作法就是直接代碼裏面修改環境變量,改完以後Archive一下就打包了。固然這種作法很正確,只不過不是很優雅很高效。若是搭建好了Jenkins(搭建教程),咱們利用它來優雅的打包。若是利用Jenkins來打包,咱們就須要來給app來配置一下多個環境變量了。以後Jenkins分別再不一樣環境下自動集成便可。接下來,咱們來談談常見的2種作法。html

目錄

  • 1.利用Build Configuration來配置多環境
  • 2.利用xcconfig文件來配置多環境
  • 3.利用Targets來配置多環境

一.利用Build Configuration來配置多環境

前言裏面咱們先談到了需求,因爲須要配置多個環境,而且多個環境都須要安裝到手機上,那麼能夠配置Build Configuration來完成這個任務。若是Build Configuration還不熟悉的,能夠先溫習一下官方文檔ios

1. 新建Build Configuration

先點擊Project裏面找到Configuration,而後選擇添加,這裏新加一個Configuration。系統默認是2個,一個Debug,一個Release。這裏咱們須要選擇是複製一個Debug仍是Release。Release和Debug的區別是,Release是不能調試程序,由於默認是屏蔽了可調試的一些參數,具體能夠看BuildSetting裏面的區別,並且Release編譯時有作編譯優化,會比用Debug打包出來的體積更小一點。git

這裏咱們選擇一個Duplicate 「Debug」 Configuration,由於咱們新的環境須要debug,添加完了以後就會多了一套Configuration了,這一套實際上是包含了一些編譯參數的配置集合。若是此時項目裏面有cocopods的話,打開Configuration Set就會發現是以下的樣子:github

在咱們本身的項目裏面用了Pod,打開配置是會看到以下信息數組

注意:剛剛新建完Build Configuration以後,這時若是有pod,請當即執行一下xcode

pod install複製代碼

pod安裝完成以後會自動生成xcconfig文件,若是你手動新建這個xcconfig,而後把原來的debug和release對應的pod xcconfig文件內容複製進來,這樣作是無效的,須要pod本身去生成xcconfig文件才能被識別到。網絡

新建完Build Configuration,這個時候須要新建pod裏面對應的Build Configuration,要否則一會編譯會報錯。若是沒用pod,能夠忽略一下這一段。app

以下圖新建一個對應以前Porject裏面新建的Build Configurationjsp

2. 新建Scheme

接下來咱們要爲新的Configuration新建一個編譯Scheme。ide

新建完成以後,咱們就能夠編輯剛剛新建的Scheme,這裏能夠把Run模式和Archive都改爲新建Scheme。以下圖:

注意:若是是使用了Git這些協同工具的同窗這裏還須要把剛剛新建的Scheme共享出去,不然其餘人看不到這個Scheme。選擇「Manage Schemes」

3. 新建User-defined Build Settings

再次回到Project的Build Settings裏面來,Add User-Defined Setting。

咱們這裏新加入2個參數,CustomAppBundleld是爲了以後打包能夠分開打成多個包,這裏須要3個不一樣的Id,建議是直接在原來的Bundleld加上Scheme的名字便可。

CustomProductName是爲了app安裝到手機上以後,手機上顯示的名字,這裏能夠按照對應的環境給予描述,好比測試服,UAT,等等。以下圖。

這裏值得提到的一點是,下面Pods的Build_DIR這些目錄實際上是Pods本身生成好的,以前執行過Pod install 以後,這裏默認都是配置好的,不須要再改動了。

4. 修改info.plist文件 和 Images.xcassets

先來修改一下info.plist文件。

因爲咱們新添加了2個CustomAppBundleld 和 CustomProductName,這裏咱們須要把info.plist裏面的Bundle display name修改爲咱們自定義的這個字典。編譯過程當中,編譯器會根據咱們設置好的Scheme去本身選擇Debug,Release,TestRelease分別對應的ProductName。

咱們還須要在Images.xcassets裏面新添加2個New iOS App Icon,名字最好和scheme的名字相同,這樣好區分。

新建完AppIcon以後,再在Build Setting裏面找到Asset Catalog Compiler裏面,而後把這幾種模式下的App Icon set Name分別設置上對應的圖標。如上圖。

既然咱們已經新建了這幾個scheme,那接下來怎麼把他們都打包成app呢??這裏有一份官方的文檔Troubleshooting Application Archiving in Xcode這裏面詳細記錄了咱們平時點擊了Archive以後是怎麼打包的。

這裏分享一下我分好這些環境的心得。一切切記,每一個環境都要設置好Debug 和 Release!千萬別認爲線上的版本只設置Release就好,哪天須要調試線上版本,沒有設置Debug就無從下手了。也千萬別認爲測試環境的版本只要設置Debug就好,萬一哪天要發佈一個測試環境須要發Release包,那又無從下手了。個人建議就是每一個環境都配置Debug 和 Release,即便之後不用,也提早設置好,以防萬一。合理的設置應該以下圖這樣。

| -------------------------- |------------------|
|           Scheme           |   Configurations |  
| -------------------------- |------------------| 
|      XXXXProjectTest       |      Debug       | 
|                            |------------------|
|                            |      Release     | 
| -------------------------- |------------------|
|      XXXXProjectAppStore   |      Debug       | 
|                            |------------------|
|                            |      Release     | 
| -------------------------- |------------------|
|      XXXXProjectUAT        |      Debug       | 
|                            |------------------|
|                            |      Release     | 
| -------------------------- |------------------|複製代碼

注意這裏必定要把Scheme的名字和編譯方式區分開,選擇了一個Scheme,只是至關於選擇了一個環境,並非表明這Debug仍是Release。

我建議Scheme只配置環境,而進來的Run和Archive來配置Debug和Release,我建議每一個Scheme都按照上圖來,Run對應的Debug,Archive對應的Release。

配置好上述以後,就能夠選擇不一樣環境運行app了。能夠在手機上生成不一樣的環境的app,能夠同時安裝。以下圖。

5. 配置和獲取環境變量

接下來說幾種動態配置環境變量的方法

1. 使用GCC預編譯頭參數GCC_PREPROCESSOR_DEFINITIONS

咱們進入到Build Settings裏面,能夠找到Apple LLVM Preprocessing,這裏咱們能夠找到Preprocessor Macros在這裏,咱們是能夠加一些環境變量的宏定義來標識符。Preprocessor Macros能夠根據不一樣的環境預先制定不一樣定義的宏。

如上圖,圈出來的地方其實就是一個標識符。

有了這些咱們預先設置的標識符以後,咱們就能夠在代碼裏面寫入以下的代碼了。

#ifdef DEVELOP
#define searchURL @"http://www.baidu.com"
#define sociaURL  @"weibo.com"
#elif UAT
#define searchURL @"http://www.bing.com"
#define sociaURL  @"twitter.com"
#else
#define searchURL @"http://www.google.com"
#define sociaURL  @"facebook.com"
#endif複製代碼
2. 使用plist文件動態配置環境變量

咱們先來新建3個名字同樣的plist做爲3個環境的配置文件。

這裏名字同樣的好處是寫代碼方便,由於就只須要去讀取「Configuration.plist」就能夠了,若是名字不同,還要分別去把對應環境的plist名字拼接出來才能讀取。

衆所周知,在一個文件夾裏面新建2個相同名字的文件,Mac 系統都會提示咱們名字相同,不容許咱們新建。那咱們怎麼新建3個相同名字的文件呢?這其實很簡單,分別放在3個不一樣文件夾下面便可。以下圖:

我就是這樣放置的,你們能夠根據本身習慣去放置文件。

接下來咱們要作的是在編譯的時候,運行app前,動態的copy Configuration.plist到app裏面,這裏須要設置一個copy腳本。

進入到咱們的Target裏面,找到Build Phases,咱們新建一個New Copy Files Phase,而且重命名爲Copy Configuration Files

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.plist 文件能夠在編譯的時候,選擇其中一個打包進咱們的app。

再寫代碼每次讀取這個plist裏面的信息就能夠作到動態化了。

- (NSString *) readValueFromConfigurationFile {
    NSBundle *bundle = [NSBundle mainBundle];
    NSString *path = [bundle pathForResource:@"Configuration" ofType:@"plist"];
    NSDictionary *config = [NSDictionary dictionaryWithContentsOfFile:path];
    return config[@"serverURL"];
}複製代碼

這裏我假設plist文件裏面預設置了一個serverURL的字符串,用這種方式就能夠讀取出來了。固然在plist裏面也能夠設置數組,字典,相應的把返回值和Key值改一下就能夠了。

3. 使用單例來處理環境切換

固然使用一個單例也能夠作到環境切換。新建一個單例,而後能夠在設置菜單裏面加入一個列表,裏面列出全部的環境,而後用戶選擇之後,單例就初始化用戶所選的環境。和上面幾種方式不一樣的是,這種方式就是在一個app裏面切換多種環境。看你們的需求,任取所需。

二.利用文件來配置多環境

說道xcconfig,這個官方文檔上面也提到的不是很詳細,在網上尋找了一下,卻是找到了另一份詳細非官方文檔。The Unofficial Guide to xcconfig files

提到xcconfig,就要先說說幾個概念。

1. 區分幾個概念

先來區分一下Xcode Workspace、Xcode Scheme、Xcode Project、Xcode Target、Build Settings 這5者的關係。這5者的關係在蘋果官方文檔上其實都已經說明的很清楚了。詳情見文檔Xcode Concepts

我來簡單來解讀一下文檔。

Xcode Workspace

A workspace is an Xcode document that groups projects and other documents so you can work on them together. A workspace can contain any number of Xcode projects, plus any other files you want to include. In addition to organizing all the files in each Xcode project, a workspace provides implicit and explicit relationships among the included projects and their targets.

workspace這個概念你們應該都很清楚了。它能夠包含多個Project和其餘文檔文件。

Xcode Project

An Xcode project is a repository for all the files, resources, and information required to build one or more software products. A project contains all the elements used to build your products and maintains the relationships between those elements. It contains one or more targets, which specify how to build products. A project defines default build settings for all the targets in the project (each target can also specify its own build settings, which override the project build settings).

project就是一個個的倉庫,裏面會包含屬於這個項目的全部文件,資源,以及生成一個或者多個軟件產品的信息。每個project會包含一個或者多個 targets,而每個 target 告訴咱們如何生產 products。project 會爲全部 targets 定義了默認的 build settings,每個 target 也能自定義本身的 build settings,且 target 的 build settings 會重寫 project 的 build settings。

最後這句話比較重要,下面設置xcconfig的時候就會用到這一點。

Xcode Project 文件會包含如下信息,對資源文件的引用(源碼.h和.m文件,frame,資源文件plist,bundle文件等,圖片文件image.xcassets還有Interface Builder(nib),storyboard文件)、文件結構導航中用來組織源文件的組、Project-level build configurations(Debug\Release)、Targets、可執行環境,該環境用於調試或者測試程序。

Xcode Target

A target specifies a product to build and contains the instructions for building the product from a set of files in a project or workspace. A target defines a single product; it organizes the inputs into the build system—the source files and instructions for processing those source files—required to build that product. Projects can contain one or more targets, each of which produces one product.

target 會有且惟一輩子成一個 product, 它將構建該 product 所需的文件和處理這些文件所需的指令集整合進 build system 中。Projects 會包含一個或者多個 targets,每個 target 將會產出一個 product。

這裏值得說明的是,每一個target 中的 build setting 參數繼承自 project 的 build settings, 一旦你在 target 中修改任意 settings 來重寫 project settings,那麼最終生效的 settings 參數以在 target 中設置的爲準. Project 能夠包含多個 target, 可是在同一時刻,只會有一個 target 生效,可用 Xcode 的 scheme 來指定是哪個 target 生效。

Build Settings

A build setting is a variable that contains information about how a particular aspect of a product’s build process should be performed. For example, the information in a build setting can specify which options Xcode passes to the compiler.

build setting 中包含了 product 生成過程當中所需的參數信息。project的build settings會對於整個project 中的全部targets生效,而target的build settings是重寫了Project的build settings,重寫的配置以target爲準。

一個 build configaration 指定了一套 build settings 用於生成某一 target 的 product,例如Debug和Release就屬於build configaration。

Xcode Scheme

An Xcode scheme defines a collection of targets to build, a configuration to use when building, and a collection of tests to execute.

一個Scheme就包含了一套targets(這些targets之間可能有依賴關係),一個configuration,一套待執行的tests。

這5者的關係,舉個可能不恰當的例子, Xcode Workspace就如同工廠,Xcode Project如同車間,每一個車間能夠獨立於工廠來生產產品(project可獨立於workspace存在),可是各個車間組合起來就須要工廠來組織(若是用了cocopods,就須要用workspace)。Xcode Target是一條條的流水線,一條流水線上面只生產一種產品。Build Settings是生產產品的祕方,若是是生產汽水,Build Settings就是其中各個原料的配方。Xcode Scheme是生產方案,包含了流水線生產,祕方,還包含生產完成以後的質檢(test)。

2. 來建立一個xcconfig文件

而後建立好了這個文件,咱們在project裏面設置一下。

在這些地方把配置文件換成咱們剛剛新建的文件。

接下來就要編寫咱們的xcconfig文件了。這個文件裏面能夠寫的東西挺多的。細心的同窗就會發現,其實咱們一直使用的cocopods就是用這個文件來配置編譯參數的。咱們隨便看一個簡單的cocopods的xcconfig文件,就是下圖這樣子:

GCC_PREPROCESSOR_DEFINITIONS = $(inherited) COCOAPODS=1
HEADER_SEARCH_PATHS = $(inherited) "${PODS_ROOT}/Headers/Public" "${PODS_ROOT}/Headers/Public/Forms"
OTHER_CFLAGS = $(inherited) -isystem "${PODS_ROOT}/Headers/Public" -isystem "${PODS_ROOT}/Headers/Public/Forms"
OTHER_LDFLAGS = $(inherited) -ObjC -l"Forms"
PODS_ROOT = ${SRCROOT}/Pods複製代碼

咱們因爲須要配置網絡環境,那能夠這樣寫

//網絡請求baseurl
REQUESTBASE_URL = @"http:\\/\\/10.20.100.1"複製代碼

固然也能夠寫成cocopods那樣

GCC_PREPROCESSOR_DEFINITIONS = $(inherited) WEBSERVICE_URL='$(REQUESTBASE_URL)' MESSAGE_SYSTEM_URL='$(MESSAGE_SYSTEM_URL)'複製代碼

這裏利用了一個GCC_PREPROCESSOR_DEFINITIONS編譯參數。

Space-separated list of option specifications. Specifies preprocessor macros in the form foo (for a simple #define) or foo=1 (for a value definition). This list is passed to the compiler through the gcc -D option when compiling precompiled headers and implementation files.

GCC_PREPROCESSOR_DEFINITIONS 是 GCC 預編譯頭參數,一般咱們能夠在 Project 文件下的 Build Settings 對預編譯宏定義進行默認賦值。

它就是在Build Settings裏面的 Apple LLVM 7.X - Preprocessing - Preprocessor Macros 這裏。

Preprocessor Macros 實際上是按照 Configuration 選項進行默認配置的, 它是能夠根據不一樣的環境預先制定不一樣定義的宏,或者爲不一樣環境下的相同變量定義不一樣的值。

xcconfig 咱們能夠寫入不一樣的 Configuration 選項配置不一樣的文件。每個 xcconfig 能夠配置 Build Settings 裏的屬性值, 其實實質就是經過 xcconfig 去修改 GCC_PREPROCESSOR_DEFINITIONS 的值,這樣咱們就能夠作到動態配置環境的需求了。

最後還須要提的一點是,這個配置文件的level的問題。如今本地有這麼多配置,到底哪個最終生效呢?打開Build 裏面的level,咱們來看一個例子。

咱們目前能夠看到有5個配置,他們是有優先級的。優先級是從左往右,依次下降的。Resolved = target-level > project-level > 自定義配置文件 > iOS 默認配置。左邊第一列永遠顯示的是當前生效的最終配置結果。

知道了這個優先級以後,咱們能夠更加靈活的配置咱們的app了。

最後關於xcconfig配置,基本使用就這些了。可是這裏面的學問不只僅這些。

還能利用xcconfig動態配置Build Settings裏面的不少參數。這其實相似於cocopods的作法。可是有一個大神的作法很優雅。值得你們感興趣的人去學習學習。iOS大神Justin Spahr-Summers的開源庫xcconfigs提供了一個類權威的模板, 這是一個很好的學習使用xcconfig的庫,強烈推薦。

最後這裏有一個Demo,配置了Cocopods,配置了xcconfig文件,還有Build Configuration的,你們能夠看看,請多多指教,Demo

三.利用Targets來配置多環境

配置一個多環境其實一個Scheme和xcconfig已經徹底夠用了,爲何還要有這個第三點呢?雖然說僅僅爲了配置一個多環境這點「小事」,可是利用多個Targets也能實現需求,只不過有點「興師動衆」了。

關於構建Targets這個技術,我也是在2年前的公司實踐過。當時的需求是作一個OEM的產品。本身公司有主要產品,也幫其餘公司作OEM。一說到OEM,你們應該就知道Targets用到這裏的妙用了。利用Targets能夠瞬間大批量產生大量的app。

2013年巧哥也發過關於Targets的文章,猿題庫iOS客戶端的技術細節(一):使用多target來構建大量類似App,我原來公司在2014年也實現了這種功能。

僅僅只用一套代碼,就能夠生產出7個app。7個app的證書都是不一樣的,配置也都不一樣,可是代碼只須要維護一套代碼,就能夠完成維護7個app的目標。

下面咱們來看看怎麼新建Targets,有2種方法。

一種方法是徹底新建一個Targets,另一種方法是複製原有的Targets。

其實第一種方法創建出Targets,以後看你需求是怎麼樣的。若是也想是作OEM這種,能夠把新建出來的project刪掉,本地仍是維護一套代碼,而後在新建的Targets 的Build Phases裏面去把本地現有代碼加上,參數本身能夠隨意配置。這樣也是一套代碼維護多個app。

第二種方法就是複製一個原有的Targets,這種作法只用本身去改參數就能夠了。

再來講說Targets的參數。

因爲咱們新建了Targets,至關於新建了一個app了。因此裏面的全部的文件所有均可以更改。包括info.plist,源碼引用,Build Settings……全部參數均可以改,這樣就不只僅侷限於修改Scheme和xcconfig,因此以前說僅僅配置一個多環境用Targets有點興師動衆,可是它確實能完成目的。根據第二章裏面咱們也提到了,Targets至關於流水線,僅次於Project的地位,能夠想象,有了Targets,咱們沒有什麼不能修改的。

PS.最後關於Targets還有一點想說的,若是你們有多個app,而且這幾個app之間有超過80%的代碼都是徹底同樣的,或者說僅僅只是個別界面顯示不一樣,邏輯都徹底相同,建議你們用Targets來作,這樣只須要維護一套代碼就能夠了。維護多套相同的代碼,實在太沒有效率了。一個bug須要在多套代碼上面來回改動,費時費力。

這時候可能有人會問了,若是維護一套代碼,之後這些app若是需求有不一樣怎麼辦??好比要進入不一樣界面,跳轉不一樣界面,頁面也顯示不一樣怎麼辦??這個問題其實很簡單。在Targets裏面的Compile Sources裏面是能夠給每一個不一樣的Targets添加不一樣的編譯代碼的。只須要在每一個不一樣的Targets裏面加入不一樣界面的代碼進行編譯就能夠了,在跳轉的那個界面加上宏,來控制不一樣的app跳轉到相應界面。這樣本地仍是維護的一套代碼,只不過每一個Targets編譯的代碼就是這套代碼的子集了。這樣維護起來仍是很方便。也實現了不一樣app不一樣界面,不一樣需求了。

最後

其實這篇文章的需求源自於上篇Jenkins自動化持續集成,有一個需求是能打不一樣環境的包。以前沒有Jenkins的時候就改改URL運行一遍就好,雖然說作法不夠優雅,可是也不麻煩。如今想持續集成,只好把環境都分好,參數配置正確,這樣Jenkins能夠一次性多個環境的包一塊兒打。真正作到多環境的持續集成。

最後就能夠打出不一樣環境的包了。請你們多多指教。

相關文章
相關標籤/搜索