游戲助手技術(shù)細節(jié):結(jié)合Shell Script快速建立大量相似iOS App
背景
游戲助手項目是由一系列的游戲助手App組成,需要在現(xiàn)有基礎功能上,通過換皮膚、渠道識別以及新增功能,快速制作出小差異化的App,例如:爐石傳說助手、迷你西游助手,大話西游2助手等。

如何復制?
目前,有兩種可行方式:Targets(編譯目標)與Subprojects(子工程)
- Targets - 同一項目工程里,通過復制多個target,利用target與scheme的配合編譯不同的資源文件,得到多個App;
- Subprojects - 把主框架獨立成庫項目,再復制出多個子工程得到多個App。
下面,先從Targets方式說起,分別介紹一下兩個方式在我們項目中的具體實踐,并講述為什么我們放棄使用targets的方式,以及使用子項目方式都有哪些難點和關(guān)鍵點。
Targets方式
話不多說,先直接上圖給大家看看:

雖然xcode的文件組是虛擬的,但真實文件結(jié)構(gòu)也差不多,就不截圖上來了,下面直入實現(xiàn)細節(jié)。
實現(xiàn)細節(jié):
- 每個target指定不同的Info.plist文件;
- Build Phases的Copy Bundle Resources中僅添加對應資料夾的資源;
- Build Settings -> Preprocessing添加Preprocessing Macros預處理宏常量以區(qū)分各App的其它實現(xiàn)細節(jié);
- 復制同名帶_TS的為測試服數(shù)據(jù)target,區(qū)別在于bundle id添加測試服標識,以及Build Settings -> Custom Compiler Flags設置預編譯常量表明為測試服數(shù)據(jù)源;
- 預編譯常量:每個資料夾下獨立的AppBuilder.h(例如開放平臺id與統(tǒng)計id等);
- 服務端通過渠道標識區(qū)分來自哪個助手App;
- 指定target對應的scheme進行編譯,最終得到不同App。
使用Run Script
- Run Script腳本:放置bundle id等基礎信息,每次跑都修改Info.plist,而不是直接修改Info.plist;
- Run Script處理AppBuilder.h的使用,通過拷貝至基礎代碼替換文件的方式實現(xiàn)xcode唯一引用,而非直接引用資料夾下的。

復制助手的步驟:
- 右擊某個助手target,Duplicate(通過復制新建)這個target;
- 移除target內(nèi)上一個助手的資源,例如幾百個上個助手的皮膚圖片,一些不可重用有代碼等,但注意不要碰到基礎代碼內(nèi)的東西;
- 建立資源資料夾,準備Info.plist,AppBuilder.h,以及相關(guān)資源;
- 資源文件僅添加進xcode的這個新建target;
- 修改AppBuilder.h,Run Script的內(nèi)容;
- 修改xcode中新target的Product Name;
- 使用新的Info.plist為target的Info.plist
- 修改target內(nèi)的其它預編譯常量
- 通過對這個新的target 進行duplicate,修改類似相關(guān)的點得到測試服target
以上步驟缺一不可,當然,還是有進一步整理空間的,但主要問題不在這,請繼續(xù)往下看。
Targets的優(yōu)缺點:
優(yōu)點是工程結(jié)構(gòu)簡單,清晰,統(tǒng)一,另外唐巧《使用多target來構(gòu)建大量相似App》對此進行了很好的詮釋,最初我們的項目工程也是這種方式,但當助手數(shù)量增加后,但缺點也越來越明顯:
- 工程文件(xcodeproj)日益增大,如上面的Copy Bundle Resource就有1700+資源文件,compile的文件就300+,每一個資源文件的引用就是1行記錄,14個助手App,就有28個target,共28x2000 = 56000行記錄,單個文件就20MB;
- 因為上一條,導致xcode處理效率下降,甚至卡死,哪怕只修改target內(nèi)的一個字母,2012年i5 MBA直接卡頓30秒以上;
- 在協(xié)作與版本管理上,會造成多個人多次反復修改同一項目核心文件project.pbxproj,出現(xiàn)沖突機率很大,而且該文件不適合人工修改,一旦沖突出現(xiàn),如果修改得多,他人根本就無從下手,xcode也無法打開,只好無奈revert了。
在SNS工具上也為此向大牛唐巧請教過,他也無奈表示目前還沒什么辦法解決,于是我們決定用子項目的方式把target分出來。
子項目方式:利用腳本批處理復制
實現(xiàn)計劃
- 確定基礎功能明確,確立框架,最終基礎庫由Foundation與UI兩庫組成,以.a文件形式提供;
- 代碼重構(gòu),把注入式的配置文件AppBuilder.h、預編譯常量全部分離,脫出基礎庫,基礎庫不再預編譯進任何助手信息;
- Run Script改造,不再耦合任何助手信息,轉(zhuǎn)為讀取另外配置文件,并獨立成文件;
- 精簡target,不再儲存資源文件引用以外的信息
- 所有信息,包括需要預編譯信息,統(tǒng)一由運行期設置
- 建立Template項目,使用xcode workspace組織各子項目
- 編寫腳本,從Template生成新項目,目的把上面繁瑣的復制步驟去人工化,減負并減少出錯機率
執(zhí)行難點
大量的預編譯常量使用
因為AppBuilder.h使用預編譯常量記錄信息,在基礎庫里散落各地,需要逐一整理出來,并用運行時的單例來取代它們。這部分沒什么辦法,只能慢慢挑魚骨頭。

