利用isa-swizzling hook UITableViewCell的點擊事件

最近在作無痕埋點相關的事情,須要對用戶的操做進行插樁進行上報,其餘事件都還好說,cell點擊事件遇到了點問題,最初的想法是hook UITableViewCell的setSelected(_ selected: Bool, animated: Bool)方法。swift

可是此方法有2個問題:bash

  1. 不太好獲取cell所在的位置
  2. 即便UITableView的代理方法沒實現didSelectRowAtIndexPath方法,也會上報埋點

後來再與同事的討論中迸發出來一個想法,可否利用KVO中用到的isa-swizzling進行hookUITableViewCell的點擊,這個場景和KVO的場景其實差很少,KVO是對某個值觀察,當值改變的時候,調用某個固定的方法,而我如今的需求是對UITableViewCell的點擊進行觀察,當點擊的時候,調用咱們上報埋點的方法閉包

簡單介紹下KVO的原理:

  1. 當某個類的屬性被觀察時,系統會在運行時動態的建立一個該類的子類。而且把改對象的isa指向這個子類函數

  2. 假設被觀察的屬性名是name,若父類裏有setName:或這_setName:,那麼在子類裏重寫這2個方法,若2個方法同時存在,則只會重寫setName:一個(這裏和KVCset時的搜索順序是同樣的)ui

  3. 若被觀察的類型是NSString,那麼重寫的方法的實現會指向_NSSetObjectValueAndNotify這個函數,如果Bool類型,那麼重寫的方法的實現會指向_NSSetBoolValueAndNotify這個函數,這個函數裏會調用willChangeValueForKey:didChangevlueForKey:,而且會在這2個方法調用之間,調用父類set方法的實現this

  4. 系統會在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

  5. 當使用KVC賦值的時候,在NSObject裏的setValue:forKey:方法裏,若父類不存在setName:或這_setName:這些方法,會調用_NSSetValueAndNotifyForKeyInIvar這個函數,這個函數裏一樣也會調用willChangeValueForKey:didChangevlueForKey:,若存在則調用代理

hook Cell的點擊事件步驟以下:

注:生成子類類型的名字的規則爲當前的類名+"_sub_czb_tableview_delegate_analysis"指針

  1. hook UITableView的setDelegate方法
  2. 在setDelegate方法中判斷要設置delegate是否爲nil或者delegate是否沒實現了tableView:didSelectRowAtIndexPath:方法
  3. 如果則設置UITableView的delegate並結束,不然進行下一步
  4. 判斷當前類的類名是否知足生成子類類型的規則,如果則設置UITableView的delegate並結束,不然進行下一步
  5. 判斷須要生成的子類類型是否已經註冊過,若沒註冊過跳到第7不,不然進行下一步
  6. 若註冊過,將delegate的isa指向已經註冊過的子類類型,而後設置UITableView的delegate,結束
  7. 建立一個delegate類型的子類,並註冊
  8. 爲此子類添加一個與tableView點擊事件代理同名的方法,並在此方法中調用父類此方法的實現
  9. 將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)
        }
    }
}

複製代碼

其餘收穫:

  1. @convention的使用code

    • @convention(swift) : 代表這個是一個swift的閉包
    • @convention(block) :代表這個是一個兼容oc的block的閉包
    • @convention(c) : 代表這個是兼容c的函數指針的閉包
  2. 在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_implementationWithBlockunsafeBitCast,例:

      let block = unsafeBitCast(czb_didSelectRow, to: AnyObject.self)
      let imp = imp_implementationWithBlock(block)
      複製代碼
相關文章
相關標籤/搜索