While reading the source code of a team project, I found some flaws in the way Method Swizzling is written. This article is about what the correct way to write iOS Method Swizzling should look like.

Here is an implementation of iOS Method Swizzling.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
+ (void)load {
    Class class = [self class];

    SEL fromSelector = @selector(func);
    SEL toSelector = @selector(easeapi_func);

    Method fromMethod = class_getInstanceMethod(class, fromSelector);
    Method toMethod = class_getInstanceMethod(class, toSelector);

    method_exchangeImplementations(fromMethod, toMethod);
} 

This way of writing works fine some of the time, but there are actually some problems. So what is the problem?

An example

To illustrate the problem, let’s assume a scenario.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@interface Father: NSObject
-(void)easeapi;
@end
@implementation Father
-(void)easeapi {
    //your code
}
@end

//Son1继承自Father
@interface Son1: Father
@end
@implementation Son1
@end

//Son2继承自Father,并HOOK了easeapi方法。
@interface Son2: Father
@end
@implementation Son2
+ (void)load {
    Class class = [self class];

    SEL fromSelector = @selector(easeapi);
    SEL toSelector = @selector(new_easeapi);

    Method fromMethod = class_getInstanceMethod(class, fromSelector);
    Method toMethod = class_getInstanceMethod(class, toSelector);

    method_exchangeImplementations(fromMethod, toMethod);
}
-(void)new_easeapi {
    [self new_easeapi];
    //your code
}
@end

It looks like nothing is wrong and Son2’s method is exchanged successfully, but when we execute [Son1 easeapi], we find CRASH.

‘-[Son1 new_easeapi]: unrecognized selector sent to instance 0x600002d701f0’'

This is strange, we HOOK the method of Son2, how can we generate a crash of Son1?

Why does the crash happen

To explain this, it’s important to go back to the principle.

First, to be clear, class_getInstanceMethod looks for the implementation of the parent class.

In the above example, the easeapi is implemented in Son2’s parent class Father. After executing method_exchangeImplementations, Father’s easeapi and Son2’s new_easeapi exchange methods.

After the exchange, when Son1 (a subclass of Father) executes the easeapi method, it will find Father’s easeapi method implementation by “message lookup”.

Here’s the point!

Since the method swap has already occurred, Son2’s new_easeapi method is actually executed.

1
2
3
4
-(void)new_easeapi {
    [self new_easeapi];
    //your code
}

The darn thing is that [self new_easeapi] is executed in new_easeapi. At this point, self here is a Son1 instance, but there is no SEL for new_easeapi in Son1 and its parent class Father, and the corresponding SEL cannot be found, so naturally it will CRASH.

What is the case that there is no problem?

It says above: “This writing method works fine some of the time”. So, when exactly does executing method_exchangeImplementations directly not cause problems?

At least in the following scenarios there will be no problems.

  • There is an implementation of caseapi in Son2

    In the above example, if we override the easeapi method in Son2, executing class_getInstanceMethod(class, fromSelector) gets the easeapi implementation of Son2, not Father’s. This way, the execution of method_exchangeImplementations will not affect Father’s implementation.

  • new_easeapi implementation improvements

    1
    2
    3
    4
    
    - (void) new_easeapi {
        //[self new_easeapi];//屏蔽掉这句代码
        //your code
    }
    

    In this scenario, since [self new_easeapi] will not be executed, there will be no problem either. However, this will not achieve the effect of HOOK.

Improved Optimization

Recommended Method Swizzling implementation.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];

        SEL fromSelector = @selector(easeapi);
        SEL toSelector = @selector(new_easeapi);

        Method fromMethod = class_getInstanceMethod(class, fromSelector);
        Method toMethod = class_getInstanceMethod(class, toSelector);

        if(class_addMethod(class, fromSelector, method_getImplementation(toMethod), method_getTypeEncoding(toMethod))) {
            class_replaceMethod(class, toSelector, method_getImplementation(fromMethod), method_getTypeEncoding(fromMethod));
        } else {
            method_exchangeImplementations(fromMethod, toMethod);
        }
    });
}

At least two changes can be observed.

  • dispatch_once

    Although dyld can guarantee that the call to Class’s load is thread-safe, it is still recommended to use dispatch_once for protection against the extreme case of load being shown to force the call to repeat swap (the first swap was successful, and the next swap back …) which can cause logical confusion.

  • Added class_addMethod judgment

class_addMethod & class_replaceMethod

Still understood by definition.

class_addMethod

Adds an implementation of SEL to the specified Class (or a binding between SEL and the specified IMP), returns YES for a successful addition, NO for a SEL that already exists or a failed addition.

It has two points to note.

  • If the SEL is implemented in a parent class, a method overriding the parent class is added.
  • Returns NO if there is already a SEL in that Class.

Executing class_addMethod avoids interfering with the parent class, which is why it is recommended that you try to use class_addMethod first. Obviously, because of the iOS Runtime messaging mechanism, executing only method_exchangeImplementations may affect the methods of the parent class. Based on this principle, it is perfectly fine to use method_exchangeImplementations directly if the HOOK is for a method implemented in this class.

class_replaceMethod

  • If the specified SEL does not exist for the Class, class_replaceMethod does the same thing as class_addMethod.
  • If the specified SEL exists for that Class, then class_replaceMethod does the same thing as method_setImplementation.