InterposeKit

SwiftPM xcodebuild pod lib lint Xcode 11.4+ Swift 5.2+

InterposeKit 是一个现代化的 Swift 库,用于优雅地进行方法拦截(swizzling),支持对类和单个对象进行 Hook。它具有完善的文档、经过测试,使用“纯” Swift 5.2 编写,并且适用于 @objc dynamic Swift 函数或 Objective-C 实例方法。 InterposeKit 的灵感来源于 Mac Catalyst 中的一个竞争条件,需要使用巧妙的方法拦截来修复,我还写了一篇关于我在博客上的实现想法

该库不基于method_exchangeImplementations添加新方法和交换实现,而是直接使用 class_replaceMethod 直接替换实现。 这避免了 通常方法拦截的一些问题

你可以调用原始实现,并在方法调用之前、之后或替代方法调用添加代码。
这类似于 Aspects 库,但尚未进行动态子类化。

对比:不使用助手函数和使用 InterposeKit 拦截属性

用法

假设你想修改 TestClass 中的 sayHi 方法

class TestClass: NSObject {
    // Functions need to be marked as `@objc dynamic` or written in Objective-C.
    @objc dynamic func sayHi() -> String {
        print("Calling sayHi")
        return "Hi there 👋"
    }
}

let interposer = try Interpose(TestClass.self) {
    try $0.prepareHook(
        #selector(TestClass.sayHi),
        methodSignature: (@convention(c) (AnyObject, Selector) -> String).self,
        hookSignature: (@convention(block) (AnyObject) -> String).self) {
            store in { `self` in
                print("Before Interposing \(`self`)")
                let string = store.original(`self`, store.selector) // free to skip
                print("After Interposing \(`self`)")
                return string + "and Interpose"
            }
    }
}

// Don't need the hook anymore? Undo is built-in!
interposer.revert()

想要只 Hook 单个实例?没问题!

let hook = try testObj.hook(
    #selector(TestClass.sayHi),
    methodSignature: (@convention(c) (AnyObject, Selector) -> String).self,
    hookSignature: (@convention(block) (AnyObject) -> String).self) { store in { `self` in
        return store.original(`self`, store.selector) + "just this instance"
        }
}

这是我们调用 print(TestClass().sayHi()) 时得到的结果

[Interposer] Swizzled -[TestClass.sayHi] IMP: 0x000000010d9f4430 -> 0x000000010db36020
Before Interposing <InterposeTests.TestClass: 0x7fa0b160c1e0>
Calling sayHi
After Interposing <InterposeTests.TestClass: 0x7fa0b160c1e0>
Hi there 👋 and Interpose

主要特性

对象 Hook

InterposeKit 可以 Hook 类和对象。 类 Hook 类似于方法拦截,但基于对象的 Hook 提供了多种设置 Hook 的新方法。 这是通过在运行时创建动态子类来实现的。

注意:如果对象使用 KVO,则 Hook 将失败并出现错误。 KVO 机制很脆弱,很容易导致崩溃。 支持在创建 Hook 后使用 KVO,不会导致问题。

定义签名的各种方法

除了使用 methodSignaturehookSignature 之外,以下变体也可以定义签名

methodSignature + 转换后的 block

let interposer = try Interpose(testObj) {
    try $0.hook(
        #selector(TestClass.sayHi),
        methodSignature: (@convention(c) (AnyObject, Selector) -> String).self) { store in { `self` in
            let string = store.original(`self`, store.selector)
            return string + testString
            } as @convention(block) (AnyObject) -> String }
}

通过存储对象定义类型

// Functions need to be `@objc dynamic` to be hookable.
let interposer = try Interpose(testObj) {
    try $0.hook(#selector(TestClass.returnInt)) { (store: TypedHook<@convention(c) (AnyObject, Selector) -> Int, @convention(block) (AnyObject) -> Int>) in {

        // You're free to skip calling the original implementation.
        let int = store.original($0, store.selector)
        return int + returnIntOverrideOffset
        }
    }
}

延迟 Hook

有时,可能需要 Hook 系统框架深处的一个类,该类会在稍后的时间加载。 Interpose 为此提供了一个解决方案,并使用动态链接器中的 Hook 来在加载新类时收到通知。

try Interpose.whenAvailable(["RTIInput", "SystemSession"]) {
    let lock = DispatchQueue(label: "com.steipete.document-state-hack")
    try $0.hook("documentState", { store in { `self` in
        lock.sync {
            store((@convention(c) (AnyObject, Selector) -> AnyObject).self)(`self`, store.selector)
        }} as @convention(block) (AnyObject) -> AnyObject})

    try $0.hook("setDocumentState:", { store in { `self`, newValue in
        lock.sync {
            store((@convention(c) (AnyObject, Selector, AnyObject) -> Void).self)(`self`, store.selector, newValue)
        }} as @convention(block) (AnyObject, AnyObject) -> Void})
}

常见问题解答

你为什么不叫它 Interpose? “Kit”感觉太老派了。

将其命名为 Interpose 是计划,但后来出现了 SR-898。 虽然让一个类与模块同名在大多数情况下有效,但在你启用 build-for-distribution 时,这会中断。 有一些讨论来解决这个问题,但这将更接近 2020 年底,甚至更晚。

我想 Hook Swift! 你做了另一个 ObjC swizzle 的东西,为什么?

UIKit 和 AppKit 不会消失,并且 Bug 也不会消失。 我认为这是一个很少需要的工具来修复系统级问题。 有一些方法可以在 Swift 中做到这一点,但那是一个单独的(并且更加困难!)项目。 (有关详细信息,请参见 动态函数替换 #20333 又名 @_dynamicReplacement。)

我可以发布这个吗?

是的,当然。 这个项目的目标是一个简单的库,它不会试图过于聪明。 我在 Aspects 中做了这件事,虽然我非常喜欢它,但它存在问题并且可能会与其他试图变得聪明的代码产生副作用。 InterposeKit 很无聊,所以你不必担心像“我们在我们的应用程序中添加了 New Relic,现在 你的东西崩溃了”这样的情况。

它没有做 X!

欢迎 Pull Requests! 你可能想先打开一个草稿来列出你计划的内容,我想保持功能集最小,以便它保持简单且没有魔法。

安装

构建 InterposeKit 需要 Xcode 11.4+ 或带有 Swift Package Manager 的 Swift 5.2+ 工具链。

Swift Package Manager

.package(url: "https://github.com/steipete/InterposeKit.git", from: "0.0.1") 添加到你的 Package.swift 文件的 dependencies

CocoaPods

InterposeKit 位于 CocoaPods 上。 将 pod 'InterposeKit' 添加到你的 Podfile

Carthage

github "steipete/InterposeKit" 添加到你的 Cartfile

改进的想法

使其成为现实: Carthage compatible CocoaPods

感谢

特别感谢 JP Simard 在使用 GitHub Actions 设置 Yams 方面做得非常出色 - 这对于快速构建 CI 非常有帮助。

许可证

InterposeKit 使用 MIT 许可证。