解決方法是:
-force_load path/to/your/libWeiboSDK.a 而不是 他提供的-ObjC、-all_load,下面是一些詳細說明
這裡特別給出示范路徑,比如你在項目中導入了XXX.a放在一個叫aaa的group文件下,那麼路徑就是aaa/xxx.a,或者你可以使用全路徑,點擊對應的xxx.a靜態庫,會在Xcode的右側出現該文件的路徑,把它復制過來就可以了
我的開發環境
- Mac OS X 10.10.1Xcode 6.1.1 (6A2008a)Cocos2d-x 3.2新浪微博 SDK for iOS 2015 年 1 月 5 日從githubclone 的版本遇到的問題
根據新浪微博 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而不是它自己文檔所提示的-ObjC。而且在同樣的開發環境下,我的 cocos2d-x 項目會崩潰,但是新浪微博 SDK 附帶的 Demo 可以正常工作,想必上述兩個解決方案應該是正解
但是在給自己的 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()方法了。
libWeiboSDK.a 靜態鏈接庫裡有什麼?
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 體積會變大。
為什麼 cocos2d-x 加了編譯選項會無法編譯通過?
其實准確的說法是編譯可以成功進行,鏈接器執行報錯。我們再回顧一下加了-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一樣的事情,只不過它更有針對性,它只讓鏈接器把你指定的靜態鏈接庫中的全部對象文件鏈接進來,這樣更清爽一些。