About the use of Swift4.0 Method Swizzling(iOS hook mechanism)

Posted by KoopaTroopa on Wed, 25 Dec 2019 20:00:41 +0100

There are many posts about the principle of Method Swizzling. It's clear that we won't repeat it here,

Only the use of Method Swizzling in swift 4.0 is dealt with here

Because the Runtime support of Swift itself is not very in place, especially the method swizzling is very common in OC, but after Swift, it is found that the load method is missing and needs to be replaced by initialize. Even in Swift4, it directly cancels the initialize method. So you need to initialize yourself

The solution needs to add this line of code in appdelegate
 UIViewController.initializeMethod()

/**
 You need to add this line of code in appdelegate
 UIViewController.initializeMethod()
 */

 

private let onceToken = "Method Swizzling"

extension UIViewController {
    
    public class func initializeMethod() {
        // Make sure This isn't a subclass of UIViewController, So that It applies to all UIViewController childs
        
        if self != UIViewController.self {
            return
        }
        //The DispatchQueue function ensures that the code is executed only once, preventing it from being swapped back to the desired effect
        DispatchQueue.once(token: onceToken) {
            let originalSelector = #selector(UIViewController.viewWillAppear(_:))
            let swizzledSelector = #selector(UIViewController.swizzled_viewWillAppear(animated:))
            
            let originalMethod = class_getInstanceMethod(self, originalSelector)
            let swizzledMethod = class_getInstanceMethod(self, swizzledSelector)
            //When Swizzling, it is necessary to use class "Addmethod" to determine whether there is an implementation of the method to be replaced in the original class
            let didAddMethod: Bool = class_addMethod(self, originalSelector, method_getImplementation(swizzledMethod!), method_getTypeEncoding(swizzledMethod!))
            //If the class add method returns yes, it means that there is no implementation of the method to replace in the current class, so you need to find it in the parent class. At this time, you need to use the method getimplement method to get the method implementation in the class getinstancemethod, and then class replace method to implement Swizzing
            
            if didAddMethod {
                class_replaceMethod(self, swizzledSelector, method_getImplementation(originalMethod!), method_getTypeEncoding(originalMethod!))
            } else {
                method_exchangeImplementations(originalMethod!, swizzledMethod!)
            }
            
            
            let originalSelector1 = #selector(UIViewController.viewWillDisappear(_:))
            let swizzledSelector1 = #selector(UIViewController.swizzled_viewWillDisappear(animated:))
            
            let originalMethod1 = class_getInstanceMethod(self, originalSelector1)
            let swizzledMethod1 = class_getInstanceMethod(self, swizzledSelector1)
            //When Swizzling, it is necessary to use class "Addmethod" to determine whether there is an implementation of the method to be replaced in the original class
            let didAddMethod1: Bool = class_addMethod(self, originalSelector1, method_getImplementation(swizzledMethod1!), method_getTypeEncoding(swizzledMethod1!))
            if didAddMethod1 {
                class_replaceMethod(self, swizzledSelector1, method_getImplementation(originalMethod1!), method_getTypeEncoding(originalMethod1!))
            } else {
                method_exchangeImplementations(originalMethod1!, swizzledMethod1!)
            }
        }
    }
    
    
    @objc func swizzled_viewWillAppear(animated: Bool) {
        //The code to be injected is written here
        self.swizzled_viewWillAppear(animated: animated)
       DDLOG(message: "\(NSStringFromClass(classForCoder))--Appear")

    }
    @objc func swizzled_viewWillDisappear(animated: Bool) {
        //The code to be injected is written here
        self.swizzled_viewWillDisappear(animated: animated)
       DDLOG(message: "\(NSStringFromClass(classForCoder))--Disappear")

    }
}

Since swift does not have the DispatchQueue.once method, it manually extends a convenient way to use

extension DispatchQueue {
    private static var _onceTracker = [String]()
    public class func once(token: String, block: () -> ()) {
           objc_sync_enter(self)
        defer {
            objc_sync_exit(self)
        }
        if _onceTracker.contains(token) {
            return
        }
        _onceTracker.append(token)
        block()
    }
    
    func async(block: @escaping ()->()) {
        self.async(execute: block)
    }
    
    func after(time: DispatchTime, block: @escaping ()->()) {
        self.asyncAfter(deadline: time, execute: block)
    }
}

Topics: Swift