最近在作无痕埋点相关的事情,须要对用户的操做进行插桩进行上报,其余事件都还好说,cell点击事件遇到了点问题,最初的想法是hook UITableViewCell的setSelected(_ selected: Bool, animated: Bool)方法。swift
可是此方法有2个问题:bash
后来再与同事的讨论中迸发出来一个想法,可否利用KVO中用到的isa-swizzling进行hookUITableViewCell的点击,这个场景和KVO的场景其实差很少,KVO是对某个值观察,当值改变的时候,调用某个固定的方法,而我如今的需求是对UITableViewCell的点击进行观察,当点击的时候,调用咱们上报埋点的方法闭包
当某个类的属性被观察时,系统会在运行时动态的建立一个该类的子类。而且把改对象的isa指向这个子类函数
假设被观察的属性名是name
,若父类里有setName:
或这_setName:
,那么在子类里重写这2个方法,若2个方法同时存在,则只会重写setName:
一个(这里和KVCset时的搜索顺序是同样的)ui
若被观察的类型是NSString,那么重写的方法的实现会指向_NSSetObjectValueAndNotify
这个函数,如果Bool类型,那么重写的方法的实现会指向_NSSetBoolValueAndNotify
这个函数,这个函数里会调用willChangeValueForKey:
和didChangevlueForKey:
,而且会在这2个方法调用之间,调用父类set方法的实现this
系统会在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;
spa
当使用KVC赋值的时候,在NSObject里的setValue:forKey:
方法里,若父类不存在setName:
或这_setName:
这些方法,会调用_NSSetValueAndNotifyForKeyInIvar
这个函数,这个函数里一样也会调用willChangeValueForKey:
和didChangevlueForKey:
,若存在则调用代理
注:生成子类类型的名字的规则为当前的类名+"_sub_czb_tableview_delegate_analysis"指针
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的使用code
在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)
复制代码