This experiment was born out of ReactiveCocoa#3690 – a homeopathy to fix swizzling with more swizzling.
Ideally, there would be a way to simply intercept the call with a custom handler to add some logic. However, that's far from simple:
- Need to handle existing class methods (add vs. replace).
- Need to call original implementation and pass it parameters – no way of doing it in Swift in some cases (via message forwarding), no way of doing it reliably in Objective-C in others (via
va_list
)…
In theory, where my research stops, it's possible to achieve this via scenario:
- Add a block implementation under a random non-clashing selector.
- Add a message forwarding like ReactiveCocoa does.
Swizzling is a very broad term and there are several approaches:
- Global behavior changes:
method_exchangeImplementations
for swapping method implementations globally. - Specific behavior changes:
method_setImplementation
for replacing specific method implementations. - Dynamic method addition:
class_addMethod
for adding methods at runtime. - Dynamic class changes: ISA swizzling for altering an instance’s class.
- Message redirection:
forwardingTargetForSelector:
for redirecting messages efficiently. - Complex message handling:
forwardInvocation:
for detailed control over message forwarding. - Dynamic method resolution:
resolveInstanceMethod:
for resolving methods at runtime. - Modular changes: Category method swizzling for making changes in a modular way.
Exchange the implementations of two methods. Can be used for instance and class methods to modify behavior globally in a safe manner.
Method originalMethod = class_getInstanceMethod([MyClass class], @selector(originalMethod));
Method swizzledMethod = class_getInstanceMethod([MyClass class], @selector(swizzledMethod));
method_exchangeImplementations(originalMethod, swizzledMethod);
Replace the implementation of a method directly. Can be used to replace a method with a new implementation, often for specific, non-reversible changes.
Method originalMethod = class_getInstanceMethod([MyClass class], @selector(originalMethod));
IMP newIMP = imp_implementationWithBlock(^(id _self) {
// New implementation…
});
method_setImplementation(originalMethod, newIMP);
Add a new method to a class dynamically. Can be used to add a non-existing method to a class, for example, to handle dynamic behavior or to conform to a protocol.
void newMethodIMP(id self, SEL _cmd) {
// New implementation…
}
class_addMethod([MyClass class], @selector(newMethod), (IMP)newMethodIMP, "v@:");
Change the ISA pointer of an instance to point to a different class. The ISA pointer is a key part of the Objective-C runtime that tells the system what class an object is an instance of. Used for dynamically altering the class of an instance at runtime, commonly used in proxy and KVO implementations.
object_setClass(instance, [NewClass class]);
Forward messages to another object by overriding forwardingTargetForSelector:
for redirecting messages to another object.
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(methodToForward)) {
return anotherObject;
}
return [super forwardingTargetForSelector:aSelector];
}
Override forwardInvocation:
to manually forward messages to different objects or handle them for more control over message forwarding.
- (void)forwardInvocation:(NSInvocation *)anInvocation {
if ([anotherObject respondsToSelector:[anInvocation selector]]) {
[anInvocation invokeWithTarget:anotherObject];
} else {
[super forwardInvocation:anInvocation];
}
}
Dynamically add method implementations during runtime for dynamically adding methods that aren't available at compile-time.
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(dynamicMethod)) {
class_addMethod([self class], sel, (IMP)dynamicMethodIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
Use categories to override methods. Can be used for modular code changes and to avoid touching original class implementations directly.
@implementation MyClass (Swizzling)
+ (void)load {
Method originalMethod = class_getInstanceMethod(self, @selector(originalMethod));
Method swizzledMethod = class_getInstanceMethod(self, @selector(swizzledMethod));
method_exchangeImplementations(originalMethod, swizzledMethod);
}
@end
They use ISA-swizzling – a technique that involves changing the ISA pointer of an object to change the class of an object at runtime. If I understand correctly, this is what allows per-instance swizzling without affecting the actual class… and what causes KVO issues.
They have a bunch of really cool Objective-C API usage:
- https://github.com/ReactiveCocoa/ReactiveCocoa/tree/master/ReactiveCocoa
- https://github.com/ReactiveCocoa/ReactiveCocoa/blob/master/ReactiveCocoa/ObjC+Constants.swift
- https://github.com/ReactiveCocoa/ReactiveCocoa/blob/master/ReactiveCocoa/NSObject+Intercepting.swift
-
Samuel Défago: Yet another article about method swizzling
-
Peter Steinberger: Swizzling in Swift
-
Mike Ash: objc_msgSend's New Prototype – also see comments…
-
Daniel Jalkut: Casting Objective-C Message Sends – follow up on Mike's article…
-
Dong ZHENG: A Look Under the Hood of objc_msgSend()
- https://github.com/steipete/Aspects
- https://github.com/steipete/InterposeKit
- https://github.com/rabovik/RSSwizzle
- https://github.com/rbaumbach/Swizzlean
- https://github.com/DavidGoldman/InspectiveC
- https://github.com/mikeash/MAZeroingWeakRef (from StackOverflow: My isa-swizzling breaks KVO )