Method swizzling method exchange

Posted by ytse on Tue, 08 Mar 2022 03:59:24 +0100

What is method swizzling?

  • Method swizzling means method exchange. Its main function is to replace the implementation of one method with the implementation of another method at runtime. This is what we often call iOS black magic,
  • In OC, method swizzling is used to realize AOP. AOP(Aspect Oriented Programming) is a programming idea, which is different from OOP (object-oriented programming)
    • OOP and AOP are both programming ideas
    • OOP programming idea is more inclined to encapsulate business modules and divide them into clearer logical units;
    • AOP is an aspect oriented extraction package, which extracts the common parts of each module, improves the reuse rate of modules and reduces the coupling between services.
  • Each class maintains a method list, i.e. methodList. There are different methods, i.e. method, in the methodList. Each method contains the SEL and imp of the method. Method exchange is to disconnect the original correspondence between sel and imp, and generate the correspondence between sel and the new imp

As shown in the following figure, the corresponding relationship between sel and IMP before and after exchange

Related API s involved in method swizzling

  • Get Method through sel
    • class_getInstanceMethod: get instance method
    • class_getClassMethod: get class method
  • method_getImplementation: get the implementation of a method
  • method_setImplementation: sets the implementation of a method
  • method_getTypeEncoding: gets the encoding type of the method implementation
  • class_addMethod: add method implementation
  • class_replaceMethod: use the implementation of one method to replace the implementation of another method, that is, aIMP points to bIMP, but bIMP does not necessarily point to aIMP
  • method_ Exchange implementations: exchange implementations of two methods, namely aimp - > BIMP, BIMP - > aimp

Pit 1: one time problem in the use of method swizzling

The so-called one-time is: mehod swizzling is written in the load method, and the load method will be called many times, which will lead to repeated exchange of methods and restore the direction of method sel to the original imp

Solution

The method exchange can be executed only once through the singleton design principle. In OC, dispatch can be used_ Once implementation singleton

+ (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [LGRuntimeTool lg_bestMethodSwizzlingWithClass:self oriSEL:@selector(helloword) swizzledSEL:@selector(lg_studentInstanceMethod)];
    });
}
Copy code

Pit 2: the subclass is not implemented, but the parent class is implemented

In the following code, LGPerson implements personInstanceMethod, while LGStudent inherits from LGPerson and does not implement personInstanceMethod. What problems will occur when running the following code?

//*********LGPerson class*********
@interface LGPerson : NSObject
- (void)personInstanceMethod;
@end

@implementation LGPerson
- (void)personInstanceMethod{
    NSLog(@"person Object method:%s",__func__);  
}
@end

//*********LGStudent class*********
@interface LGStudent : LGPerson
- (void)helloword;
+ (void)sayHello;
@end

@implementation LGStudent
@end

//*********Call*********
- (void)viewDidLoad {
    [super viewDidLoad];

    // Dark magic Pit 2: subclass not implemented - parent class implemented
    LGStudent *s = [[LGStudent alloc] init];
    [s personInstanceMethod];
    
    LGPerson *p = [[LGPerson alloc] init];
    [p personInstanceMethod];
}
Copy code

Among them, the method exchange code is as follows, which is realized through the classification of LGStudent

@implementation LGStudent (LG)

+ (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [LGRuntimeTool lg_methodSwizzlingWithClass:self oriSEL:@selector(personInstanceMethod) swizzledSEL:@selector(lg_studentInstanceMethod)];
    });
}

// personInstanceMethod I need something about this method of the parent class
// Add a personInstanceMethod method to you
// imp

- (void)lg_studentInstanceMethod{
    ////Will recursion occur-- Recursion will not occur because lg_studentInstanceMethod will follow oriIMP, that is, the implementation of personInstanceMethod
    [self lg_studentInstanceMethod];
    NSLog(@"LGStudent Category added lg Object method:%s",__func__);
}

@end
 Copy code

The following is the encapsulated method swizzling method

@implementation LGRuntimeTool
+ (void)lg_methodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
    
    if (!cls) NSLog(@"The incoming exchange class cannot be empty");

    Method oriMethod = class_getInstanceMethod(cls, oriSEL);
    Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
    method_exchangeImplementations(oriMethod, swiMethod);
}
Copy code

Through the debugging of the actual code, it is found that it will crash when p calling the personInstanceMethod method, which is described in detail below

  • [s personInstanceMethod]; Because imp LG failed in the exchange_ Studentinstancemethod, which is available in lg student (in LG classification), so no error will be reported
  • The crash point is [p personInstanceMethod];, The essential reason: the classification of LGStudent has carried out method exchange in LG, and the IMP in person has been exchanged into LG in LGStudent_ Studentinstancemethod, and then you need to find LG in LGPerson_ Studentinstancemethod, but there is no LG in LGPerson_ The studentinstancemethod method, that is, the related imp, cannot be found, so it crashes

