前言
前幾天早上,當我準備高高興興,投入世界上最為幸福的事情 -- 打碼時,收到了測試小姐姐的一個訊息。在那一瞬間,我有了一種不妙的預估,開啟訊息一看,不出所料,果然是你, bug 小怪獸。
問題描述
測試小姐姐說,這個版本,整個 App 的所有分享,突然都不能分享。我收到訊息時,是一臉懵逼的,App 的原生分享功能元件從一開始就有了,已經有 n 個版本沒有修改過分享的功能元件,怎麼就突然就不能分享了呢。測試小姐姐是不是下載錯版本了,自己直接跑了下開發版本除錯下,嗯,沒錯,就是測試小姐姐下載錯版本了 ........ 然而開發版本也是分享不了的。點選分享微信按鈕時,會提示“請先安裝微信客戶端”。
接下來,就是振奮人心的打倒小怪物環節。
問題查詢
分析小怪物的型別,屬於哪一個品種的,和人類是否有生殖隔離
首先,根據提示,搜尋了下 “請先安裝微信客戶端”,發現是由於呼叫友盟的 [UMSocialManager isInstall:]
判斷是否安裝微信時,友盟相關的介面返回了 false,所以業務判斷未安裝微信,就彈出了相關的提示。我嘗試了下不判斷是否安裝微信,直接呼叫友盟的分享介面進行分享,也是無法正常分享的。由於和友盟 SDK 相關,所以進行了一下的問題排查。
- 開啟友盟 SDK 的 Log 列印,檢視 SDK 內部的日誌情況如下圖
從日誌中,可以看到友盟各個分項平臺的初始化都失敗了,友盟相關的問題文章中,也給出了 2 種檢查方案,嘗試後都沒有解決問題。
- 查下友盟的相關版本,看是否是版本導致的問題。
出現問題的友盟 SDK 的版本為 6.9.6,而截至到 4月3日,友盟官網上最新的 SDK 的版本是 6.9.8。查了下更新文件,主要解決的是微信進行要求的 Universal Link 的相關配置。雖然感覺應該沒有關係,但抱著嘗試的態度,還是把 SDK 更新到最新版本 6.9.8。經過測試後,問題還是無法解決。
- 回滾到上個版本,對比 2 個版本之間的程式碼差異。
回滾到上個可用版本後,發現友盟的分享功能(SDK 版本是 6.9.6)是可以正常使用的。因此說明是由於最新版本新增的相關程式碼,導致了友盟 SDK 的分享出錯問題。
經過上面 3 個方面檢查後,最終確定了是由於最新版本引入的程式碼,導致友盟分享失敗問題。由於最新版本的確沒有改動過和友盟相關的程式碼,所以只能通過二分法來縮小程式碼訪問,最終,確定了是由於最新版本引入的一個內部效能監控庫
,導致友盟分享功能失效。在詳細定位效能監控庫
中的功能,最後發現,是 效能監控庫
中統計 App 啟動時所有 +load
方法耗時的程式碼,導致了友盟分享失效。
根因查詢
緊接著,悄咪咪的查詢小怪物的弱點,追求一招致勝。這樣普通人才會覺得你很帥,畢竟帥是一輩子的事情。
經過縮小程式碼的修改範圍,最終確定了是統計+load
方法耗時的相關程式碼,導致友盟分享失效。到了這裡,其實還是非常的疑惑,因為這 2 部分的程式碼,基本是沒啥關係的,為啥會相互影響呢。詢問了之前負責這部分程式碼的相關同事,瞭解到統計+load
方法耗時的實現,是參考了計算 +load 方法的耗時 。詳細的實現方式可以連結,大概的實現思路如下:
在所有的動態庫載入前, hook 所有類的 +load 方法,然後在 +hookLoad 方法的前後,插入相關的時間統計方法。最後將所有 +load 方法的耗時累加得到總時間。
由於是 hook 了專案中所有的 +load
方法,那就只能猜測,是不是友盟 SDK 內部有相關的類也實現了 +load
方法,並且在方法中做了某些處理?。依照這個思路,做了一下的操作。
- 檢視友盟 SDK 是否有實現
+load
方法,是哪個類實現了+load
方法。
在 hook +load
方法 的相關程式碼中,將進行 hook 的類都列印出來,由於友盟 SDK 中的類名都是以 UM 作為字首,所以很好辨認,最終發現了以下相關類實現了 +load
方法
- 是否是由於 hook 友盟的
+load
方法,導致原有友盟內部的+load
方法沒有被執行到,最終導致了分享異常
如何判斷友盟的原有的 +load
方法有沒有被執行呢?以 [UMSocialManager load]
方法為例,在 Xcode 上新增一個相關Symbolic Breakpoint
斷點,然後重新執行檢視斷點的執行情況,結果如下圖所示:
可以發現 [UMSocialManager load]
斷點是可以被斷點到的,相關的彙編程式碼也是有被執行的。而且從左邊的呼叫堆疊中,可以發現 [UMSocialManager load]
方法是被我們 hookLoad 方法所呼叫的。因此可以判斷,友盟中原有的 +load
方法,是有被執行的(其他幾個類,我也一一校驗過了)。
- 是否是 hook 友盟的
+load
方法,導致原有的+load
方法中的相關程式碼執行邏輯出現差異,最終導致友盟分享失敗。
上一步驗證友盟的 +load
方法是否已經被執行時,已經初略的瀏覽了各個方法的實現,涉及到的相關彙編命令都不多,比較好閱讀。通過斷點一步步除錯,對比 hook 前後 +load
方法執行的邏輯,最終發現了不一致的地方。如下圖所示:
從上面的 2 張圖片,我們可以很輕易的看了差別。那麼,為什麼暫存器 x19 的值會前後不一致呢?暫存器 x0 和 x19 中的值分別代表了什麼?我們分析 <+40> cmp x0, x19
之前的指令,就可以很容易的就得出結論:
- 暫存器 x19 裡面儲存的是一個
self
,在<+16>: mov x19, x0
時被賦值 - 暫存器 x0 裡面儲存的是
[UMSocialHandler class]
方法的返回結果,相關的彙編指令是<+24> <+32> <+36>
因此可以判斷出,這裡要執行的邏輯是判斷 self == [UMSocialHandler class]
,那麼,為什麼 hook 前 self
指向的值是 UMSocialQQHandler
而 hook 後指向的確是 UMSocialHandler
。通過左邊的呼叫堆疊,我們可以發現此時的 [UMSocialHandler load]
並不是系統預設的呼叫行為而執行,而是子類 UMSocialQQHandler
通過 [super load]
呼叫而執行。如下圖所示:
由於是通過 super
進行呼叫,所以在父類的 +load
方法中,self
指向的是 UMSocialQQHandler
而不是 UMSocialHandler
,具體細節可以參考iOS:關於super 關鍵字(使用runtime分析)。那為啥 hook +load
方法後,self
指向的是UMSocialHandler
呢,這就得詳細檢視了下相關程式碼,如下所示:
static void swizzleLoadMethod(Class cls, Method method, LMLoadInfo *info) {
retry:
do {
SEL hookSel = getRandomLoadSelector();
Class metaCls = object_getClass(cls);
IMP hookImp = imp_implementationWithBlock(^(){
info->_start = CFAbsoluteTimeGetCurrent();
((void (*)(Class, SEL))objc_msgSend)(cls, hookSel);
info->_end = CFAbsoluteTimeGetCurrent();
if (!--LMAllLoadNumber) printLoadInfoWappers();
});
BOOL didAddMethod = class_addMethod(metaCls, hookSel, hookImp, method_getTypeEncoding(method));
if (!didAddMethod) goto retry;
info->_nSEL = hookSel;
Method hookMethod = class_getInstanceMethod(metaCls, hookSel);
method_exchangeImplementations(method, hookMethod);
} while(0);
}
複製程式碼
關鍵程式碼是 imp_implementationWithBlock
引數中的 ((void (*)(Class, SEL))objc_msgSend)(cls, hookSel);
,可以看到 objc_msgSend
的第一引數,也就是最終在 +load
方法中獲取到的 self
的值,是被寫死成了被 hook 的類。舉個例子,在 hook UMSocialHandler
的 +load
方法時,cls
指向的是 UMSocialHandler
,而 cls
被賦值給了 objc_msgSend
的第一個引數,所以在 UMSocialHandler
中的 self
永遠都是指向 UMSocialHandler
。這也是導致分享失效的根本原因,由於 self
永遠都是指向 UMSocialHandler
,所以導致了 UMSocialHandler
的 +load
中的一部分邏輯永遠不會被執行到,最終導致了分享平臺初始化失敗,也就是盟友日誌列印的相關錯誤提示。
解決方案
最後,拿出我珍藏多年的寶劍,輕輕一捅。事了拂衣去,不帶走一片雲彩。
找到問題,解決方案就不難想出。問題的根因,是由於 hook 導致的 self
指向錯誤。因此,只要將原有的self
重新賦值回去就可以解決了,相關程式碼如下:
static void swizzleLoadMethod(Class cls, Method method, LMLoadInfo *info) {
retry:
do {
SEL hookSel = getRandomLoadSelector();
Class metaCls = object_getClass(cls);
IMP hookImp = imp_implementationWithBlock(^(id originSelf){
info->_start = CFAbsoluteTimeGetCurrent();
((void (*)(Class, SEL))objc_msgSend)(originSelf, hookSel);
info->_end = CFAbsoluteTimeGetCurrent();
if (!--LMAllLoadNumber) printLoadInfoWappers()`;
});
BOOL didAddMethod = class_addMethod(metaCls, hookSel, hookImp, method_getTypeEncoding(method));
if (!didAddMethod) goto retry;
info->_nSEL = hookSel;
Method hookMethod = class_getInstanceMethod(metaCls, hookSel);
method_exchangeImplementations(method, hookMethod);
} while(0);
}
複製程式碼
可以看到,修改的內容真的非常的少,就是取出 hookBlock
的第一個引數,然後賦值給 objc_msgSend
的第一個引數,就可以完美解決。