Detailed Introduction to Switching of OC Method - No Blind Spots

Keywords: PHP SDK

Links to the original text: https://www.cnblogs.com/mddblog/p/11105450.html

If you are already familiar with it, skip the general introduction and look directly at the FAQ section.

Overall introduction

Method exchange is an important embodiment of runtime and the core of message language. OC opens many interfaces to developers and allows them to participate in the whole process.

principle

oc's Method calls, such as [self test] are converted to objc_msgSend(self,@selfector(test)). objc_msgsend identifies @selector(test) as the identifier, finds the Method in the Method list of the class (and the inheritance hierarchy of the class) to which the Method receiver (self) belongs, and then gets the entry address of the imp function to complete the Method call.

typedef struct objc_method *Method;

// oc2.0 has been abandoned and can be used as a reference
struct objc_method {
    SEL _Nonnull method_name;
    char * _Nullable method_types;
    IMP _Nonnull method_imp;
}

Based on the above, there are two ways to complete the exchange:

  • One is to change @selfector(test), which is not realistic, because we are generally hook system methods, we can not get the system source code, can not modify. Even if our own code gets source code modification, it's compile-time, not run-time.
  • So we generally modify the imp function pointer. Change the mapping relationship between sel and imp;

The interface provided by the system for us

Typeedef struct objc_method * Method; Method is an opaque pointer that we can't access its members through the structure pointer, but only through exposed interfaces.

The interface is as follows. It's very simple and clear at a glance.

#import <objc/runtime.h>

/// Get the instance Method based on cls and sel
Method _Nonnull * _Nullable class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name);

/// To add a method to cls, you need to provide three members of the structure. If it already exists, it returns NO. If it does not exist, it adds and returns success.
BOOL class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, 
                const char * _Nullable types)

/// method->imp
IMP _Nonnull method_getImplementation(Method _Nonnull m);

/// Replacement
IMP _Nullable class_replaceMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, 
                    const char * _Nullable types)

/// Follow two method s and exchange their imp: this seems to be what we want
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2);

Simple use

Assuming the viewDidLoad method for exchanging UIViewController

/// A Category of UIViewController

+ (void)swizzleInstanceMethod:(Class)target original:(SEL)originalSelector swizzled:(SEL)swizzledSelector {
    Method originMethod = class_getInstanceMethod(target, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(target, swizzledSelector);
    method_exchangeImplementations(originMethod, swizzledMethod);
}

+ (void)load {
    [self swizzleInstanceMethod:[UIViewController class] original:@selector(viewDidLoad) swizzled:@selector(swizzle_viewDidLoad)];
}
/// hook
- (void)swizzle_viewDidLoad {
    [self swizzle_viewDidLoad];
}

Exchange itself is simple: the principle is simple, the interface method is few and easy to understand, because the structure definition is also three member variables, it is difficult to go anywhere!

However, when it comes to the use scenario, superimposing other external instability factors, it is far from enough to write a stable general or semi-general switching method.

Following is a detailed description of several common pits, which is why there are many articles on the Internet to introduce the exchange of methods, and why to write another article: there is no blind spot any more.

Frequent Question 1. Called many times (exchanged many times)

The code in "Simple Use" is generally okay for hook viewDidload, and the + load method is usually executed once. But if some programmers write irregularly, it will cause multiple calls.

For example, I wrote a subclass of UIViewController, implemented the + load method in the subclass, and habitually called the super method.

+ (void)load {
    // This causes multiple calls to the UIViewController parent load method
    [super load];
}

To avoid blind spots, we extend the call to load:

  • The load method is called at the time of dyld mapping image, which is also logical. After loading, the load method is called.
  • The invocation order between classes is related to the compilation order. The priority invocation of compilation is first, and the invocation order of inheritance level is first parent class and then subclass.
  • Classes and classifications are invoked in the order of priority, followed by classifications.
  • The order between classifications is related to the order of compilation, and the first call of compilation is preferred.
  • The call of the system is to get the imp call directly without leaving the message mechanism.

Manual [super load] goes by message mechanism, classification will be invoked first. If you are lucky, another programmer also implements UIViewController classification, and implements the + load method, and then compiles, then your load method will only be executed once; (After the classification method compiles, it will "overwrite" before)

For the sake of insurance, or:

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self swizzleInstanceMethod:[UIViewController class] original:@selector(viewDidLoad) swizzled:@selector(swizzle_viewDidLoad)];
    });
}

Continue to expand: What are the side effects of multiple calls?

  • According to the principle, if it's even

The result is that the method exchange does not work, but there is a legacy problem, which is called manually.

- (void)swizzle_viewDidLoad {
    [self swizzle_viewDidLoad];
}

It can cause a dead cycle.

In fact, after method exchange, do not attempt to call manually at any time, especially the system method of exchange.

  • Odd calls

After odd times everything is normal. But before odd times, it goes through even times.