Optimization: avoid imp not found

Pass class_addMethod attempts to add the method you want to exchange

  • If the addition is successful, that is, there is no such method in the class, you can use class_ Replace method, and class will be called internally_ Addmethod to add
  • If the addition is unsuccessful, that is, there is this method in the class, you can use method_ Exchange implementations
+ (void)lg_betterMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
    
    if (!cls) NSLog(@"The incoming exchange class cannot be empty");
    
    Method oriMethod = class_getInstanceMethod(cls, oriSEL);
    Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
   
    // General exchange method: exchange their own methods -- go below because they mean that adding methods fails
    // Exchange methods that you have not implemented:
    //   First step: I will try to add the method to be exchanged: personinstancemethod (SEL) - > swimethod (IMP)
    //   Then give the imp of the parent class to Swizzle personinstancemethod (IMP) - > swizzledsel 

    BOOL success = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(oriMethod));

    if (success) {// No - exchange - no parent class to process (override one)
        class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
    }else{ // Own
        method_exchangeImplementations(oriMethod, swiMethod);
    }   
}
Copy code

Here is class_replaceMethod,class_addMethod and method_ Source code implementation of exchange implementations

Where class_replaceMethod and class_addMethod is called in addMethod. The difference lies in the judgment of bool value. The following is the source code implementation of addMethod

Pit 3: the subclass is not implemented and the parent class is not implemented. What's the problem with the following calls?

When calling the personInstanceMethod method, the parent class LGPerson has only a declaration but no implementation, and the subclass LGStudent has neither declaration nor implementation

//*********LGPerson class*********
@interface LGPerson : NSObject
- (void)personInstanceMethod;
@end

@implementation LGPerson
@end

//*********LGStudent class*********
@interface LGStudent : LGPerson
- (void)helloword;
+ (void)sayHello;
@end

@implementation LGStudent
@end

//*********Call*********
- (void)viewDidLoad {
    [super viewDidLoad];

    // Dark magic Pit 2: subclass not implemented - parent class implemented
    LGStudent *s = [[LGStudent alloc] init];
    [s personInstanceMethod];
    
    LGPerson *p = [[LGPerson alloc] init];
    [p personInstanceMethod];
}
Copy code

After debugging, it is found that the running code will crash, and the error results are as follows

 .

The reason is stack overflow and recursive loop. So why recursion---- This is mainly because the personInstanceMethod is not implemented. Then, during the method exchange, the oriMethod is always not found, and then the exchange is lonely, that is, the exchange fails. When we call the personInstanceMethod (oriMethod), that is, the oriMethod will enter lg_studentInstanceMethod method, and then LG is called in this method_ Studentinstancemethod, LG at this time_ Studentinstancemethod does not point to oriMethod, and then leads to self adjustment, that is, recursive dead loop

Optimization: avoid recursive dead loops

  • If oriMethod is empty, something needs to be done to avoid method exchange being abandoned because it is meaningless
    • Pass class_addMethod add swiMethod method to oriSEL
    • Through method_setImplementation points the IMP of swiMethod to an empty implementation that does nothing
+ (void)lg_bestMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
    
    if (!cls) NSLog(@"The incoming exchange class cannot be empty");
    
    Method oriMethod = class_getInstanceMethod(cls, oriSEL);
    Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
    
    if (!oriMethod) {
        // When oriMethod is nil, copy swizzledSEL to an empty implementation that does nothing after replacement. The code is as follows:
        class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
        method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){ }));
    }
    
    // General exchange method: exchange their own methods -- go below because they mean that adding methods fails
    // Exchange methods that you have not implemented:
    //   First step: I will try to add the method to be exchanged: personinstancemethod (SEL) - > swimethod (IMP)
    //   Then give the imp of the parent class to Swizzle personinstancemethod (IMP) - > swizzledsel
    //oriSEL:personInstanceMethod

    BOOL didAddMethod = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
    if (didAddMethod) {
        class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
    }else{
        method_exchangeImplementations(oriMethod, swiMethod);
    }
    
}
Copy code

Method - swiz Ling class

The principle of method swizzling of class method and instance method is similar. The only difference is that class methods exist in metaclasses, so you can do the following operations

  • In LGStudent, there is only the declaration of the class method sayHello, which is not implemented
@interface LGStudent : LGPerson
- (void)helloword;
+ (void)sayHello;
@end

