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
Method
的实现,这比基于选择器的方法拦截更安全。revert()
轻松撤销 Hook。 如果在此期间有人更改了内容,它还会检查并报错。NSInvocation
,这需要装箱并且可能很慢。convention
部分,这将在运行时崩溃。NSInvocation
。InterposeKit 可以 Hook 类和对象。 类 Hook 类似于方法拦截,但基于对象的 Hook 提供了多种设置 Hook 的新方法。 这是通过在运行时创建动态子类来实现的。
注意:如果对象使用 KVO,则 Hook 将失败并出现错误。 KVO 机制很脆弱,很容易导致崩溃。 支持在创建 Hook 后使用 KVO,不会导致问题。
除了使用 methodSignature
和 hookSignature
之外,以下变体也可以定义签名
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 系统框架深处的一个类,该类会在稍后的时间加载。 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 是计划,但后来出现了 SR-898。 虽然让一个类与模块同名在大多数情况下有效,但在你启用 build-for-distribution 时,这会中断。 有一些讨论来解决这个问题,但这将更接近 2020 年底,甚至更晚。
UIKit 和 AppKit 不会消失,并且 Bug 也不会消失。 我认为这是一个很少需要的工具来修复系统级问题。 有一些方法可以在 Swift 中做到这一点,但那是一个单独的(并且更加困难!)项目。 (有关详细信息,请参见 动态函数替换 #20333 又名 @_dynamicReplacement
。)
是的,当然。 这个项目的目标是一个简单的库,它不会试图过于聪明。 我在 Aspects 中做了这件事,虽然我非常喜欢它,但它存在问题并且可能会与其他试图变得聪明的代码产生副作用。 InterposeKit 很无聊,所以你不必担心像“我们在我们的应用程序中添加了 New Relic,现在 你的东西崩溃了”这样的情况。
欢迎 Pull Requests! 你可能想先打开一个草稿来列出你计划的内容,我想保持功能集最小,以便它保持简单且没有魔法。
构建 InterposeKit 需要 Xcode 11.4+ 或带有 Swift Package Manager 的 Swift 5.2+ 工具链。
将 .package(url: "https://github.com/steipete/InterposeKit.git", from: "0.0.1")
添加到你的 Package.swift
文件的 dependencies
。
InterposeKit 位于 CocoaPods 上。 将 pod 'InterposeKit'
添加到你的 Podfile
。
将 github "steipete/InterposeKit"
添加到你的 Cartfile
。
Interpose.whenAvailable
以获得更好的错误冒泡。特别感谢 JP Simard 在使用 GitHub Actions 设置 Yams 方面做得非常出色 - 这对于快速构建 CI 非常有帮助。
InterposeKit 使用 MIT 许可证。