最近在作無痕埋點相關的事情,須要對用戶的操做進行插樁進行上報,其餘事件都還好說,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)
複製代碼