問題描述
iOS開發(fā)中經(jīng)常要用到模擬器,甚至比真機(jī)被用得更頻繁。模擬器相對真機(jī)有下面幾種優(yōu)勢:
* 模擬器一般不卡,性能往往比在真機(jī)上跑更穩(wěn)定,因?yàn)殡娔X有更大的內(nèi)存,更穩(wěn)定的網(wǎng)絡(luò)。
* 可以模擬系統(tǒng)、設(shè)備、地理位置等。
* 調(diào)IM時,加一個模擬器,就可以互發(fā)消息了。
* 導(dǎo)Sandbox數(shù)據(jù)方便。
* 抓包比真機(jī)方便。
* 調(diào)試比真機(jī)方便,真機(jī)需要裝證書。
* ...
然而,有時候第三方SDK集成時,第三方SDK可能不提供模擬器的x86架構(gòu),那么在鏈接時,就會提示無法找到符號。
如tinyLibTool項(xiàng)目中的demo,鏈接時,會報沒有找到x86_64架構(gòu)對應(yīng)的符號:

如果用lipo -info 命令查看libMyLib.a這個庫,就會發(fā)現(xiàn)它只提供了 arm7和 arm64兩種架構(gòu),而沒有x86_64架構(gòu)。
# lipo -info libMyLib.a
Architectures in the fat file: libMyLib.a are: armv7 arm64
如果碰到這種庫,引入它之后,項(xiàng)目就不再能在模擬器上運(yùn)行了,因?yàn)樗溄佣疾粫^。而我們往往希望引入庫之前的其他功能仍能在模擬器上調(diào)試。
解決思路
你可以要求SDK廠商提供模擬器的版本,他們頂多改幾行腳本,多產(chǎn)生一個x86架構(gòu),再把兩個.a合并就行。但是如果碰上比較老沒有維護(hù)的SDK,或者廠商認(rèn)為SDK不需要考慮模擬器上運(yùn)行的場景,那就比較麻煩了。
你可以把所有用到SDK的代碼通過TARGET_OS_SIMULATOR宏來判斷。但是這樣可能工作量比較大,而且容易出問題。
這里另外給出一種思路,我們可以根據(jù)庫中的頭文件,自己空實(shí)現(xiàn)這些接口,最后編譯產(chǎn)生一個x86架構(gòu)的庫,并把它加到工程里面,這樣工程鏈接時就不會出錯了。
空實(shí)現(xiàn),指的是函數(shù)里什么都不做,直接返回。如:
+ (instancetype)footerWithRefreshingTarget:(id)target refreshingAction:(SEL)action {
return 0;
}
我們知道,objc里面,如果調(diào)用空對象的方法,程序并不會有問題,只是什么都不做。如下面代碼,雖然footer為nil,仍不會崩潰。
MyRefreshFooter *footer = [MyRefreshFooter footerWithRefreshingTarget:nil refreshingAction:nil];
[footer resetNoMoreData];
所以在模擬器上除了SDK的功能不能用,其他模塊的功能并不會受影響。
這種思路除了能解決編譯問題,還有種好處是,不用改任何原來工程中的代碼,只是附加了一個x86的lib,不影響應(yīng)用在真機(jī)上的功能。
確定了這種思路后,還可以把這種邏輯泛化應(yīng)用到任意的庫中,通過使用適當(dāng)?shù)墓ぞ?,可以自動解析objc或cpp的頭文件,產(chǎn)生相應(yīng)空實(shí)現(xiàn)的代碼,并編譯產(chǎn)生需要的x86架構(gòu)的庫。
工具
下面介紹,我寫的工具tinyLibTool。使用它,基本可以自動產(chǎn)生空實(shí)現(xiàn)的函數(shù)。工程目錄說明:

