Exploring the Proper Use of Method Swizzling

March 1, 2019 (5y ago)

Background

When first encountering the OC runtime mechanism, one is often fascinated by its 'dark magic'. This is especially true when using Method Swizzling to hook a method and change the implementation of an existing selector, making AOP statistical tracking, APM detection, etc., all possible.

Risks

However, with great power often comes greater danger.

On Stackoverflow, there's an article: "What are the Dangers of Method Swizzling in Objective C?" which clearly describes the dangers of method swapping in OC. Here's a brief summary.

1. Method Swizzling is not an Atomic Operation

In 95% of cases, using Method Swizzling is safe. This is because we usually want the method replacement to be effective throughout the entire APP life cycle, so we perform a series of operations in the +(void)load method. There's no concurrency issue in this case. But if you accidentally write code in +(void)initialize, very bizarre situations might occur.

Actually, one should minimize operations in +(void)initialize to avoid impacting startup speed.

2. It Can Change Implementations Not Belonging to Our Code

This is an issue that becomes apparent upon consideration. If you perform method swapping without a clear understanding of the situation, it may affect someone else's code. Especially if you overwrite a method in a class without calling the superclass method, problems may arise. Therefore, to avoid potential unknowns, it's best to call the original implementation in the swapped method.

3. Potential for Naming Conflicts

When performing Method Swizzling, we generally prefix the new method.

For example:

- (void)my_setFrame:(NSRect)frame {  
    // do custom work  
    [self my_setFrame:frame];  
}  

However, there's an issue here: if somewhere else - (void)my_setFrame:(NSRect)frame is defined, it could cause problems.

Thus, the best solution is to use function pointers (though this makes the code look less like OC).

@implementation NSView (MyViewAdditions)  
  
static void MySetFrame(id self, SEL _cmd, NSRect frame);  
static void (*SetFrameIMP)(id self, SEL _cmd, NSRect frame);  
  
static void MySetFrame(id self, SEL _cmd, NSRect frame) {  
    // do custom work  
    SetFrameIMP(self, _cmd, frame);  
}  
  
+ (void)load {  
    [self swizzle:@selector(setFrame:) with:(IMP)MySetFrame store:(IMP *)&SetFrameIMP];  
}  
  
@end 

The author also provided a more ideal definition of a swizzle method:

typedef IMP *IMPPointer;  
  
BOOL class_swizzleMethodAndStore(Class class, SEL original, IMP replacement, IMPPointer store) {  
    IMP imp = NULL;  
    Method method = class_getInstanceMethod(class, original);  
    if (method) {  
        const char *type = method_getTypeEncoding(method);  
        imp = class_replaceMethod(class, original, replacement, type);  
        if (!imp) {  
            imp = method_getImplementation(method);  
        }  
    }  
    if (imp && store) { *store = imp; }  
    return (imp != NULL);  
}  
  
@implementation NSObject (FRRuntimeAdditions)  
+ (BOOL)swizzle:(SEL)original with:(IMP)replacement store:(IMPPointer)store {  
    return class_swizzleMethodAndStore(self, original, replacement, store);  
}  
@end

4. Changing Method Parameters

The author believes this is the biggest issue. When you replace a method, you also replace the parameters passed to the original method implementation.

[self my_setFrame:frame];

What this line does is:

objc_msgSend(self, @selector(my_setFrame:), frame);

The runtime looks for the implementation of my_setFrame: and once found, passes my_setFrame and frame. But actually, the method found should be the original setFrame:, so when it's called, the _cmd parameter is not the expected setFrame:, but my_setFrame, receiving an unexpected parameter.

The best way is still to use the above definition.

5. Order Issues Brought by Method Swapping

When performing method swapping on multiple classes, pay attention to the order, especially when there's a parent-child class relationship. For example:

[NSButton swizzle:@selector(setFrame:) with:@selector(my_buttonSetFrame:)];
[NSControl swizzle:@selector(setFrame:) with:@selector(my_controlSetFrame:)];
[NSView swizzle:@selector(setFrame:) with:@selector(my_viewSetFrame:)];

In the implementation above, when you call NSButton's setFrame, it will call your swapped my_buttonSetFrame method and NSView's original setFrame method.

Conversely, if the order is like this:

[NSView swizzle:@selector(setFrame:) with:@selector(my_viewSetFrame:)];
[NSControl swizzle:@selector(setFrame:) with:@selector(my_controlSetFrame:)];
[NSButton swizzle:@selector(setFrame:) with:@selector(my_buttonSetFrame:)];

It will call the swapped methods of NSButton, NSControl, and NSView, which is the correct order.

