前言
談到多環境,相信現在大多公司都至少有2-3個app環境了,比如Test環境,UAT(User Acceptance Test)使用者驗收測試環境,Release環境等等。當需要開發打多個包的時候,一般常見做法就是直接程式碼裡面修改環境變數,改完之後Archive一下就打包了。當然這種做法很正確,只不過不是很優雅很高效。如果搭建好了Jenkins(搭建教程),我們利用它來優雅的打包。如果利用Jenkins來打包,我們就需要來給app來配置一下多個環境變數了。之後Jenkins分別再不同環境下自動整合即可。接下來,我們來談談常見的2種做法。
目錄
- 1.利用Build Configuration來配置多環境
- 2.利用xcconfig檔案來配置多環境
- 3.利用Targets來配置多環境
一.利用Build Configuration來配置多環境
前言裡面我們先談到了需求,由於需要配置多個環境,並且多個環境都需要安裝到手機上,那麼可以配置Build Configuration來完成這個任務。如果Build Configuration還不熟悉的,可以先溫習一下官方文件。
1. 新建Build Configuration
先點選Project裡面找到Configuration,然後選擇新增,這裡新加一個Configuration。系統預設是2個,一個Debug,一個Release。這裡我們需要選擇是複製一個Debug還是Release。Release和Debug的區別是,Release是不能除錯程式,因為預設是遮蔽了可除錯的一些引數,具體可以看BuildSetting裡面的區別,而且Release編譯時有做編譯優化,會比用Debug打包出來的體積更小一點。
這裡我們選擇一個Duplicate “Debug” Configuration,因為我們新的環境需要debug,新增完了之後就會多了一套Configuration了,這一套其實是包含了一些編譯引數的配置集合。如果此時專案裡面有cocopods的話,開啟Configuration Set就會發現是如下的樣子:
在我們自己的專案裡面用了Pod,開啟配置是會看到如下資訊
注意:剛剛新建完Build Configuration之後,這時如果有pod,請立即執行一下
1 |
pod install |
pod安裝完成之後會自動生成xcconfig檔案,如果你手動新建這個xcconfig,然後把原來的debug和release對應的pod xcconfig檔案內容複製進來,這樣做是無效的,需要pod自己去生成xcconfig檔案才能被識別到。
新建完Build Configuration,這個時候需要新建pod裡面對應的Build Configuration,要不然一會編譯會報錯。如果沒用pod,可以忽略一下這一段。
如下圖新建一個對應之前Porject裡面新建的Build Configuration
2. 新建Scheme
接下來我們要為新的Configuration新建一個編譯Scheme。
新建完成之後,我們就可以編輯剛剛新建的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,即使以後不用,也提前設定好,以防萬一。合理的設定應該如下圖這樣。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
| -------------------------- |------------------| | 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可以根據不同的環境預先制定不同定義的巨集。
如上圖,圈出來的地方其實就是一個識別符號。
有了這些我們預先設定的識別符號之後,我們就可以在程式碼裡面寫入如下的程式碼了。
1 2 3 4 5 6 7 8 9 10 |
#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。
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.plist 檔案可以在編譯的時候,選擇其中一個打包進我們的app。
再寫程式碼每次讀取這個plist裡面的資訊就可以做到動態化了。
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檔案裡面預設定了一個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檔案,就是下圖這樣子:
1 2 3 4 5 |
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 |
我們由於需要配置網路環境,那可以這樣寫
1 2 |
//網路請求baseurl REQUESTBASE_URL = @"http:\\/\\/10.20.100.1" |
當然也可以寫成cocopods那樣
1 |
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可以一次性多個環境的包一起打。真正做到多環境的持續整合。
最後就可以打出不同環境的包了。請大家多多指教。