問題雜記:友盟分享 SDK 和 load 載入耗時統計衝突問題

Jay_zz發表於2020-04-04

前言

前幾天早上,當我準備高高興興,投入世界上最為幸福的事情 -- 打碼時,收到了測試小姐姐的一個訊息。在那一瞬間,我有了一種不妙的預估,開啟訊息一看,不出所料,果然是你, bug 小怪獸。

問題描述

測試小姐姐說,這個版本,整個 App 的所有分享,突然都不能分享。我收到訊息時,是一臉懵逼的,App 的原生分享功能元件從一開始就有了,已經有 n 個版本沒有修改過分享的功能元件,怎麼就突然就不能分享了呢。測試小姐姐是不是下載錯版本了,自己直接跑了下開發版本除錯下,嗯,沒錯,就是測試小姐姐下載錯版本了 ........ 然而開發版本也是分享不了的。點選分享微信按鈕時,會提示“請先安裝微信客戶端”。

接下來,就是振奮人心的打倒小怪物環節。

問題查詢

分析小怪物的型別,屬於哪一個品種的,和人類是否有生殖隔離

首先,根據提示,搜尋了下 “請先安裝微信客戶端”,發現是由於呼叫友盟的 [UMSocialManager isInstall:] 判斷是否安裝微信時,友盟相關的介面返回了 false,所以業務判斷未安裝微信,就彈出了相關的提示。我嘗試了下不判斷是否安裝微信,直接呼叫友盟的分享介面進行分享,也是無法正常分享的。由於和友盟 SDK 相關,所以進行了一下的問題排查。

  • 開啟友盟 SDK 的 Log 列印,檢視 SDK 內部的日誌情況如下圖

image.png

從日誌中,可以看到友盟各個分項平臺的初始化都失敗了,友盟相關的問題文章中,也給出了 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 方法

image.png

  • 是否是由於 hook 友盟的 +load 方法,導致原有友盟內部的 +load 方法沒有被執行到,最終導致了分享異常

如何判斷友盟的原有的 +load 方法有沒有被執行呢?以 [UMSocialManager load] 方法為例,在 Xcode 上新增一個相關Symbolic Breakpoint 斷點,然後重新執行檢視斷點的執行情況,結果如下圖所示:

image.png

可以發現 [UMSocialManager load] 斷點是可以被斷點到的,相關的彙編程式碼也是有被執行的。而且從左邊的呼叫堆疊中,可以發現 [UMSocialManager load] 方法是被我們 hookLoad 方法所呼叫的。因此可以判斷,友盟中原有的 +load 方法,是有被執行的(其他幾個類,我也一一校驗過了)。

  • 是否是 hook 友盟的 +load 方法,導致原有的 +load 方法中的相關程式碼執行邏輯出現差異,最終導致友盟分享失敗。

上一步驗證友盟的 +load 方法是否已經被執行時,已經初略的瀏覽了各個方法的實現,涉及到的相關彙編命令都不多,比較好閱讀。通過斷點一步步除錯,對比 hook 前後 +load 方法執行的邏輯,最終發現了不一致的地方。如下圖所示:

image.png
image.png

從上面的 2 張圖片,我們可以很輕易的看了差別。那麼,為什麼暫存器 x19 的值會前後不一致呢?暫存器 x0 和 x19 中的值分別代表了什麼?我們分析 <+40> cmp x0, x19 之前的指令,就可以很容易的就得出結論:

  1. 暫存器 x19 裡面儲存的是一個self,在 <+16>: mov x19, x0 時被賦值
  2. 暫存器 x0 裡面儲存的是 [UMSocialHandler class] 方法的返回結果,相關的彙編指令是 <+24> <+32> <+36>

因此可以判斷出,這裡要執行的邏輯是判斷 self == [UMSocialHandler class],那麼,為什麼 hook 前 self 指向的值是 UMSocialQQHandler 而 hook 後指向的確是 UMSocialHandler。通過左邊的呼叫堆疊,我們可以發現此時的 [UMSocialHandler load] 並不是系統預設的呼叫行為而執行,而是子類 UMSocialQQHandler 通過 [super load] 呼叫而執行。如下圖所示:

image.png

由於是通過 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的第一個引數,就可以完美解決。

參考資料

計算 +load 方法的耗時iOS:關於super 關鍵字(使用runtime分析)

相關文章