在这个数据为王的时代,市面有用户的 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 .