不为人知的技巧:Swift 中用特有方法实现"黑魔法"方法交换

这里每天分享一个 iOS 的新知识,快来关注我吧

前言

=====

在 iOS 开发中,方法交换(Method Swizzling)是一种极具灵活性的技术,也是面试中经常问到的问题。它允许开发者在运行时修改或替换现有方法的实现。这种技术通常用于在不需要子类化或修改原始类的情况下,增强或改变方法的行为。

今天,我们将深入探讨方法交换的原理、用途以及如何在 Swift 中进行方法交换。

什么是方法交换?

方法交换涉及到在运行时交换两个方法的实现。通过这种技术,开发者可以在不更改原始类的情况下,为方法添加或修改行为。

方法交换的用途

方法交换有多种应用场景:

  1. 扩展或修改现有方法:在不需要子类化的情况下,对框架或第三方库中的方法进行扩展或修改。

  2. 调试或日志记录:例如,你想知道某个方法是否被调用了,但无法修改源码,此时可以交换方法来自定义输出日志,或者来衡量某个方法的性能如何。

  3. 实现跨领域关注点:如在多个方法或类中实现认证或缓存功能。

举个具体的例子,Firebase SDK 是一个使用方法交换特别多的 SDK,它通过替换 UIViewController 的生命周期方法,来自动启用视图跟踪,简化了设置过程,几乎不用手动设置,就可以直接开启这个功能。

另一个例子是 Pulse,它可以自动记录 App 内所有的网络请求,原理就是它内部交换了 URLSession 的初始化方法,以便自动捕获通过 URLSession 进行的网络事件。

如何进行方法交换?

传统方法

在以前 OC 时代,我们使用 OC 提供的方法,如 class_getInstanceMethodclass_getClassMethodmethod_exchangeImplementations,来交换方法的实现。

但在 swift 下略微有点不同,我们分别来说,以下在 ViewController.swift 中的代码为例:

swift 复制代码
import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let someClass = SomeClass()
        someClass.originalMethod()
    }
}

class SomeClass {
    @objc dynamic func originalMethod() {
        print("I am the original method")
    }
}

运行以上代码,我们会看到 originalMethod 的输出:

css 复制代码
I am the original method

现在,我们将添加一些代码以将 originalMethod 的实现替换为 swizzledOriginalMethod 的实现:

swift 复制代码
import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let selector1 = #selector(SomeClass.originalMethod)
        let selector2 = #selector(SomeClass.swizzledOriginalMethod)
        let method1 = class_getInstanceMethod(SomeClass.self, selector1)!
        let method2 = class_getInstanceMethod(SomeClass.self, selector2)!
        method_exchangeImplementations(method1, method2)

        let someClass = SomeClass()
        someClass.originalMethod()
    }
}

class SomeClass {
    @objc dynamic func originalMethod() {
        print("I am the original method")
    }
}

extension SomeClass {
    @objc func swizzledOriginalMethod() {
        print("I am the replacement method")
    }
}

运行以上代码,我们可以确认在调用 originalMethod 时,实际上调用的是 swizzledOriginalMethod

css 复制代码
I am the replacement method

现代 swift 方法

在 swift 中有个更简单的方法,Swift 中添加了 @_dynamicReplacement 注解,以支持更原生的方法交换实现。

在看下以下在 ViewController.swift 中的代码:

swift 复制代码
import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let someClass = SomeClass()
        someClass.originalMethod()
        someClass.orignalMethodWithParameters(for: "Param1", param2: "Param2")
        print(someClass.originalMethodWithParametersAndReturnType(param: "Param"))
    }
}

class SomeClass {
    dynamic func originalMethod() {
        print("I am the original method")
    }

    dynamic func orignalMethodWithParameters(for param1: String, param2: String) {
        print("I am the original method with parameters - param1: \(param1) - param2: \(param2)")
    }

    dynamic func originalMethodWithParametersAndReturnType(param: String) -> Bool {
        print("I am the original method with parameter - param: \(param) - and return type: Bool")
        return true
    }
}