@implementation LGStudent

@end
 Copy code
  • Implement the method exchange of class methods in the load method of LGStudent's classification
+ (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
         [LGRuntimeTool lg_bestClassMethodSwizzlingWithClass:self oriSEL:@selector(sayHello) swizzledSEL:@selector(lg_studentClassMethod)];
    });
}
+ (void)lg_studentClassMethod{
    NSLog(@"LGStudent Category added lg Class method:%s",__func__);
   [[self class] lg_studentClassMethod];
}
Copy code
  • The method exchange of encapsulated class methods is as follows
    • Need to pass class_getClassMethod method gets the class method
    • Calling class_addMethod and class_ When the replacemethod method is added and replaced, the class that needs to be passed in is the metaclass, which can be added through object_ The getClass method gets the metaclass of the class
//Encapsulated method swizzling method
+ (void)lg_bestClassMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
    
    if (!cls) NSLog(@"The incoming exchange class cannot be empty");

    Method oriMethod = class_getClassMethod([cls class], oriSEL);
    Method swiMethod = class_getClassMethod([cls class], swizzledSEL);
    
    if (!oriMethod) { // Avoiding actions is meaningless
        // When oriMethod is nil, copy swizzledSEL to an empty implementation that does nothing after replacement. The code is as follows:
        class_addMethod(object_getClass(cls), oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
        method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){
            NSLog(@"Here comes an empty one imp");
        }));
    }
    
    // General exchange method: exchange their own methods -- go below because they mean that adding methods fails
    // Exchange methods that you have not implemented:
    //   First step: I will try to add the method to be exchanged: personinstancemethod (SEL) - > swimethod (IMP)
    //   Then give the imp of the parent class to Swizzle personinstancemethod (IMP) - > swizzledsel
    //oriSEL:personInstanceMethod

    BOOL didAddMethod = class_addMethod(object_getClass(cls), oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
    
    if (didAddMethod) {
        class_replaceMethod(object_getClass(cls), swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
    }else{
        method_exchangeImplementations(oriMethod, swiMethod);
    }
    
}
Copy code
  • Call as follows
- (void)viewDidLoad {
    [super viewDidLoad];
    
    [LGStudent sayHello];
}
Copy code
  • The running results are as follows. Since the compliance method is not implemented, it will go to the empty imp

Application of method swizzling

The most common application of method swizzling is to prevent out of bounds crashes of arrays, dictionaries, etc

In iOS, NSNumber, NSArray, NSDictionary and other classes are class clusters. The implementation of an NSArray may consist of multiple classes. Therefore, if you want to Swizzling NSArray, you must obtain its "real body" for Swizzling. Direct operation on NSArray is invalid.

The class names of NSArray and NSDictionary are listed below. This class can be retrieved through the Runtime function.

Class name

Real body

NSArray

__NSArrayI

NSMutableArray

__NSArrayM

NSDictionary

__NSDictionaryI

NSMutableDictionary

__NSDictionaryM

Take NSArray as an example

  • Create a classification CJLArray of NSArray
@implementation NSArray (CJLArray)
//If the following code doesn't work, most of the reason for this problem is that it calls the super load method. In the following load method, the load method of the parent class should not be called. This will result in invalid method exchange
+ (void)load{
    Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
    Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(cjl_objectAtIndex:));
    
    method_exchangeImplementations(fromMethod, toMethod);
}

//If the following code doesn't work, most of the reason for this problem is that it calls the super load method. In the following load method, the load method of the parent class should not be called. This will result in invalid method exchange
- (id)cjl_objectAtIndex:(NSUInteger)index{
    //Judge whether the subscript is out of bounds. If it is out of bounds, it will enter abnormal interception
    if (self.count-1 < index) {
        // Do some exception handling here, or you won't know that an error has occurred.
#ifdef DEBUG / / debugging phase
        return [self cjl_objectAtIndex:index];
#else / / release phase
        @try {
            return [self cjl_objectAtIndex:index];
        } @catch (NSException *exception) {
            // After the crash, the crash information will be printed to facilitate our debugging.
            NSLog(@"---------- %s Crash Because Method %s  ----------\n", class_getName(self.class), __func__);
            NSLog(@"%@", [exception callStackSymbols]);
            return nil;
        } @finally {
            
        }
#endif
    }else{ // If there is no problem, the method is called normally
        return [self cjl_objectAtIndex:index];
    }
}

@end
 Copy code
  • Test code
 NSArray *array = @[@"1", @"2", @"3"];
[array objectAtIndex:3];
Copy code
  • The print result is as follows. The crash log will be output, but it will not crash in practice