简介
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()
}
网友评论