問題描述
iOS開發中經常要用到模擬器,甚至比真機被用得更頻繁。模擬器相對真機有下面幾種優勢:
* 模擬器一般不卡,性能往往比在真機上跑更穩定,因為電腦有更大的內存,更穩定的網絡。
* 可以模擬系統、設備、地理位置等。
* 調IM時,加一個模擬器,就可以互發消息了。
* 導Sandbox數據方便。
* 抓包比真機方便。
* 調試比真機方便,真機需要裝證書。
* ...
然而,有時候第三方SDK集成時,第三方SDK可能不提供模擬器的x86架構,那么在鏈接時,就會提示無法找到符號。
如tinyLibTool項目中的demo,鏈接時,會報沒有找到x86_64架構對應的符號:
如果用lipo -info
命令查看libMyLib.a
這個庫,就會發現它只提供了 arm7
和 arm64
兩種架構,而沒有x86_64架構。
# lipo -info libMyLib.a
Architectures in the fat file: libMyLib.a are: armv7 arm64
如果碰到這種庫,引入它之后,項目就不再能在模擬器上運行了,因為它鏈接都不會過。而我們往往希望引入庫之前的其他功能仍能在模擬器上調試。
解決思路
你可以要求SDK廠商提供模擬器的版本,他們頂多改幾行腳本,多產生一個x86架構,再把兩個.a
合并就行。但是如果碰上比較老沒有維護的SDK,或者廠商認為SDK不需要考慮模擬器上運行的場景,那就比較麻煩了。
你可以把所有用到SDK的代碼通過TARGET_OS_SIMULATOR
宏來判斷。但是這樣可能工作量比較大,而且容易出問題。
這里另外給出一種思路,我們可以根據庫中的頭文件,自己空實現這些接口,最后編譯產生一個x86架構的庫,并把它加到工程里面,這樣工程鏈接時就不會出錯了。
空實現,指的是函數里什么都不做,直接返回。如:
+ (instancetype)footerWithRefreshingTarget:(id)target refreshingAction:(SEL)action {
return 0;
}
我們知道,objc
里面,如果調用空對象的方法,程序并不會有問題,只是什么都不做。如下面代碼,雖然footer
為nil
,仍不會崩潰。
MyRefreshFooter *footer = [MyRefreshFooter footerWithRefreshingTarget:nil refreshingAction:nil];
[footer resetNoMoreData];
所以在模擬器上除了SDK的功能不能用,其他模塊的功能并不會受影響。
這種思路除了能解決編譯問題,還有種好處是,不用改任何原來工程中的代碼,只是附加了一個x86的lib,不影響應用在真機上的功能。
確定了這種思路后,還可以把這種邏輯泛化應用到任意的庫中,通過使用適當的工具,可以自動解析objc或cpp的頭文件,產生相應空實現的代碼,并編譯產生需要的x86架構的庫。
工具
下面介紹,我寫的工具tinyLibTool。使用它,基本可以自動產生空實現的函數。工程目錄說明:
使用步驟:
1. 把要解析的庫的頭文件,放入input目錄。
2. 運行 ``python tinyLibTool.py`` 腳本,產生``output``。
3. 運行 ``ruby proj_tool.rb``,自動往工程中加入文件,并打開工程。
4. 編譯工程,選擇模擬器,生產libSim.a。
5. 把libSim.a放到原來的工程,原來的工程就可以鏈接通過。
1. objc頭文件解析
objc類的語法還算比較簡單的,可以通過正則來抓取函數。
獲取類名和類的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
這個函數。
2. cpp頭文件解析
一般SDK提供給iOS用時,會通過objc來暴露接口。如果SDK直接提供cpp接口,我們還是要空實現cpp接口。
解析cpp接口比較麻煩,特別是可能引入了c++11之類的特性,還有類中可以嵌套定義內部類,正則對嵌套的處理比較麻煩。好在我們可以借助編譯器前端clang來實現cpp的解析[libTooliing]。相關的工具源碼在tiny-lib-tool-src
下,腳本調用邏輯在dealCppHeader
函數中。
注:我們只需簡單地提取出函數名,手動簡單解析應該也可以,比較繁瑣而已。
使用clang解析的部分較復雜,你可以直接用項目中生成的tiny-lib-tool。或者說工程中沒有涉及cpp接口,你可以跳過這個章節。
clang環境
如果你要手動編譯tiny-lib-tool-src
,需要安裝c++編譯工具鏈、llvm庫、cmake。
c++編譯工具鏈一般隨Xcode或command-line-tool提供。
llvm庫,你可以下載源碼自己編譯,或選擇下載預編譯好的庫(LLVM Download Page)。
注:推薦下載現成的,源碼編譯太費時。用brew安裝llvm可能也可以。下載現成的,最好選擇llvm 4.0.0,最新的4.0.1的libclang在mac 10.12上有問題。
cmake是目前最流行的一套c++跨平臺編譯工具,自帶Makefile、Xcode工程、VS工程、Ninja等生成器。可以通過 brew install cmake
來安裝。
c++編譯工具和llvm都需要寫到環境變量中(最好放到.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工具
環境準備好后,就可以進入 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基礎
如果你想了解clang,可以從 Clang documentation開始。
項目中使用到的libTooling 和 AST Matcher 也可以看下。
項目中主要代碼,添加Matcher:
DeclarationMatcher classMatcher = functionDecl().bind("staticFuncDecl");
Matcher.addMatcher(classMatcher, &HandlerForClassMatcher);
Matcher的回調
virtual void run(const MatchFinder::MatchResult &Result){
if (const FunctionDecl *cmd1 = Result.Nodes.getNodeAs<FunctionDecl>("staticFuncDecl")) {
// 只處理當前文件,不處理被包含頭文件中的類
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. 產生lib工程
解析了objc和cpp的header并產生相應的空實現后,我們需要創建一個lib工程,并把這些代碼加入工程中。項目根目錄中放了一個libSim的模板工程,它會被拷入output中,然后你可以調用如下腳本:
ruby proj_tool.rb
該腳本會把實現文件加入工程中,并打開工程。當然,你也可以手動創建lib工程,手動添加實現文件。
注: 選用ruby腳本,是因為它對Xcode工程文件的支持比較好,有一個專門的
xcodeproj
庫。
TODO&Bug
1. 解析objc也用clang來解析。
2. 解析時,動態處理未知的符號定義。參考cling。
3. std::string等標準庫類型會被解析成內部實現。
引用&參考
1. Generate C interface from C++ source code using Clang libtooling