Method Swizzle 打點 UIKit 的問題

問題背景

在這個數據爲王的時代,市面有用戶的 APP,都會進行日誌打點,咱們也不例外。git

若是一個個頁面去打點,實在費時費力,咱們難免想經過 AOP 方式去 Hook 咱們想要的方法,就能作到一次打點,統一管理的目的了。github

好比對於一個頁面的進出,只須要對 viewWillAppearviewWillDisappear 作記錄。objective-c

有鑑於此,iOS 上對於 UIKit ,咱們項目有了一套基於 Method Swizzle 實現的的事件打點方案。安全

然而咱們發現了一個問題:bash

加入某一個第三方庫,進行使用出現了崩潰,通過排查,肯定到和父子類初始化順序有關。app

Method Swizzle 方案

先簡單的說一下咱們打點追蹤的 Method Swizzle 方案。ide

隨着 App 啓動開始作方法交換

咱們在 APP 啓動時,在 TrackerCenter 中開始作方法交換:函數

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.
    [[HZTrackerCenter sharedInstance] beginTracker];
    return YES;
}

複製代碼

beginTracker 中各個 UI 類的方法作交換:測試

- (void)beginTracker
{
    ...
    [UIControl HZ_swizzle];
    [UICollectionView HZ_swizzle];
    ...
}

複製代碼

Method Swizzle 的統一方法

方法交換的關鍵代碼,統一使用到的方法 HZ_swizzleMethod:newSel: ,也是市面常見的代碼,以下:ui

+ (BOOL)HZ_swizzleMethod:(SEL)originalSel newSel:(SEL)newSel {
    Method originMethod = class_getInstanceMethod(self, originalSel);
    Method newMethod = class_getInstanceMethod(self, newSel);
    
    if (originMethod && newMethod) {
        if (class_addMethod(self, originalSel, method_getImplementation(newMethod), method_getTypeEncoding(newMethod))) {
            
            IMP orginIMP = method_getImplementation(originMethod);
            class_replaceMethod(self, newSel, orginIMP, method_getTypeEncoding(originMethod));
        } else {
            method_exchangeImplementations(originMethod, newMethod);
        }
        return YES;
    }
    return NO;
}
複製代碼

思路是:

1.根據 SEL 取得兩個 Method,判斷兩個 Method 是否存在,是否能夠進行交換

2.使用 class_addMethod() ,若是類中沒有實現 originalSel 對應的方法,那就先添加 Method . 若是本類中包含一個同名的實現,則函數會返回NO,這裏就會直接使用 method_exchangeImplementations 對兩個方法進行交換。

3.當 class_addMethod() 成功,則表示 originalSel 的 IMP,已經爲 newMethod 的實現。下一步則使用 class_replaceMethod 對 newSel 進行 IMP 的替換。

note: 而爲何不直接使用 method_exchangeImplementations, 而是先添加再交換,是爲了保證只在子類中交換方法,不影響父類。 若是本類中沒有 originalSel 的實現,class_getInstanceMethod() 返回的是某父類 Method 對象,直接交換的後果,會把父類的 IMP 跟這個類的 Swizzle IMP 交換。影響到整個父類和其子類。

不瞭解 SEL,Method,IMP ,建議能夠看一看這篇 Objective-C Runtime

不一樣的類,方法交換的策略

普通 UI 類,直接操做

對於普通的 UI 類,如 UIControl , 咱們直接就進行交換了,以下:

@implementation UIControl (Tracker)

- (void)HZ_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
 
    if (target && action && ![NSStringFromSelector(action) hasPrefix:@"_"]) {
        //進行事件記錄
    }
    
    [self HZ_sendAction:action to:target forEvent:event];
}

+ (void)HZ_swizzle {
    [UIControl HZ_swizzleMethod:@selector(sendAction:to:forEvent:)
                         newSel:@selector(ET_sendAction:to:forEvent:)];
}

@end


複製代碼

特定 UI 類,對代理對象操做

不一樣於其它 UI 類,對於 UITableViewUICollectionView ,想要統計它們的點擊事件,就不能夠對其類直接進行交換。由於它的對應的事件實現,是在 delegate 對象中。

因而在 setDelegate: 的時機,對 delegate 對象 進行操做。

例如 UICollectionView

@implementation UICollectionView (Tracker)

- (void)HZ_setDelegate:(id<UICollectionViewDelegate>)delegate {
    if ([delegate isKindOfClass:[NSObject class]]) {
        SEL sel = @selector(collectionView:didSelectItemAtIndexPath:);
        
        //newSel 名: HZ_collectionView:didSelectItemAtIndexPath:
        SEL newSel = [NSObject HZ_newSelFormOriginalSel:sel];
        
        Method originMethod = class_getInstanceMethod(delegate.class, sel);
          
        if (originMethod && ![delegate.class HZ_methodHasSwizzed:sel]) {

        
            IMP newIMP =  (IMP)HZ_collectionViewDidSelectRowAtIndexPath;
            class_addMethod(delegate.class, newSel,newIMP, method_getTypeEncoding(originMethod));
            
            [delegate.class HZ_swizzleMethod:sel newSel:newSel];
            [delegate.class HZ_setMethodHasSwizzed:sel];
        }
    }
    [self HZ_setDelegate:delegate];
}

+ (void)HZ_swizzle {
    [UICollectionView HZ_swizzleMethod:@selector(setDelegate:)
                                newSel:@selector(HZ_setDelegate:)];
}

@end
複製代碼