原代碼:
#if !kOpenPlatformEnabled
self.shareButton.hidden = YES;
//codes...
#else
self.shareButton.hidden = NO;
//codes else...
#endif
修改為
if (![BuildInfo shareInstance].openPlatformEnabled]){
self.shareButton.hidden = YES;
//codes...
} else {
self.shareButton.hidden = NO;
//codes else...
}
因為#define常量在編譯期就固定值,不包含常量類型信息,xcode不能輔助識別和Refactor,不利于維護和debug,所以常量定義盡量使用const而非#define。
// File.h
extern NSString *const MyKey;
// File.m
NSString *const MyKey = @"MyKey";
預編譯 vs 運行時
預編譯可以選擇性的編譯代碼或打包資源,例如正式包不會包含任何測試服信息,即使逆向工程也無法從中找到任何信息,在一定程序上起到數(shù)據(jù)保護作用。
#if BUILD_FOR_ONLINE_APP
return @"https://myonlinehost.163.com";
#else
return @"http://123.123.123.123/";
#endif
project.pbxproj文件
利用子工程的方式與target方式一樣需要進行項目配置,但最大的問題還在于xcode工程文件本身的組織方式,
- xcode文件的組是虛擬組,文件引用并非掃描當下目錄文件,不關(guān)心實體文件路徑只關(guān)心文件名,并且隱藏了具體路徑細節(jié);
- 通過Scheme指定Target進行編譯,target參數(shù)眾多,人工復制修改需要好記性與細心,是個繡花活。

.xcodeproj是一個包,顯示包內(nèi)容后會發(fā)現(xiàn)最核心的是project.pbxproj文件,這個文件其實是一個類似JSON形式的文本,但并非JSON,我們希望從中找出規(guī)律,讓腳本在復制的時候自動修改對應的內(nèi)容。

文件內(nèi)容非常龐大,但仔細觀察,還是能發(fā)現(xiàn)一些規(guī)律的,例如:
/* Begin PBXBuildFile section */
94EDEA9C1A13500E00AAEA5F /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 94EDEA9B1A13500E00AAEA5F /* AppDelegate.m */; };
....
/* End PBXBuildFile section */
類似注釋號的/* Begin PBXBuildFile section */明確了需要Build的文件;而類似94EDEA9B1A13500E00AAEA5F為索引key,xcode通過key來檢索所以信息點。
當然,我們并不需要太過關(guān)心這些,畢竟這文件不是給人去修改的。
Hack掉project.pbxproj
- 首先,利用xcode做好一個Template工程,并把所有文件路徑設置為Relative to Group,這樣能簡化路徑搜尋;