使用步驟:
1. 把要解析的庫的頭文件,放入input目錄。
2. 運(yùn)行 ``python tinyLibTool.py`` 腳本,產(chǎn)生``output``。
3. 運(yùn)行 ``ruby proj_tool.rb``,自動往工程中加入文件,并打開工程。
4. 編譯工程,選擇模擬器,生產(chǎn)libSim.a。
5. 把libSim.a放到原來的工程,原來的工程就可以鏈接通過。
1. objc頭文件解析
objc類的語法還算比較簡單的,可以通過正則來抓取函數(shù)。
獲取類名和類的body:
re.compile(r'''(?i)(@interface\s+(\w+)\s*?(?:.*?)$.*?^@end)''', re.S|re.M)
獲取類中方法:
re.compile(r'''(?i)(^\s*[-|+]\s*?\((.*?)\)(\w*?)(.*?)\s*?);''', re.S|re.M)
具體可以查看python腳本中的 dealObjcHead這個函數(shù)。
2. cpp頭文件解析
一般SDK提供給iOS用時,會通過objc來暴露接口。如果SDK直接提供cpp接口,我們還是要空實(shí)現(xiàn)cpp接口。
解析cpp接口比較麻煩,特別是可能引入了c++11之類的特性,還有類中可以嵌套定義內(nèi)部類,正則對嵌套的處理比較麻煩。好在我們可以借助編譯器前端clang來實(shí)現(xiàn)cpp的解析[libTooliing]。相關(guān)的工具源碼在tiny-lib-tool-src下,腳本調(diào)用邏輯在dealCppHeader函數(shù)中。
注:我們只需簡單地提取出函數(shù)名,手動簡單解析應(yīng)該也可以,比較繁瑣而已。
使用clang解析的部分較復(fù)雜,你可以直接用項(xiàng)目中生成的tiny-lib-tool?;蛘哒f工程中沒有涉及cpp接口,你可以跳過這個章節(jié)。
clang環(huán)境
如果你要手動編譯tiny-lib-tool-src,需要安裝c++編譯工具鏈、llvm庫、cmake。
c++編譯工具鏈一般隨Xcode或command-line-tool提供。
llvm庫,你可以下載源碼自己編譯,或選擇下載預(yù)編譯好的庫(LLVM Download Page)。
注:推薦下載現(xiàn)成的,源碼編譯太費(fèi)時。用brew安裝llvm可能也可以。下載現(xiàn)成的,最好選擇llvm 4.0.0,最新的4.0.1的libclang在mac 10.12上有問題。
cmake是目前最流行的一套c++跨平臺編譯工具,自帶Makefile、Xcode工程、VS工程、Ninja等生成器。可以通過 brew install cmake 來安裝。
c++編譯工具和llvm都需要寫到環(huán)境變量中(最好放到.zshrc或.bashrc中),如:
export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"
export LLVM_DIR="/work/compiler/llvm400"
export PATH="$PATH:$LLVM_DIR/bin"
你可以通過以下命令查看是否安裝成功,如果有這兩條命令,則安裝成功。
# clang -v
# llvm-config --libs
編譯clang工具
環(huán)境準(zhǔn)備好后,就可以進(jìn)入 tiny-lib-tool-src源碼目錄,然后通過cmake工具編譯了。
# cd tiny-lib-tool-src
# mkdir build
# cd build
# cmake ..
# make & make install
注:如果想用自己編譯的
tiny-lib-tool,可能需要修改腳本中的
self.tool_cmd = r'''./tiny-lib-tool'''
clang libTooling基礎(chǔ)
如果你想了解clang,可以從 Clang documentation開始。
項(xiàng)目中使用到的libTooling 和 AST Matcher 也可以看下。
項(xiàng)目中主要代碼,添加Matcher:
DeclarationMatcher classMatcher = functionDecl().bind("staticFuncDecl");
Matcher.addMatcher(classMatcher, &HandlerForClassMatcher);
Matcher的回調(diào)
virtual void run(const MatchFinder::MatchResult &Result){
if (const FunctionDecl *cmd1 = Result.Nodes.getNodeAs<FunctionDecl>("staticFuncDecl")) {
// 只處理當(dāng)前文件,不處理被包含頭文件中的類
SourceManager &srcMgr = Result.Context->getSourceManager();
string fileName = srcMgr.getFilename(cmd1->getLocation()).str();
if (fileName.rfind(InputFile)==string::npos) {
return;
}
// 判斷是否是類中的方法
if (const CXXMethodDecl *cmd = dyn_cast<CXXMethodDecl>(cmd1)) {
// 類中方法聲明的處理
//...
else {
// 不在類中的方法聲明的處理
//...
}
}
}
3. 產(chǎn)生lib工程
解析了objc和cpp的header并產(chǎn)生相應(yīng)的空實(shí)現(xiàn)后,我們需要創(chuàng)建一個lib工程,并把這些代碼加入工程中。項(xiàng)目根目錄中放了一個libSim的模板工程,它會被拷入output中,然后你可以調(diào)用如下腳本:
ruby proj_tool.rb
該腳本會把實(shí)現(xiàn)文件加入工程中,并打開工程。當(dāng)然,你也可以手動創(chuàng)建lib工程,手動添加實(shí)現(xiàn)文件。
注: 選用ruby腳本,是因?yàn)樗鼘code工程文件的支持比較好,有一個專門的
xcodeproj庫。
TODO&Bug
1. 解析objc也用clang來解析。
2. 解析時,動態(tài)處理未知的符號定義。參考cling。
3. std::string等標(biāo)準(zhǔn)庫類型會被解析成內(nèi)部實(shí)現(xiàn)。
引用&參考
1. Generate C interface from C++ source code using Clang libtooling