這裏的思路是:

  1. 根據 Sel 生成一個 newSel。
  2. 判斷 Sel 的 Method 存在,而且 sel 尚未被被 swizzle 過。
  3. 對 delegate 對象的類,增長 newSel 實現。
  4. 對 sel 和 newSel 進行交換
  5. 對已經 swizzle 到 Sel 標記

問題場景

上述對於特定的 UI 類, UITableViewUICollectionView 的代理對象作 swizzle 。乍看是是並無問題的,而且穩定運行了好久。

直到有一天,咱們引入一個第三方庫。

這個第三方是一個 UIView ,不過這個 UIView 是一個 UICollectionView 的 delegate 對象.

這自己也並沒有問題。

而出於業務須要,咱們繼承了它,實現了一個子類。子類中也沒有進行 UICollectioView 的代理方法覆寫。

這個時候,就出現了循環調用的問題。

出現問題的流程

排查發現,只要父類先於子類進行交換的操做,以後點擊子類,就會發生循環調用,致使崩潰。

咱們來複原整個問題的流程:

1.按照上文方案,父類進行 swizzle ,結果爲:

SEL OriginSel NewSel
IMP NewImp OriginImp
  • OriginSel 實現對應 NewImp
  • NewSel 實現對應 OriginImp

2.對子類進行 swizzle,先插入了 NewSel ,實現對應爲 NewImp :

SEL NewSel
IMP NewImp

3.交換方法中,進行了 class_addMethod,OriginImp 對應實現爲 NewImp:

SEL OriginSel NewSel
IMP NewImp NewImp

4.交換方法中,對 NewSel 進行 class_replaceMethod:

IMP orginIMP = method_getImplementation(originMethod);
class_replaceMethod(self, newSel, orginIMP,method_getTypeEncoding(originMethod));

複製代碼

在進行上面 步驟 4 的時候,問題就顯現了,由於子類中未實現 originalSel。而 originMethod 經過 class_getInstanceMethod(self, originalSel) 得來,獲取到的其實是父類 originalSel 的實現,看到上表,父類已經被交換,獲取的實現爲 NewImp.

因此這時候 replace 操做,並無達到真正的目的。

最後子類的結果仍然爲 :

SEL OriginSel NewSel
IMP NewImp NewImp

問題表現

到了這一步,開始還原發生循環的過程.

其中 NewImp 的實現爲一個用來打日誌的靜態方法:

void HZ_collectionViewDidSelectRowAtIndexPath(id self, SEL _cmd, UICollectionView *collectionView, NSIndexPath *indexPath) 
{
    
    //do your track thing
    
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
    
    SEL sel = [NSObject HZ_newSelFormOriginalSel:@selector(collectionView:didSelectItemAtIndexPath:)];
    [self performSelector:sel
               withObject:collectionView 
               withObject:indexPath];
#pragma clang diagnostic pop
}
複製代碼

上面的方法,最後會再調用 NewSel.

正常的調用鏈應該是:

OriginSel->NewImp->NewSel->OriginImp.

在 NewImp 中打點完成後,調用系統真正的 OriginImp,就結束了。

而咱們如今 Swizzle 後的子類,調用鏈是這樣的:

OriginSel->NewImp->NewSel->NewImp->NewSel->NewImp..

這樣就出現了 NewImp->NewSel->NewImp->NewSel..的俄羅斯套娃,因此引起了崩潰。

問題解決

通過上述分析,發生問題就在於,子類進入 Swizzle 後,子類自己沒有實現,OriginalMethod 使用 method_getImplementation 方法,是會拿父類的實現。父類已經交換,結果拿到的是 NewImp。

若是要解決問題,因爲沒法去控制使用者調用父子類的順序,咱們要在進行 Swizzle 前進行判斷,避免這樣的狀況發生。

初步解決方案

第一時間,我想到的是經過 OriginalSel 的實現進行判斷。

由於不管父子類, OriginalSel 的實現都是拿的父類中的,第二次去拿,會發生危險。

經過標誌位的真假來決定,是否進行 Swizzle:

  • 當父類 OriginalSel 進行過修改,子類再進來,就再也不進行 Swizzle 操做。

  • 當子類 OriginalSel 進行修改,父類進來,也再也不進行 Swizzle 操做。

但測試證實,若是再加上一個 孫子類,這時候又將發生問題。仍然和以前的相似,感興趣的能夠試驗一下。

最終解決方案

最後的方案,就是直接切入最核心的一點,判斷 OriginalIMP 和 NewIMP。

問題發生,就是在於 OriginalIMP 實際上變成了 NewIMP 。

那麼只要在 Swizzle 前,取出來 OriginalIMP 和 NewIMP 直接比對:

  • 若是相同,證實有父類實現已經進行過交換。則再也不作 Swizzle。
  • 不是的話,則能夠進行安全的 Sizzle 操做。

核心代碼以下:

IMP originIMP = method_getImplementation(originMethod);
IMP newIMP =  (IMP)HZ_collectionViewDidSelectRowAtIndexPath;

if (originMethod && !(originIMP==newIMP))
{      
    class_addMethod(delegate.class, newSel,newIMP, method_getTypeEncoding(originMethod));
    [delegate.class HZ_swizzleMethod:sel newSel:newSel];
}
複製代碼

通過這樣處理以後,咱們的日誌打點就能夠正常運行,不再用擔憂 父子重複 Swizzle 致使循環調用了。

相關的示例代碼,包括了問題和解決方案,都已經上傳到 GitHub .

相關文章
相關標籤/搜索