- 準備好要引用的代碼、資源、Info.plist、Framework等;
- 修改項目名字,scheme name,target name為好標記的字母,例如統(tǒng)一使用
Sample為名字替換目標; - 準備target內(nèi)其它需要的內(nèi)容;
- 關(guān)于AppDelegate.m,上面說了,xcode并不關(guān)心文件放哪里,所以你可以用A項目的AppDelegate.m放到B項目里,只要沒BUG,一樣能運行起來,所以最終的子項目只是借用了原AppDelegate.m,而不是每次都用新的文件,.h文件也如是。換句話說,xcode工程文件只是一個虛擬文件組織器,任何認為不需要獨立出來的文件都可以指向同一個,甚至是main.m文件!
- 讓工程正確運行起來,通過測試;
自動腳本Shell Script
當上面的操作準備好后,再打開project.pbxproj,會發(fā)現(xiàn)一切都如此清晰,只需要用shell來替換埋設好的關(guān)鍵字即可。
在Mac,用來替換文件字符串,并拷貝到目標目錄,可以用到sed命令:
sed -e "s/TemplateString/NewString/g" Template/template.xcodeproj/project.pbxproj > NewPath/NewNameFile/project.pbxproj
# 如果有多個要替換,可以多個集合多個替換字符串:
sed -e "s/TemplateString/NewString/g" -e "s/TemplateString2/NewString2/g" Template/template.xcodeproj/project.pbxproj > NewPath/NewNameFile/project.pbxproj
我們的腳本必須能接受幾個基本的參數(shù),然后把參數(shù)轉(zhuǎn)成對應的信息:
例如名為create.sh,執(zhí)行時這樣:
./create.sh -n ZGMH -b com.abc.zgmh -c zgmh_channel -p 1004
腳本開頭可以這樣寫:
while [ "$1" != "" ]; do
case $1 in
-n | --name ) shift
name=$1
;;
-b | -bundleid ) shift
bundleid=$1
;;
-c | --channel ) shift
channel=$1
;;
-p | --pushid ) shift
pushid=$1
;;
* ) usage
exit 1
esac
shift
done
這樣就能接受參數(shù)了
更多設置:settingVars.txt
在前文,我們看到在Run Script中進行了Info.plist修改,我們把Run Script單獨抽出來,并且用第三個文件來定義更多參數(shù)。
settingVars.txt:
# 配置信息 有空格必須用英文半角雙引號括起來
CHANNEL=sampleChannel #渠道名與目錄
# 以下為info.plist內(nèi)對應的內(nèi)容
APP_BUNDLE_ID=appBundleId #CFBundleIdentifier
CFBUNDLE_URL_NAME=zsUrlName #CFBundleURLName
CFBUNDLE_URL_SCHEMES=zsScmeme #CFBundleURLSchemes
CFBUNDLE_DISPLAY_NAME=某某游戲助手 #CFBundleDisplayName
如何讀進去腳本里?
#read setting vars
varsContent=$(<settingVars.txt)
eval "$varsContent"
簡單粗暴到連自己都不相信[捂臉]。
如何讀寫plist文件?
/usr/libexec/PlistBuddy -c "Set :CFBundleDisplayName ${CFBUNDLE_DISPLAY_NAME}" ${INFO_PLIST_FILE_PATH}
通過PlistBuddy來設置和讀取plist,把接下來的字段補充完成即可,全部代碼就不貼出來了。
小結(jié)一下
- 在xcode中配置好一切,埋設標識符;
- 編寫Shell Script,用
sed命令替換標識符并輸出成新的project.pbxproj文件, - 復制Template下所有文件到新目錄;
- 編寫統(tǒng)一runscript,讀取新助手資料夾下的settingVars.txt內(nèi)容,修改Info.plist;
- 復制助手準備完成
以上的步驟只需要一次準備,以后的助手復制只需要一條命令:
./create.sh -n ZGMH -b com.abc.zgmh -c zgmh_channel -p 1004
項目使用了workspace來管理,只在需要的助手才放進去:

總結(jié)
善于運用Shell Script,能幫人自動化執(zhí)行一些重復勞動,結(jié)合xcode的run script,可以最大限度解放勞動力,提高效率減少人工錯誤率。上文內(nèi)容包括了大部分助手復制技術(shù)實現(xiàn)關(guān)鍵點,事實上我們還可以給run script傳參數(shù),用來生成不同的內(nèi)部測試包。下一步,還會把自動化打包部分腳本再完善,自動生成多個包,并進行簽名。
【完】