最近在作一個 iOS 的 cocos2d-x 項目接入新浪微博 SDK 的時候被「坑」了,最後終於順利的解決了。發現網上也有很多人遇到同樣的問題,可是能找到的數量有限的解決辦法寫得都不詳細,很難讓人理解,我來深刻的寫一寫。git
Mac OS X 10.10.1程序員
Xcode 6.1.1 (6A2008a)github
Cocos2d-x 3.2架構
新浪微博 SDK for iOS 2015 年 1 月 5 日從 github clone 的版本app
根據新浪微博 SDK 附帶的文檔接入項目後,在模擬器運行項目,在調用註冊方法時發生崩潰。註冊方法代碼:編輯器
1 |
[WeiboSDK registerApp: @"xxxxxxxx"]; |
崩潰信息打印以下:函數
1 |
[__NSDictionaryM weibosdk_WBSDKJSONString] : unrecognized selector sent to instance 0x170255780 |
新浪微博 SDK 附帶的文檔中有這麼一個說明:工具
在工程中引入靜態庫以後,須要在編譯時添加 –ObjC 編譯選項,避免靜態庫中類 加載 不全形成程序崩潰。方法:程序 Target->Buid Settings->Linking 下 Other Linker Flags 項添加-ObjC測試
在網上看到遇到一樣崩潰錯誤的人有提到在編譯時添加 -all_load
編譯選項時也能夠解決問題。方法也是在 Target->Buid Settings->Linking 下 Other Linker Flags 項添加-all_load
。網站
無獨有偶,我在打開新浪微博 SDK 附帶的 Demo 項目時發現這個項目的編譯選項也是-all_load
而不是它本身文檔所提示的。並且在一樣的開發環境下,個人 cocos2d-x 項目會崩潰,可是新浪微博 SDK 附帶的 Demo 能夠正常工做,想必上述兩個解決方案應該是正解-ObjC
可是在給本身的 cocos2d-x 項目添加了編譯選項後,再次編譯運行就發生了錯誤,錯誤信息以下:
1 2 3 4 5 6 7 8 |
Undefined symbols for architecture i386: "_GCControllerDidConnectNotification", referenced from: -[GCControllerConnectionEventHandler observerConnection:disconnection:] in libcocos2dx iOS.a(CCController-iOS.o) "_GCControllerDidDisconnectNotification", referenced from: -[GCControllerConnectionEventHandler observerConnection:disconnection:] in libcocos2dx iOS.a(CCController-iOS.o) "_OBJC_CLASS_$_GCController", referenced from: objc-class-ref in libcocos2dx iOS.a(CCController-iOS.o) (maybe you meant: _OBJC_CLASS_$_GCControllerConnectionEventHandler) |
不管是設置成仍是-ObjC
編譯都會失敗,都會報上述找不到符號的連接錯誤。-all_load
這裏先給出正確的解決辦法再談談爲何要這麼作。正確的作法仍是設置 Other Linker Flags 這個編譯選項,只不過即不用用也不能用-ObjC
,而是要用-all_load
-force_load path/to/your/libWeiboSDK.a
,後面跟的是新浪微博 SDK 靜態連接庫的確切位置。
這裏不打算過多的介紹編譯連接相關的只是,可是強烈推薦一本書《程序員的自我修養》,光看正標題你可能會擔憂這是本沒什麼「正經」內容的書,至少我當初第一次看到這書名的時候就是這麼認爲的,可是我錯了,這本書的副標題是連接、裝載與庫。相信我,看過這本書 N 遍以後你自會對程序從源代碼編譯連接到生成二進制程序的原理和過程有一個很是透徹的理解,而且更重要的是看過這本書 N 遍以後你會上升幾個層次。
言歸正傳,一個工程的源代碼最終變成二進制的可執行程序、動態連接庫或靜態連接庫要經歷這麼幾個過程:
1 |
源代碼 ==[編譯器]==》 彙編碼 ==[彙編器]==》 對象文件 ==[連接器]==》 可執行程序、動態連接庫或靜態連接庫 |
通俗的講,咱們在源碼中寫的全局變量名
、函數名
或類名
在生成的*.o
對象文件中都叫作符號,存在一個叫作符號表
的地方。
舉個例子:咱們在a.c
文件中寫了一個函數叫foo()
,而後在main.c
文件中調用了foo()
函數,在將源碼編譯生成的對象文件中a.o
對象文件中的符號表裏保存着foo()
函數符號,並經過該符號能夠定位到a.o
文件中關於foo()
方法的具體實現代碼。
連接器在連接生成最終的二進制程序的時候會發現main.o
對象文件中引用了符號foo()
,而foo()
符號並無在main.o
文件中定義,因此不會存在與main.o
對象文件的符號表中,因而連接器就開始檢查其餘對象文件,當檢查到a.o
文件中定義了符號foo()
,因而就將a.o
對象文件連接進來。這樣就確保了在main.c
中可以正常調用a.c
中實現的foo()
方法了。
Unix 的靜態連接庫沒什麼神祕的,它就是個壓縮包,和平時比較常見的 zip 或 rar 之類的壓縮包同樣,只不過人家是用一個叫 ar 的壓縮工具壓縮的而已。因此咱們給它解壓縮一下,看看它裏面都有什麼。既然是用 ar 壓縮的,解壓天然也要用 ar 這個工具。在命令行執行:
1 |
ar -x lieWeiboSDK.a |
結果報錯了:
1 2 |
ar: libWeiboSDK.a is a fat file (use libtool(1) or lipo(1) and ar(1) on it) ar: libWeiboSDK.a: Inappropriate file type or format |
這裏先解釋一下它爲何這麼肥(fat)。在作 iOS 開發時咱們都知道能夠用模擬器和真機來測試咱們的項目,可是這兩個平臺的架構是不同的,模擬器是 i386 x86_64 架構的,而咱們的設備是 armv7 arm64 架構的。當在製做靜態連接庫的時候也要針對不一樣的架構製做出針對真機和模擬器的兩個靜態連接庫,而當咱們想在本身的項目中使用靜態連接庫的時候,若是在模 擬器上運行咱們要用針對模擬器的靜態庫版本,用真機設備測試的時候還要切換到針對真機的靜態連接庫,這樣一來很是的麻煩。
前面說過了靜態連接庫就是個壓縮包,那麼咱們是否能將這兩個靜態連接庫壓縮成一個靜態連接庫這樣就能夠同時支持模擬器和真機設備兩種架構了呢?答案是確定的。好比咱們手頭有一個靜態連接庫的兩個架構版本:libXXX.i386_x86_64.a
和libXXX.armv7_arm64.a
,那麼咱們能夠經過以下命令來生成一個統一的靜態連接庫:
1 |
lipo -create libXXX.i386_x86_64.a libXXX.armv7_arm64.a -output libXXX.a |
這樣咱們就獲得了一個統一版本的靜態庫libXXX.a
,它的好處是同時支持模擬器架構和真機設備架構,缺點是它的體積變大了,也就是說它很肥(fat)。
而libWeiboSDK.a
就是這麼一個合體後的靜態庫,咱們照樣能夠經過命令來驗證這一點:
1 |
lipo -info libWeiboSDK.a |
這個命令會輸出:
1 |
Architectures in the fat file: libWeiboSDK.a are: armv7 arm64 i386 x86_64 |
既然是個胖子,那咱們就要先給它瘦身才能解壓。咱們隨便從裏面抽出一個架構的靜態連接庫來,瘦身命令是:
1 |
lipo -thin i386 libWeiboSDK.a -output libWeiboSDK.i386.a |
這樣咱們就把針對 i386 平臺的新浪微博 SDK 靜態連接庫給抽離出來了,咱們管它叫libWeiboSDK.i386.a
,如今咱們再用ar
命令解壓它看看裏面有什麼
1 |
ar -x libWeibo.i386.a |
解壓完成後你會看到好多好多以.o
結尾的對象文件,回憶回憶剛剛咱們講到的編譯連接過程,這些對象文件就是給連接器
最終生成靜態連接庫時用到的文件,因爲太多了,我只列出咱們要講到的幾個:
1 2 3 4 5 6 7 8 9 |
-rw-r--r-- 1 leenjewel staff 13K Jan 8 15:47 NSData+WBSDKBase64.o -rw-r--r-- 1 leenjewel staff 42K Jan 8 15:47 UIImage+WBSDKResize.o -rw-r--r-- 1 leenjewel staff 12K Jan 8 15:47 UIImage+WBSDKStretch.o -rw-r--r-- 1 leenjewel staff 74K Jan 8 15:47 UIView+WBSDKSizes.o -rw-r--r-- 1 leenjewel staff 58K Jan 8 15:47 WBAidManager.o -rw-r--r-- 1 leenjewel staff 15K Jan 8 15:47 WBAuthorizeRequest.o -rw-r--r-- 1 leenjewel staff 16K Jan 8 15:47 WBAuthorizeResponse.o -rw-r--r-- 1 leenjewel staff 19K Jan 8 15:47 WBBaseMediaObject.o -rw-r--r-- 1 leenjewel staff 265K Jan 8 15:47 WBSDKJSONKit.o |
當咱們把新浪微博 SDK 的靜態連接庫引入咱們本身的項目,並 Build 咱們本身的項目到模擬器或真機設備上運行的過程其實也是一個編譯連接的過程,最終從項目 Build 生成能夠在模擬器或真機設備運行的 App,而這個過程當中對新浪微博 SDK 的靜態連接庫的處理方式和咱們剛剛拆開libWeiboSDK.a
的過程差很少:
將 libWeibSDK.a 根據當前所構建的平臺架構(模擬器仍是真機設備)進行瘦身
將瘦身的靜態庫解壓拆包
將用到的對象文件連接進入項目
而咱們遇到的崩潰問題偏偏是出在了將用到的對象文件連接進入項目
這一步。
蘋果的開發者網站針對這個問題有一篇說明文章,咱們來引用一下里面的內容:
The dynamic nature of Objective-C complicates things slightly. Because the code that implements a method is not determined until the method is actually called,
這句話解釋起來就是說 Objective-C 是有運行時(runtime)的,一個方法要執行什麼代碼是在運行時決定的,而不是在連接時決定的。想要再深刻了解 Objective-C 運行時知識的,能夠看看這裏
Objective-C does not define linker symbols for methods. Linker symbols are only defined for classes.
由於在 Objective-C 中,一個方法的執行是要到運行時才決定的,因此在連接時,連接器只連接類的符號,並不會連接方法的符號。
For example, if main.m includes the code [[FooClass alloc] initWithBar:nil]; then main.o will contain an undefined symbol for FooClass, but no linker symbols for the -initWithBar: method will be in main.o
最後還舉了一個例子:當你在main.m
文件中初始化一個類FooClass
的對象,而後調用了這個類FooClass
的一個對象方法initWithBar
,在連接器分析由main.m
編譯生成的main.o
對象文件時,發現這個對象文件沒有定義符號FooClass
因而就會去其餘.o
對象文件中去尋找FooClass
符號的定義,而至於方法符號initWithBar
的定義在哪裏連接器是不關心的,由於initWithBar
的執行是由運行時負責的,連接器無論。
好了,如今問題來了,咱們再重複一下這句話:
1 |
Objective-C 中方法的執行實在運行時決定的,因此連接器只連接類的符號,不連接方法的符號 |
咱們再回過頭看看崩潰的報錯信息:
1 |
[__NSDictionaryM weibosdk_WBSDKJSONString] : unrecognized selector sent to instance 0x170255780 |
這說明崩潰的緣由是在運行時調用__NSDictionaryM
類對象的weibosdk_WBSDKJSONString
方法時沒有找到該方法的定義。這裏不難看出__NSDictionaryM
是Foundation Framework
中的類,而方法weibosdk_WBSDKJSONString
是新浪微博 SDK 本身定義的方法,新浪在這裏使用了分類技術擴展了__NSDictionaryM
類的行爲。咱們來驗證這一點:
咱們已經解壓出libWeiboSDK.a
中的所有.o
對象文件,咱們用nm
命令導出所有對象文件中的符號:
1 |
nm *.o >> libWeiboSDK.symbols.txt |
而後咱們用個文本編輯器打開libWeiboSDK.symbols.txt
查找weibosdk_WBSDKJSONString
,咱們能夠查到以下結果:
1 2 3 4 |
WBSDKJSONKit.o: 00007ba0 t -[NSArray(WBSDKJSONKitSerializing) weibosdk_WBSDKJSONString] 00007de8 t -[NSDictionary(WBSDKJSONKitSerializing) weibosdk_WBSDKJSONString] 000079cd t -[NSString(WBSDKJSONKitSerializing) weibosdk_WBSDKJSONString] |
這就能夠說明新浪微博 SDK 確實使用了分類技術擴展了NSArray
、NSDictionary
和NSString
三個 Foundation Framework 下面的類的行爲。好,如今能夠真相大白了:
在連接時,連接器發現WBSDKJSONKit.o
對象文件中缺乏類符號NSArray
、NSDictionary
和NSString
。
連接器從Foundation Framework
中找到了類的符號定義,從而將Foundation Framework
中相關的對象文件連接進來
因爲連接器不連接方法符號,因此weibosdk_WBSDKJSONString
這樣的方法符號徹底被忽略了。
因爲類符號的定義在Foundation Farmework
中定義,因此WBSDKJSONKit.o
對象文件中沒有符號被引用,連接器就沒有把這個對象文件連接進來。
運行時運行到weibosdk_WBSDKJSONString
方法時,因爲Foundation Framework
中是不存在這個方法的定義的,而存在這個方法定義的WBSDKJSONKit.o
對象文件又沒有被連接器連接進來,因此崩潰了。
咱們繼續引用蘋果的開發者網站針對這個問題的說明文章中的內容:
Passing the -ObjC option to the linker causes it to load all members of static libraries that implement any Objective-C class or category. This will pickup any category method implementations. But it can make the resulting executable larger, and may pickup unnecessary objects. For this reason it is not on by default.
加了-ObjC
選項後,不論是否被引用到,連接器會把 Objective-C 的類和分類的全部對象文件所有連接,所有連接後方法符號所有被連接進來,崩潰的問題天然被解決了。
而-all_load
選項更完全,這個選項會讓連接器把所有的對象文件都連接進來,固然,代價就是構建的 APP 體積會變大。
其實準確的說法是編譯能夠成功進行,連接器執行報錯。咱們再回顧一下加了-ObjC
或-all_load
連接選項後連接器的報錯信息:
1 2 3 4 5 6 7 8 |
Undefined symbols for architecture i386: "_GCControllerDidConnectNotification", referenced from: -[GCControllerConnectionEventHandler observerConnection:disconnection:] in libcocos2dx iOS.a(CCController-iOS.o) "_GCControllerDidDisconnectNotification", referenced from: -[GCControllerConnectionEventHandler observerConnection:disconnection:] in libcocos2dx iOS.a(CCController-iOS.o) "_OBJC_CLASS_$_GCController", referenced from: objc-class-ref in libcocos2dx iOS.a(CCController-iOS.o) (maybe you meant: _OBJC_CLASS_$_GCControllerConnectionEventHandler) |
根據報錯信息咱們可以瞭解到報錯是一個名叫CCController-iOS.o
對象文件致使的,而這個文件對應的源代碼是CCController-iOS.mm
,經過閱讀源碼咱們發現,這個文件中定義了一個 Objective-C 的類GCControllerConnectionEventHandler
,這個類中的方法引用了GCControllerDidConnectNotification
和GCControllerDidDisconnectNotification
兩個類,而這兩個類實在GameController Framework
中定義的。
而 cocos2d-x 生成的項目默認並無爲咱們引入GameController Framework
,因此在連接時因爲連接器找不到對應類的符號定義,因此纔會報錯。若是你到 Xcode->Target->Buid Phases-> 下 Link Binary With Libraries 項添加GameController Framework
就能夠解決問題了,可是這種解決方式很不乾淨
-force_load path/to/your/libWeiboSDK.a
連接選項實際上是幹了和-ObjC
、-all_load
同樣的事情,只不過它更有針對性,它只讓連接器把你指定的靜態連接庫中的所有對象文件連接進來,這樣更清爽一些。
但願個人解釋已經夠深刻了。
:–)