For example, if you call swizzle_viewDidLoad manually, it's obvious that the first exchange, normal, second exchange, is no exchange. Then you do the third exchange on other threads, and it's not dead cycle. Haha, it's fun, but you have to be careful, don't play with fire, play online!!!

This may happen, for example, if the exchange is not placed in the load method, or dispatch_once, but instead writes a start-like method, which is called by oneself or others by mistake.

Finally: To prevent multiple exchanges, always add dispatch_once unless you know what you're doing.

Extension again: Common multiple exchanges

What we have said here is different from what we have said above in terms of exchange methods, such as those we often encounter in our development.

We exchange viewDidLoad ourselves, and then third-party libraries exchange viewDidLoad, so before the exchange (arrows represent mapping relationships):

sysSel -> sysImp
ourSel -> ourImp
thirdSel -> thirdImp

The first step is to exchange with the system:

sysSel -> ourImp
ourSel -> sysImp
thirdSel -> thirdImp

Second, third party and system exchange:

sysSel -> thirdImp
ourSel -> sysImp
thirdSel -> ourImp

Assuming that push has a VC, first of all sysSel of the system, then the order of invocation is:

thirdImp,ourImp,sysImp

No problem!

Question 2. The class being exchanged does not implement this method

We're still adding methods to the classification to exchange.

Scenario 1: The parent class implements the swapped method

We intend to exchange subclass methods, but subclasses are not implemented. The parent class implements class_getInstance Method (target, swizzled Selector); the result of execution returns to the method of the parent class, so the subsequent exchange is equivalent to the exchange of methods of the parent class.

Normally there will be no problem, but a series of hidden dangers have been buried. If other programmers also inherit this parent class. The example code is as follows

/// Parents
@interface SuperClassTest : NSObject
- (void)printObj;
@end
@implementation SuperClassTest
- (void)printObj {
    NSLog(@"SuperClassTest");
}
@end

/// Subclass 1
@interface SubclassTest1 : SuperClassTest
@end
@implementation SubclassTest1
- (void)printObj {
    NSLog(@"printObj");
}
@end

/// Subclass 2
@interface SubclassTest2 : SuperClassTest
@end
@implementation SubclassTest2
/// Whether or not to rewrite this method will produce different results
- (void)printObj {
    // Whether super is called or not is a different result.
    [super printObj];
    NSLog(@"printObj");
}
@end

/// Subclass 1 Classification for Exchange

+ (void)swizzleInstanceMethod:(Class)target original:(SEL)originalSelector swizzled:(SEL)swizzledSelector {
    Method originMethod = class_getInstanceMethod(target, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(target, swizzledSelector);
    method_exchangeImplementations(originMethod, swizzledMethod);
}
+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self swizzleInstanceMethod:[SubclassTest1 class] original:@selector(printObj) swizzled:@selector(swiprintObj)];
    });
}

- (void)swiprintObj {
    NSLog(@"swi1:%@",self);
    [self swiprintObj];
}

The sample code realizes the exchange between printObj and swiprintObj.

  • Question 1: If the instance object of the parent class calls printObj normally, it will cause swiprintObj to call first and then printObj. This is not what we want. If you want to monitor the parent class, you can exchange the methods of the parent class directly.
  • Question 2: Assume that sub2 (subclass 2) does not implement printObj, but its instance object also calls printObj. Normally it should be able to call the printObj method of the parent class, but because it is exchanged, it calls swiprintObj of sub1. The implementation of swiprintObj contains [self switObj], where the self-switObj is sub2, and sub2 does not implement swiprintObj, and collapses directly.
  • Question 3: The sub2 subclass overrides printObj. Everything is OK. Sub2 instance object calls are OK, but if you call the super method in printObj...

So how to avoid this?

Use the class_addMethod method to avoid it. The results of the re-optimization are as follows:

+ (void)swizzleInstanceMethod:(Class)target original:(SEL)originalSelector swizzled:(SEL)swizzledSelector {
    Method originMethod = class_getInstanceMethod(target, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(target, swizzledSelector);
    if (class_addMethod(target, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))) {
        class_replaceMethod(target, swizzledSelector, method_getImplementation(originMethod), method_getTypeEncoding(originMethod));
    }
    else {
        method_exchangeImplementations(originMethod, swizzledMethod);
    }
}

Detailed step-by-step analysis is as follows:

  • class_addMethod before execution

superSel -> superImp
sub1SwiSel -> sub1SwiImp

  • After class_addMethod is executed, sel is added to the subclass, but the corresponding imp implementation is the imp of swizzledMethod, that is, the imp of switching method.

superSel -> superImp
sub1Sel -> sub1SwiImp
sub1SwiSel -> sub1SwiImp

The exchanged method sub1Sel has pointed to the imp implementation of the exchanged method. The next step is to point the sel of the exchanged method to the imp of the exchanged method. Wasn't the switched method not implemented??? Yes, OC inheritance, the implementation of the parent class is its implementation superImp

  • class_replaceMethod, replacing the sub1SwiSel implementation with superImp