当运行上述代码时,我们将看到以下输出:

sql 复制代码
I am the original method
I am the original method with parameters - param1: Param1 - param2: Param2
I am the original method with parameter - param: Param - and return type: Bool
true

现在,添加以下代码以 @_dynamicReplacement 注解来用新实现替换原始方法:

swift 复制代码
extension SomeClass {
    @_dynamicReplacement(for: originalMethod)
    func replacementMethod() {
        print("I'm the replacement method")
    }

    @_dynamicReplacement(for: orignalMethodWithParameters(for:param2:))
    func replacementMethodWithParameters(param1: String, param2: String) {
        print("I am the replacement method with parameters - param1: \(param1) - param2: \(param2)")
    }

    @_dynamicReplacement(for: originalMethodWithParametersAndReturnType(param:))
    func replacement_withParameters_andReturnType(param: String) -> Bool {
        print("I am the replacement method with parameter - param: \(param) - and return type: Bool")
        return false
    }
}

再次运行应用程序,我们将在控制台中看到以下输出:

vbnet 复制代码
I'm the replacement method
I am the replacement method with parameters - param1: Param1 - param2: Param2
I am the replacement method with parameter - param: Param - and return type: Bool
false

如上所示,扩展中的方法实现已经替换了 SomeClass 的原始方法实现,而无需我们对子类进行子类化。

优缺点

像任何技术解决方案一样,在考虑方法交换时,权衡其优缺点是至关重要的。它应该作为最后的手段,当其他方法都不可行或成本很高时使用。

  • 方法交换非常强大,但如果使用不当,也可能带来风险。它修改了现有方法的行为,这可能导致意外的副作用或与代码库其他部分的冲突。

  • 应谨慎使用交换,并且仅在必要时使用。重要的是要彻底测试并确保交换不会引入错误或性能问题。

  • 交换可能使代码库更难以理解和维护,因为方法的行为可能并不立即显而易见。

  • 如果使用方法交换,重要的是要遵循最佳实践:

  1. 仅在自己的类或类别中进行方法交换,以避免与其他代码发生冲突。

  2. 使用唯一的方法名称以防止命名冲突。

  3. 确保在交换方法时线程安全。

  4. 清晰地记录和注释交换的方法。

替代方案

如果可能的话,你应该考虑以下替代方案来替代交换,因为相对来说以下方案更安全,更易于维护:

  1. 子类化:使用集成子类化和重写方法通常是一种更安全和更易于维护的方法。

  2. 组合:使用组合和包装对象可以提供类似的好处,而无需修改原始类。

  3. 依赖注入:通过协议或委托注入行为,可以在不进行运行时修改的情况下实现灵活性。

总之,方法交换是一种强大的技术工具,但也需要谨慎使用。相信读完本文能帮助你更好地理解和运用这项技术,也能在面试中应对自如。

这里每天分享一个 iOS 的新知识,快来关注我吧

本文同步自微信公众号 "iOS新知",每天准时分享一个新知识,这里只是同步,想要及时学到就来关注我吧!

相关推荐
2501_915921433 小时前
没有Mac如何完成iOS 上架:iOS App 上架App Store流程
android·ios·小程序·https·uni-app·iphone·webview
万少4 小时前
HarmonyOS DevEco的三个小技巧
harmonyos·客户端
瓜子三百克1 天前
值类型大小与内存分配
swift·内存分配
初级代码游戏1 天前
Maui劝退:用windows直接真机调试iOS,无须和Mac配对
macos·ios·配置·maui·热重载
fundroid1 天前
Swift 进军 Android,Kotlin 该如何应对?
android·ios
形影相吊1 天前
iOS防截屏实战
ios
吴Wu涛涛涛涛涛Tao1 天前
Flutter 弹窗解析:从系统 Dialog 到完全自定义
flutter·ios
kymjs张涛1 天前
零一开源|前沿技术周报 #7
android·前端·ios
思考着亮2 天前
15-错误处理
ios
思考着亮2 天前
9.方法
ios