最近在做无痕埋点相关的事情,需要对用户的操作进行插桩进行上报,其他事件都还好说,cell点击事件遇到了点问题,最初的想法是hook UITableViewCell的setSelected(_ selected: Bool, animated: Bool)方法。
但是此方法有2个问题:
- 不太好获取cell所在的位置
- 即使UITableView的代理方法没实现didSelectRowAtIndexPath方法,也会上报埋点
后来再与同事的讨论中迸发出来一个想法,能否利用KVO中用到的isa-swizzling进行hookUITableViewCell的点击,这个场景和KVO的场景其实差不多,KVO是对某个值观察,当值改变的时候,调用某个固定的方法,而我现在的需求是对UITableViewCell的点击进行观察,当点击的时候,调用我们上报埋点的方法
简单介绍下KVO的原理:
-
当某个类的属性被观察时,系统会在运行时动态的创建一个该类的子类。并且把改对象的isa指向这个子类
-
假设被观察的属性名是
name,若父类里有setName:或这_setName:,那么在子类里重写这2个方法,若2个方法同时存在,则只会重写setName:一个(这里和KVCset时的搜索顺序是一样的) -
若被观察的类型是NSString,那么重写的方法的实现会指向
_NSSetObjectValueAndNotify这个函数,若是Bool类型,那么重写的方法的实现会指向_NSSetBoolValueAndNotify这个函数,这个函数里会调用willChangeValueForKey:和didChangevlueForKey:,并且会在这2个方法调用之间,调用父类set方法的实现 -
系统会在
willChangeValueForKey:对observe里的change[old]赋值,取值是用valueForKey:取值的,didChangevlueForKey:对observe里的change[new]赋值,然后调用observe的这个方法- (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change context:(nullable void *)context; -
当使用KVC赋值的时候,在NSObject里的
setValue:forKey:方法里,若父类不存在setName:或这_setName:这些方法,会调用_NSSetValueAndNotifyForKeyInIvar这个函数,这个函数里同样也会调用willChangeValueForKey:和didChangevlueForKey:,若存在则调用
hook Cell的点击事件步骤如下:
注:生成子类类型的名字的规则为当前的类名+"_sub_czb_tableview_delegate_analysis"
- hook UITableView的setDelegate方法
- 在setDelegate方法中判断要设置delegate是否为nil或者delegate是否没实现了tableView:didSelectRowAtIndexPath:方法
- 若是则设置UITableView的delegate并结束,否则进行下一步
- 判断当前类的类名是否满足生成子类类型的规则,若是则设置UITableView的delegate并结束,否则进行下一步
- 判断需要生成的子类类型是否已经注册过,若没注册过跳到第7不,否则进行下一步
- 若注册过,将delegate的isa指向已经注册过的子类类型,然后设置UITableView的delegate,结束
- 创建一个delegate类型的子类,并注册
- 为此子类添加一个与tableView点击事件代理同名的方法,并在此方法中调用父类此方法的实现
- 将delegate的isa指向刚刚创建的子类类型
代码如下:
typealias TableviewDidSelectRow = @convention(c) (NSObject, Selector, UITableView, IndexPath) -> Void
let czb_didSelectRow:@convention(block) (NSObject, UITableView, IndexPath) -> Void = {
(this, tableView, indexPath) in
let superClass: AnyClass? = this.superclass
let sel = NSSelectorFromString("tableView:didSelectRowAtIndexPath:")
let method = class_getInstanceMethod(superClass, sel)
if let impl = class_getMethodImplementation(superClass, sel) {
let fn = unsafeBitCast(impl, to: TableviewDidSelectRow.self)
fn(this, sel, tableView, indexPath)
}
}
extension UITableView {
static func enableAutoAnalysis () {
let originalSelector = NSSelectorFromString("setDelegate:")
let swizzledSelector = #selector(czb_setDelegate(_:))
/// 此方法是对对应的方法进行hook
swizzlingForClass(UITableView.classForCoder(),originalSelector: originalSelector,swizzledSelector: swizzledSelector)
}
@objc func czb_setDelegate(_ delegate: NSObject?) {
let sel = NSSelectorFromString("tableView:didSelectRowAtIndexPath:")
guard let delegate = delegate,delegate.responds(to: sel) else {
czb_setDelegate(nil)
return
}
var className = NSStringFromClass(delegate.classForCoder)
if className.hasSuffix("_sub_czb_tableview_delegate_analysis") {
czb_setDelegate(delegate)
return
}
className += "_sub_czb_tableview_delegate_analysis"
if let analysisClass = NSClassFromString(className) {
object_setClass(delegate, analysisClass)
czb_setDelegate(delegate)
return
}
if let customClass = objc_allocateClassPair(delegate.classForCoder, className, 0),
let method = class_getInstanceMethod(delegate.classForCoder, sel) {
objc_registerClassPair(customClass)
let type = method_getTypeEncoding(method)
let imp = imp_implementationWithBlock(unsafeBitCast(czb_didSelectRow, to: AnyObject.self))
class_addMethod(customClass, sel, imp, type)
object_setClass(delegate, customClass)
czb_setDelegate(delegate)
}else {
czb_setDelegate(delegate)
}
}
}
其他收获:
-
@convention的使用
- @convention(swift) : 表明这个是一个swift的闭包
- @convention(block) :表明这个是一个兼容oc的block的闭包
- @convention(c) : 表明这个是兼容c的函数指针的闭包
-
在Swift中如何把IMP转成func以及如何通过一个block创建一个IMP
-
如何把IMP转成func
通过typealias和@convention(c)声明一个和IMP相同参数的闭包,例:
typealias TableviewDidSelectRow = @convention(c) (NSObject, Selector, UITableView, IndexPath) -> Void利用unsafeBitCast函数转换,例:
let fn = unsafeBitCast(impl, to: TableviewDidSelectRow.self) -
如何通过一个block创建一个IMP
创一个用建@convention(block)修饰的闭包,例:
let czb_didSelectRow:@convention(block) (NSObject, UITableView, IndexPath) -> Void = { (this, tableView, indexPath) in ///实现代码 }利用
imp_implementationWithBlock和unsafeBitCast,例:let block = unsafeBitCast(czb_didSelectRow, to: AnyObject.self) let imp = imp_implementationWithBlock(block)
-