superSel -> superImp
sub1Sel -> sub1SwiImp
sub1SwiSel -> superImp

When the system sends sel message to the object, it executes sub1SwiImp, sends sub1SwiSel in sub1SwiImp, executes superImp and completes hook.

What we said about adding method s to subclasses is not really a new one, but sharing imp, and function implementation is not new. The advantage of this is that the imp corresponding to superSel remains unchanged, and its own and other subclasses are unaffected to solve this problem perfectly; but keep looking at other problems

Scenario 2: The parent class is not implemented either

Embarrassed, there is no way to achieve, then exchange a hammer???

Let's start with the results. After the swap function is executed, the method will not be swapped, but manual invocation of the following will also cause a dead loop.

- (void)swiprintObj {
    NSLog(@"swi1:%@",self);
    [self swiprintObj];
}

So we need to add judgment and return a bool value to the method caller or, more directly, throw an exception.

/// Attention to exchange class methods is to get metaclass, object_getClass. class_getClassMethod
+ (void)swizzleInstanceMethod:(Class)target original:(SEL)originalSelector swizzled:(SEL)swizzledSelector {
    Method originMethod = class_getInstanceMethod(target, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(target, swizzledSelector);
    if (originMethod && swizzledMethod) {
        if (class_addMethod(target, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))) {
            class_replaceMethod(target, swizzledSelector, method_getImplementation(originMethod), method_getTypeEncoding(originMethod));
        }
        else {
            method_exchangeImplementations(originMethod, swizzledMethod);
        }
    }
    else {
        @throw @"originalSelector does not exit";
    }
}

Plus dispatch_once is already perfect, but not perfect, mainly because the scene is different, the situation is different. We only have to understand the principle and treat different scenarios differently.

Finally, add a new class to exchange system methods

Above all, the exchange method is implemented in the classification. Here, a new "private class" is created to exchange system methods.

When writing SDK, the classification has the problem of overlapping multiple names, and the compilation option is added - ObjC. The compilation phase of the problem is still undetectable. Then we can use a new private class to exchange, and class renaming can compile and report errors directly. The exchange method is slightly different from the classified exchange above.

For example, hook viewDidload, the code is as follows:

@interface SwizzleClassTest : NSObject
@end

@implementation SwizzleClassTest
+ (void)load {
    /// Private classes, dispatch_once is not required
    Class target = [UIViewController class];
    Method swiMethod = class_getInstanceMethod(self, @selector(swi_viewDidLoad));
    Method oriMethod = class_getInstanceMethod(target, @selector(viewDidLoad));
    if (swiMethod && oriMethod) {
        if (class_addMethod(target, @selector(swi_viewDidLoad), method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod))) {
            // Here's the new method for UIViewController
            swiMethod = class_getInstanceMethod(target, @selector(swi_viewDidLoad));
            method_exchangeImplementations(oriMethod, swiMethod);
        }
    }
}

- (void)swi_viewDidLoad {
    // It can't be called. Here self is an instance of the UIViewController class or subclass. If you call test, it will crash directly. Or make a type judgment [self isKindOfClass:[SwizzleClassTest class], and then call it
    // [self test];
    [self swi_viewDidLoad];
}

- (void)test {
    NSLog(@"Do not do this");
}

@end

class_addMethod is also used here, adding a swi_viewDidLoad sel and its imp implementation to UIViewController, sharing the imp implementation of SwizzleClassTest.

In addition, the system sends the viewdidload message and then calls the swi_viewDidLoad method, where the self is UIViewController, so it can't [self test] again, otherwise it will crash. Nor can you manually [self swi_viewDidLoad] elsewhere; it's a dead loop, because self is SwizzleClassTest, and its method is not swapped.

It can be compared before and after the exchange.

Before the exchange:

SwizzleClassTest_swi_viewDidLoadSel -> SwizzleClassTest_swi_viewDidLoadImp

UIViewController_viewDidLoadSel -> UIViewController_viewDidLoadImp

After exchange:

SwizzleClassTest_swi_viewDidLoadSel -> SwizzleClassTest_swi_viewDidLoadImp

UIViewController_swi_viewDidLoadSel -> UIViewController_viewDidLoadImp
UIViewController_viewDidLoadSel -> UIViewController_swi_viewDidLoadImp

You can see that SwizzleClassTest is unaffected and the mapping relationship remains unchanged.

summary

  • First, we need to know the principle of method exchange.
  • Familiar with its common interface;
  • The exchanged method does not have the problem of parent class and child class.
  • And the inheritance and coverage of methods in oc.
  • Problem of duplicate exchange and its consequences;
  • Understanding self is just a hidden parameter, not necessarily an instance object of the class where the current method resides.

Finally, every time the method is exchanged, it is necessary to deduce carefully and calculate the possible impact.

Posted by Worqy on Fri, 28 Jun 2019 11:18:17 -0700