在這個數據爲王的時代,市面有用戶的 APP,都會進行日誌打點,咱們也不例外。git
若是一個個頁面去打點,實在費時費力,咱們難免想經過 AOP 方式去 Hook 咱們想要的方法,就能作到一次打點,統一管理的目的了。github
好比對於一個頁面的進出,只須要對 viewWillAppear
和 viewWillDisappear
作記錄。objective-c
有鑑於此,iOS 上對於 UIKit ,咱們項目有了一套基於 Method Swizzle
實現的的事件打點方案。安全
然而咱們發現了一個問題:bash
加入某一個第三方庫,進行使用出現了崩潰,通過排查,肯定到和父子類初始化順序有關。app
先簡單的說一下咱們打點追蹤的 Method Swizzle
方案。ide
咱們在 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];
...
}
複製代碼
方法交換的關鍵代碼,統一使用到的方法 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 類,如 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 類,對於 UITableView
和 UICollectionView
,想要統計它們的點擊事件,就不能夠對其類直接進行交換。由於它的對應的事件實現,是在 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
複製代碼
這裏的思路是:
上述對於特定的 UI 類, UITableView
和 UICollectionView
的代理對象作 swizzle 。乍看是是並無問題的,而且穩定運行了好久。
直到有一天,咱們引入一個第三方庫。
這個第三方是一個 UIView ,不過這個 UIView 是一個 UICollectionView 的 delegate 對象.
這自己也並沒有問題。
而出於業務須要,咱們繼承了它,實現了一個子類。子類中也沒有進行 UICollectioView 的代理方法覆寫。
這個時候,就出現了循環調用的問題。
排查發現,只要父類先於子類進行交換的操做,以後點擊子類,就會發生循環調用,致使崩潰。
咱們來複原整個問題的流程:
1.按照上文方案,父類進行 swizzle ,結果爲:
SEL | OriginSel | NewSel |
---|---|---|
IMP | NewImp | 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 直接比對:
核心代碼以下:
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 .