Skip to content

Commit c3099fb

Browse files
committed
Added support for cleanup of the dynamic subclass
1 parent 66e0acc commit c3099fb

File tree

4 files changed

+141
-9
lines changed

4 files changed

+141
-9
lines changed

Sources/InterposeKit/Hooks/HookStrategy/ObjectHookStrategy/ObjectHookRegistry.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ internal enum ObjectHookRegistry {
1313

1414
objc_setAssociatedObject(
1515
block,
16-
&self.associatedKey,
16+
&ObjectHookRegistryKey,
1717
WeakReference(handle),
1818
.OBJC_ASSOCIATION_RETAIN
1919
)
@@ -26,16 +26,16 @@ internal enum ObjectHookRegistry {
2626

2727
guard let reference = objc_getAssociatedObject(
2828
block,
29-
&self.associatedKey
29+
&ObjectHookRegistryKey
3030
) as? WeakReference<ObjectHookHandle> else { return nil }
3131

3232
return reference.object
3333
}
3434

35-
private static var associatedKey: UInt8 = 0
36-
3735
}
3836

37+
fileprivate var ObjectHookRegistryKey: UInt8 = 0
38+
3939
fileprivate class WeakReference<T: AnyObject>: NSObject {
4040

4141
fileprivate init(_ object: T?) {

Sources/InterposeKit/Hooks/HookStrategy/ObjectHookStrategy/ObjectHookStrategy.swift

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,8 @@ internal final class ObjectHookStrategy: HookStrategy {
143143

144144
self.appliedHookIMP = hookIMP
145145
self.storedOriginalIMP = originalIMP
146+
147+
self.object.incrementHookCount()
146148
ObjectHookRegistry.register(self.handle, for: hookIMP)
147149

148150
Interpose.log("Replaced implementation for -[\(self.class) \(self.selector)] IMP: \(originalIMP) -> \(hookIMP)")
@@ -200,6 +202,11 @@ internal final class ObjectHookStrategy: HookStrategy {
200202
}
201203

202204
Interpose.log("Restored implementation for -[\(self.class) \(self.selector)] IMP: \(originalIMP)")
205+
206+
// Decrement the hook count and if this was the last hook, uninstall the dynamic subclass.
207+
if self.object.decrementHookCount() {
208+
ObjectSubclassManager.uninstallSubclass(for: object)
209+
}
203210
}
204211

205212
// ============================================================================ //
@@ -233,3 +240,54 @@ internal final class ObjectHookStrategy: HookStrategy {
233240
}
234241

235242
}
243+
244+
extension NSObject {
245+
246+
/// Increments the number of active object-based hooks on this instance by one.
247+
fileprivate func incrementHookCount() {
248+
self.hookCount += 1
249+
}
250+
251+
/// Decrements the number of active object-based hooks on this instance by one and returns
252+
/// `true` if this was the last hook, or `false` otherwise.
253+
fileprivate func decrementHookCount() -> Bool {
254+
guard self.hookCount > 0 else { return false }
255+
self.hookCount -= 1
256+
return self.hookCount == 0
257+
}
258+
259+
/// The current number of active object-based hooks on this instance.
260+
///
261+
/// Internally stored using associated objects. Always returns a non-negative value.
262+
private var hookCount: Int {
263+
get {
264+
guard let count = objc_getAssociatedObject(
265+
self,
266+
&ObjectHookCountKey
267+
) as? NSNumber else { return 0 }
268+
269+
return count.intValue
270+
}
271+
set {
272+
let newCount = max(0, newValue)
273+
if newCount == 0 {
274+
objc_setAssociatedObject(
275+
self,
276+
&ObjectHookCountKey,
277+
nil,
278+
.OBJC_ASSOCIATION_ASSIGN
279+
)
280+
} else {
281+
objc_setAssociatedObject(
282+
self,
283+
&ObjectHookCountKey,
284+
NSNumber(value: newCount),
285+
.OBJC_ASSOCIATION_RETAIN_NONATOMIC
286+
)
287+
}
288+
}
289+
}
290+
291+
}
292+
293+
private var ObjectHookCountKey: UInt8 = 0

Sources/InterposeKit/Hooks/HookStrategy/ObjectHookStrategy/ObjectSubclassManager.swift

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,29 @@ internal enum ObjectSubclassManager {
6565
internal static func uninstallSubclass(
6666
for object: NSObject
6767
) {
68-
fatalError("Not yet implemented")
68+
// Get the InterposeKit-managed dynamic subclass installed on the object.
69+
guard let dynamicSubclass = self.installedSubclass(for: object) else { return }
70+
71+
// Retrieve the original class (superclass of the dynamic subclass) we want to restore
72+
// the object to.
73+
guard let originalClass = class_getSuperclass(dynamicSubclass) else { return }
74+
75+
// Restore the object’s class to its original class.
76+
object_setClass(object, originalClass)
77+
78+
Interpose.log({
79+
let subclassName = NSStringFromClass(dynamicSubclass)
80+
let originalClassName = NSStringFromClass(originalClass)
81+
let objectAddress = String(format: "%p", object)
82+
return "Removed subclass: \(subclassName), restored \(originalClassName) on object \(objectAddress)"
83+
}())
84+
85+
// Dispose of the dynamic subclass.
86+
//
87+
// This is safe to call here because all hooks have been reverted. Unfortunately, we can’t
88+
// validate this explicitly, as `objc_disposeClassPair(...)` offers no feedback mechanism
89+
// and will silently fail if the subclass is still in use.
90+
objc_disposeClassPair(dynamicSubclass)
6991
}
7092

7193
// ============================================================================ //

Tests/InterposeKitTests/ObjectHookTests.swift

Lines changed: 56 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import XCTest
44
fileprivate class ExampleClass: NSObject {
55
@objc dynamic var intValue = 1
66
@objc dynamic func doSomething() {}
7+
@objc dynamic func doSomethingElse() {}
78
@objc dynamic var arrayValue: [String] { ["base"] }
89
}
910

@@ -48,7 +49,7 @@ final class ObjectHookTests: XCTestCase {
4849
)
4950
}
5051

51-
func testMultipleHooks() throws {
52+
func testMultipleHooksOnSingleObject() throws {
5253
let object = ExampleClass()
5354
XCTAssertEqual(object.arrayValue, ["base"])
5455

@@ -82,7 +83,7 @@ final class ObjectHookTests: XCTestCase {
8283
XCTAssertEqual(object.arrayValue, ["base"])
8384
}
8485

85-
func testHookOnMultipleObjects() throws {
86+
func testHooksOnMultipleObjects() throws {
8687
let object1 = ExampleClass()
8788
let object2 = ExampleClass()
8889

@@ -201,7 +202,7 @@ final class ObjectHookTests: XCTestCase {
201202
_ = token
202203
}
203204

204-
func testCleanUp_implementationPreserved() throws {
205+
func testCleanUp_hook_implementationPreserved() throws {
205206
let object = ExampleClass()
206207
var deallocated = false
207208

@@ -223,7 +224,7 @@ final class ObjectHookTests: XCTestCase {
223224
XCTAssertFalse(deallocated)
224225
}
225226

226-
func testCleanUp_implementationDeallocated() throws {
227+
func testCleanUp_hook_implementationDeallocated() throws {
227228
let object = ExampleClass()
228229
var deallocated = false
229230

@@ -246,5 +247,56 @@ final class ObjectHookTests: XCTestCase {
246247

247248
XCTAssertTrue(deallocated)
248249
}
250+
251+
func testCleanUp_dynamicSubclass() throws {
252+
let object = ExampleClass()
253+
254+
// Original class
255+
XCTAssertTrue(object_getClass(object) == ExampleClass.self)
256+
257+
let hook1 = try object.applyHook(
258+
for: #selector(ExampleClass.doSomething),
259+
methodSignature: (@convention(c) (NSObject, Selector) -> Void).self,
260+
hookSignature: (@convention(block) (NSObject) -> Void).self
261+
) { hook in
262+
return { `self` in hook.original(self, hook.selector) }
263+
}
264+
265+
let hook2 = try object.applyHook(
266+
for: #selector(ExampleClass.doSomething),
267+
methodSignature: (@convention(c) (NSObject, Selector) -> Void).self,
268+
hookSignature: (@convention(block) (NSObject) -> Void).self
269+
) { hook in
270+
return { `self` in hook.original(self, hook.selector) }
271+
}
272+
273+
let hook3 = try object.applyHook(
274+
for: #selector(ExampleClass.doSomethingElse),
275+
methodSignature: (@convention(c) (NSObject, Selector) -> Void).self,
276+
hookSignature: (@convention(block) (NSObject) -> Void).self
277+
) { hook in
278+
return { `self` in hook.original(self, hook.selector) }
279+
}
280+
281+
// Dynamic subclass
282+
XCTAssertTrue(object_getClass(object) != ExampleClass.self)
283+
284+
try hook1.revert()
285+
try hook2.revert()
286+
287+
// Dynamic subclass
288+
XCTAssertTrue(object_getClass(object) != ExampleClass.self)
289+
290+
// Back to original subclass after reverting the last hook
291+
try hook3.revert()
292+
XCTAssertTrue(object_getClass(object) == ExampleClass.self)
293+
294+
try hook2.apply()
295+
XCTAssertTrue(object_getClass(object) != ExampleClass.self)
296+
297+
// Back to original subclass after reverting the last hook
298+
try hook2.revert()
299+
XCTAssertTrue(object_getClass(object) == ExampleClass.self)
300+
}
249301

250302
}

0 commit comments

Comments
 (0)