美文网首页OC-开发案例收集
在Swift中使用method swizzling的一点心得

在Swift中使用method swizzling的一点心得

作者: 袁方 | 来源:发表于2019-10-17 19:29 被阅读0次

简介

method swizzling指方法替换.
每一个类内部都存在一个类似字典的列表. 方法名( SEL)为Key, 与其对应的Value则是指向该方法实现的指针(IMP). method swizzling可以在runtime时将SEL和IMP进行更换. 比如SELa1原来对应IMPa1, SELa2原来对应IMPa2. method swizzling后,SELa1就可以对应 IMPa2, SELa2就对应IMPa1.

警告⚠️

  • method swizzling一定要保证唯一性和原子性. 唯一性是指应该尽可能在 +load 方法中实现, 这样可以保证方法一定会调用且不会出现异常. 原子性是指使用 dispatch_once 来执行方法交换, 这样可以保证只运行一次. (Swift中的DispatchQueue并没有once的方法, 笔者自行添加了DispatchQueue.once方法的实现. )

  • 不要轻易使用 method swizzling. 因为动态交换方法实现并没有编译器的安全保证, 可能会在运行时造成奇怪的bug.

使用场景: 比如在版本开发末期, 增加了一项需求: 不改变原大小的前提下将App内所有UILabel的字体更换为某个新的字体. 如果最初动手开发时没有将字体写成动态获取, 那使用method swizzling是一个方便的解决方法.

解决思路: 将系统的setText:方法替换为我们自己实现的swizzled_setText(text:), 并在新方法内调整字体大小.

附实现

注: 以下代码需要写入UILabel的扩展(extension)里

public static func initializeMethod() {
    if self != UILabel.self {
        return
        
    }
    
    DispatchQueue.once {
        // UILabel的setText(:)方法属于setter方法, 无法直接进行调用或者替换. 所以直接写成 #selector(UILabel.setText(:))会报错. 
        // 对于这类成员变量setter方法的替换, 我们可以直接用setter方法对应的字符串去初始化我们想要的Selector对象
        let originalSelector = Selector("setText:")
        let swizzledSelector = #selector(UILabel.swizzled_setText(text:))
        
        let originalMethod = class_getInstanceMethod(self, originalSelector)
        let swizzledMethod = class_getInstanceMethod(self, swizzledSelector)
        // 在进行 Swizzling 的时候,需要用 class_addMethod 先进行判断一下原有类中是否有要替换方法的实现
        let didAddMethod: Bool = class_addMethod(self, originalSelector, method_getImplementation(swizzledMethod!), method_getTypeEncoding(swizzledMethod!))
        // 如果 class_addMethod 返回 yes,说明当前类中没有要替换方法的实现,所以需要在父类中查找,这时候就用到 method_getImplemetation 去获取 class_getInstanceMethod 里面的方法实现,然后再进行 class_replaceMethod 来实现 Swizzing
        
        if didAddMethod {
            class_replaceMethod(self, swizzledSelector, method_getImplementation(originalMethod!), method_getTypeEncoding(originalMethod!))
        } else {
            method_exchangeImplementations(originalMethod!, swizzledMethod!)
        }
        
    }
    
}

@objc func swizzled_setText(text: String) -> Void {
    self.swizzled_setText(text: text)

    self.font = UIFont.init(name: EnumSet.FontType.Normal.rawValue, size: self.font.pointSize)
    
}

注: 以下代码需要写入DispatchQueue的扩展(extension)里

private static var _onceTracker = [String]()

public class func once(file: String = #file,
                       function: String = #function,
                       line: Int = #line,
                       block: () -> Void) {
    let token = "\(file):\(function):\(line)"
    once(token: token, block: block)
}

public class func once(token: String,
                       block: () -> Void) {
    objc_sync_enter(self)
    defer { objc_sync_exit(self) }
    
    guard !_onceTracker.contains(token) else { return }
    
    _onceTracker.append(token)
    block()
}

相关文章

网友评论

    本文标题:在Swift中使用method swizzling的一点心得

    本文链接:https://www.haomeiwen.com/subject/tspvmctx.html