So, it's still recommended to perform method swapping in the +(void)load method, as it ensures that the parent class's load method is called before the child's, avoiding errors.

6. Brings Many Inconveniences in Understanding and Debugging

This needs no further explanation, especially when there's no documentation. Sometimes, if you encounter runtime operations written by a colleague in some corner unknown to anyone, it can lead to unpredictable problems, making debugging extremely troublesome.

Correct Approach

So what is the correct posture for Method Swizzling?

As the author emphasized above, perform method replacement in the load

The swizzle perfect definition given by the author above is already quite the correct approach. But here's another point to note.

Some articles on the internet talk about implementing Method Swizzling through a Category as follows:

#import <objc/runtime.h>

@implementation UIViewController (Tracking)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];

        SEL originalSelector = @selector(viewWillAppear:);
        SEL swizzledSelector = @selector(xxx_viewWillAppear:);

        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

        // When swizzling a class method, use the following:
        // Class class = object_getClass((id)self);
        // ...
        // Method originalMethod = class_getClassMethod(class, originalSelector);
        // Method swizzledMethod = class_getClassMethod(class, swizzledSelector);

        BOOL didAddMethod =
            class_addMethod(class,
                originalSelector,
                method_getImplementation(swizzledMethod),
                method_getTypeEncoding(swizzledMethod));

        if (didAddMethod) {
            class_replaceMethod(class,
                swizzledSelector,
                method_getImplementation(originalMethod),
                method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

#pragma mark - Method Swizzling

- (void)xxx_viewWillAppear:(BOOL)animated {
    [self xxx_viewWillAppear:animated];
    NSLog(@"viewWillAppear: %@", self);
}

@end

However, this is not rigorous enough and is part of the previously mentioned risks. If origin_imp uses the _cmd parameter, the _cmd after the hook does not meet expectations.

Suppose we now want to hook - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event;

Then, if implemented as above, `[self xxx_touchesBegan:touches withEvent:event];

will crash. The reason is that this function containsforwardTouchMethod`, and after disassembling, the implementation is similar:

static void forwardTouchMethod(id self, SEL _cmd, NSSet *touches, UIEvent *event) {
  // The responder chain is used to figure out where to send the next touch
    UIResponder *nextResponder = [self nextResponder];
    if (nextResponder && nextResponder != self) {
      // Not all touches are forwarded - so we filter here.
        NSMutableSet *filteredTouches = [NSMutableSet set];
        [touches enumerateObjectsUsingBlock:^(UITouch *touch, BOOL *stop) {
          // Checks every touch for forwarding requirements.
            if ([touch _wantsForwardingFromResponder:self toNextResponder:nextResponder withEvent:event]) {
                [filteredTouches addObject:touch];
            }else {
              // This is interesting legacy behavior. Before iOS 5, all touches are forwarded (and this is logged)
                if (!_UIApplicationLinkedOnOrAfter(12)) {
                    [filteredTouches addObject:touch];
                    // Log old behavior
                    static BOOL didLog = 0;
                    if (!didLog) {
                        NSLog(@"Pre-iOS 5.0 touch delivery method forwarding relied upon. Forwarding -%@ to %@.", NSStringFromSelector(_cmd), nextResponder);
                    }
                }
            }
        }];
        // here we basically call [nextResponder touchesBegan:filteredTouches event:event];
        [nextResponder performSelector:_cmd withObject:filteredTouches withObject:event];
    }
}

If we exchange the IMP, [nextResponder performSelector:_cmd withObject:filteredTouches withObject:event]; will not have a corresponding implementation, and _cmd will become the SEL we replaced. Obviously, the nextResponder has not implemented the corresponding method, leading to a crash.

In this case, you can write like this:

static IMP __original_TouchesBegan_Method_Imp;

@implementation UIView (Debug)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        
        SEL originalSelector = @selector(touchesBegan:withEvent:);
        SEL swizzledSelector = @selector(dae_touchesBegan:withEvent:);
        
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        
        __original_TouchesBegan_Method_Imp = method_getImplementation(originalMethod);
        
        BOOL didAddMethod =
        class_addMethod(class,
                        originalSelector,
                        method_getImplementation(swizzledMethod),
                        method_getTypeEncoding(swizzledMethod));
        
        if (didAddMethod) {
            class_replaceMethod(class,
                                swizzledSelector,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

- (void)dae_touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // custom 
    
     void (*functionPointer)(id, SEL, NSSet<UITouch *> *, UIEvent *) = (void(*)(id, SEL, NSSet<UITouch *> *, UIEvent*))__original_TouchesBegan_Method_Imp;
        
    functionPointer(self, _cmd, touches, event);
}

This way, the correct IMP can be found.