Swizzling Method Swizzling Method Swizzling Method Swizzling Method Swizzling But it was black magic, and a sinkhole. This chapter will examine it and turn it into white magic

What is Method Swizzling?

  1. Method Swizzling (Method swap), as the name implies, is to swap the implementation of the two methods, that is, from the original a-AIMP, B-BIMP correspondence into A-BIMP, B-AIMP.

  2. Each class maintains a list of methods that contain SEL and ITS corresponding IMP information. What Method exchange does is to disconnect SEL from IMP and generate a corresponding relationship with the new IMP.

  3. Method Swizzing happens at run time and is basically used to swap two methods at run time, we can write Method Swizzling code anywhere, but the swap only works after this Method Swilzzling code has run out.

  4. Method Swizzling is the best interpretation of OC dynamic. In-depth study and understanding of its characteristics will help us keep low coupling degree of code while increasing the volume of business, and reduce the workload and difficulty of maintenance.

You can use a picture to better illustrate this

Before the exchange:

Method Swizzling related functions API

// get a method class_getInstanceMethod from SELCopy the code
// Gets the implementation of a method method_getImplementationCopy the code
// Get an OC implementation's encoding type method_getTypeEncodingCopy the code
// Add the implementation class_addMethod to the methodCopy the code
// Replace the implementation of one method with the implementation of another class_replaceMethodCopy the code
// Swap implementations of two methods method_exchangeImplementationsCopy the code

Method Swizzling precautions for use

  • 1. Method exchange shall ensure uniqueness and atomicity
    • Uniqueness: should be as close as possible+loadMethod, which guarantees that the method will be called without exception.
    • Atomicity: usedispatch_onceTo perform the method exchange, which ensures that it is run only once.
  • 2. Be sure to call the original implementation
    • Since the internal implementation of iOS is invisible to us, the use of method swapping may cause its code structure to change and have other effects on the system, so the original implementation should be invoked to keep the internal operations running properly
  • 3. Method names must not conflict
    • This is common sense, to avoid conflicts with other libraries.
  • 4. Make comments and logs
    • Keep a good record of the affected methods, otherwise some of the output may get confused over time or when someone else debuts the code.
  • 5. If you don’t have to, use methods sparingly
    • While method swapping allows us to solve problems efficiently, it can lead to unexplained bugs if not handled properly.

The typical pitch-swap method actively calls load

The first pitfall is simple: if we swap methods in load and do not do any processing, if we call load again, the method IMP will be swapped back, resulting in an unsuccessful swap.

The solution is also relatively simple, as mentioned in Note 1 above, to use the singleton pattern to exchange methods, ensuring that the exchange of methods is performed only once

Typical pitfall – Subclasses have no implementation, swapping parent class methods

Pit some examples

We can do this by creating an example of a parent class LGPerson, a subclass LGStudent, and a class LGStudent+LG to swap methods.

@interface LGPerson : NSObject - (void)personInstanceMethod; @end@implementation LGPerson - (void)personInstanceMethod{NSLog(@"person object method :%s",__func__); } @end ************************************************************** @interface LGStudent : LGPerson @end @implementation LGStudent @end **************************************************************Copy the code

First of all, ordinary method exchange, and in the VC normal call, according to the print results, we can find that in the subclass to exchange the method of the parent class, there is no crash, and the subclass of the classification of the exchange method is also normally executed

@implementation LGStudent (LG)

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

- (void)lg_studentInstanceMethod{
    [self lg_studentInstanceMethod];
    NSLog(@"LGStudent class add LG object method :%s",__func__); } @ the end * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * call * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - (void) viewDidLoad {[super viewDidLoad]; LGStudent *s = [[LGStudent alloc] init]; [s personInstanceMethod]; } * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * printing result * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * 2020-01-20 10:45:31. 809408 + 0800 Person object :-[LGPerson personInstanceMethod] 2020-01-20 10:45:31.809568+0800 [LGStudent(lg) lg_studentInstanceMethod] [81429:20470219]Copy the code

However, if we call, the parent class itself to call this method, there will be a crash, the reason is also relatively clear, is that the subclass will exchange this method, the parent class and not after the exchange method IMP, so there will be a crash can not find the method

- (void)viewDidLoad {
    [super viewDidLoad];
    
    LGStudent *s = [[LGStudent alloc] init];
    [s personInstanceMethod];
    
    LGPerson *p = [[LGPerson alloc] init];
    [p personInstanceMethod];
}

Copy the code

The solution

The bottom line is: if the method doesn’t have one of its own, add the method you want to swap. Then the parent method IMP points to the swapped method

When swapping methods, try adding the original method to the class and point the IMP to the swapped method

  • If it succeeds, the class does not have one before, so you need to replace the IMP of the parent method into the swapped method, thus completing the closed loop shown in the figure above
  • If not, the method is already in the class itself, so simply swap
+ (void)lg_betterMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
    
    if(! cls) NSLog(@"The exchange class passed in cannot be empty"); // oriSEL personInstanceMethod // swizzledSEL lg_studentInstanceMethod Method oriMethod = class_getInstanceMethod(cls, oriSEL); Method swiMethod = class_getInstanceMethod(cls, swizzledSEL); // try to add // ✅ personInstanceMethod(sel) - lg_studentInstanceMethod(imp) BOOL success = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(oriMethod));if//✅lg_studentInstanceMethod (swizzledSEL) -personInstancemethod (imp) class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod)); }else{// Implementations has method_exchangeImplementations(oriMethod, swiMethod); }}Copy the code

Typical pitfall – methods are declared, not implemented

Again using the above example, if LGStudent has a method – (void) HelloWord is only alive and has no implementation.

Even if we used the above solution and added a method, it was nil because the original method couldn’t be found. So it creates an infinite loop

We can resolve this pothole by determining if the original method exists and adding an empty implementation. And then I’m going to make a judgment call and I’m going to switch, and I’m going to be perfect

The specific code is as follows

+ (void)lg_bestMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
    
    if(! cls) NSLog(@"The exchange class passed in cannot be empty");
    
    Method oriMethod = class_getInstanceMethod(cls, oriSEL);
    Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
    
    if(! OriMethod) {✅// When oriMethod is nil, replace swizzledSEL with an empty implementation that does nothing as follows: class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod)); method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){ })); } ✅ Swizzle personInstanceMethod(IMP) ✅ 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 the code

Method Swizzling is commonly used

The following swizzling methods are encapsulated exactly as in the code above

No intrusive burial point

The three most common burial points in iOS development are the number of times a page has been entered, the length of time it has been on the page, and the number of click events. All of this can be done with Method Swizzling.

In the following example, we exchange viewWillAppear and viewWillDisappear methods in UIViewController to realize the statistics of the entry and exit interface, and record the relevant class names. Through the mapping relationship, we can clearly know the user’s behavior

@implementation UIViewController (logger) + (void)load { static dispatch_once_t onceToken; Dispatch_once (&oncetoken, ^{✅// get SEL to be replaced by @selector, As SMHook: hookClass: fromeSelector: toSelector parameters passed in SEL fromSelectorAppear = @ the selector (viewWillAppear:); SEL toSelectorAppear = @selector(hook_viewWillAppear:); [SMHook hookClass:self fromSelector:fromSelectorAppear toSelector:toSelectorAppear]; SEL fromSelectorDisappear = @selector(viewWillDisappear:); SEL toSelectorDisappear = @selector(hook_viewWillDisappear:); [SMHook hookClass:self fromSelector:fromSelectorDisappear toSelector:toSelectorDisappear]; }); } - (void)hook_viewWillAppear:(BOOL)animated {✅// [self hook_viewWillAppear:animated]; } - (void)hook_viewWillDisappear:(BOOL)animated {✅ To perform the original viewWillDisappear method [self insertToViewWillDisappear]; [self hook_viewWillDisappear:animated]; } - (void)insertToViewWillAppear {✅// Buried point for logging in ViewWillAppear [[[[SMLogger create] message:[NSString stringWithFormat:@"%@ Appear",NSStringFromClass([self class])]] classify:ProjectClassifyOperation] save]; } - (void) insertToViewWillDisappear {✅ / / when ViewWillDisappear logging buried point [[[[SMLogger create] message: [nsstrings stringWithFormat:@"%@ Disappear",NSStringFromClass([self class])]]
      classify:ProjectClassifyOperation]
     save];
}
@end
Copy the code

So by clicking on methods, we can also do non-intrusive burying via run-time method substitution.

The main thing here is to find the click event method sendAction:to:forEvent: and replace it with the one you defined in the +load() method. The complete code implementation is as follows:

Unlike UIViewController lifecycle buries, UIButton may have multiple different subclasses in a view class, and subclasses of the same UIButton may have different buries in different view classes. Therefore, we need to combine “Action selector name NSStringFromSelector(Action)” + “view class name NSStringFromClass([Target Class])” into a unique identifier for buried recording

+ (void)load { static dispatch_once_t onceToken; Dispatch_once (&oncetoken, ^{✅// get SEL to be replaced by @selector, As SMHook: hookClass: fromeSelector: toSelector parameters passed in SEL fromSelector = @ the selector (sendAction: to:forEvent:);
        SEL toSelector = @selector(hook_sendAction:to:forEvent:);
        [SMHook hookClass:self fromSelector:fromSelector toSelector:toSelector];
    });
}

- (void)hook_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
    [self insertToSendAction:action to:target forEvent:event];
    [self hook_sendAction:action to:target forEvent:event];
}
- (void)insertToSendAction:(SEL)action to:(id)target forEvent:(UIEvent *) Event {✅// logs are recordedif ([[[event allTouches] anyObject] phase] == UITouchPhaseEnded) {
        NSString *actionString = NSStringFromSelector(action);
        NSString *targetName = NSStringFromClass([target class]);
        [[[SMLogger create] message:[NSString stringWithFormat:@"% @ % @",targetName,actionString]] save]; }}Copy the code

With the exception of UIViewController and UIButton controls, all Cocoa framework controls can use this method for non-intrusive burying points. Taking the most complex UITableView control in Cocoa framework as an example, you can use the Hook setDelegate method to achieve no intrusive buried points. Also, for Gesture events in Cocoa, we can hook initWithTarget: Action: to achieve non-intrusive burying points.

Prevent arrays, dictionaries, etc from crashing out of bounds

This example, I believe, has been used by everyone in the development, because array out of bounds is the most likely way to cause crash, and generally crash is serious, so we must avoid it

In iOS, NSNumber, NSArray, NSDictionary and other classes are Class Clusters. An implementation of NSArray may consist of multiple classes. Therefore, if you want to Swizzling the NSArray, you must obtain its real body to Swizzling, direct operation on the NSArray is invalid. This is because Method Swizzling doesn’t work with NSArray and other classes.

Because these classes, clusters of classes, are actually a kind of abstract factory design pattern. Inside the abstract factory there are many other subclasses that inherit from the current class, and the abstract factory class creates different abstract objects to use depending on the situation. For example, if we call NSArray’s objectAtIndex: method, this class will check inside the method and create different abstract classes to operate on.

So if we Swizzling an NSArray class, we’re just Swizzling the parent class, and we’re creating other subclasses inside NSArray to do that, and we’re not really Swizzling the NSArray itself, so we should be doing it in its “real” form.

NSArray = NSArray; NSDictionary = NSArray;

Here is a common example

@implementation NSArray (CrashHandle) ✅// If the following code doesn't work, it's mostly because it calls the super load method. In the load method below, the parent class's load method should not be called. + (void)load {Method originMethod = class_getInstanceMethod(objc_getClass())"__NSArrayI"), @selector(objectAtIndex:));
    Method swizzlingMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(wy_objectAtIndex:)); method_exchangeImplementations(fromMethod, toMethod); } ✅// to avoid conflict with the system method, I usually prefix the swizzling method - (id)wy_objectAtIndex:(NSUInteger)index {✅// determine whether the subscript is out of bounds, if it is into exception interceptionif (self.count-1 < index) {
        @try {
            return[self cm_objectAtIndex:index]; } @catch (NSException *exception) {✅// A crash message will be printed after a crash. If you are online, you can send crash information to the server NSLog(@) from here"---------- %s Crash Because Method %s ----------\n", class_getName(self.class), __func__);
            NSLog(@"% @", [exception callStackSymbols]);
            returnnil; } @finally {}} ✅// If there is no problem, the method call proceeds normallyelse {
        return[self cm_objectAtIndex:index]; }} * * * * * * * * * * * * * * * * * * * * * * * * * * call * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * - (void) viewDidLoad {[super viewDidLoad]; NSArray *array = @[@0, @1, @2, @3]; [array objectAtIndex:3]; [array objectAtIndex:4]; [array objectAtIndex:4]; }Copy the code

The above two examples are only common in development, and there are many other applications that need to be adjusted according to the requirements. These are all practical applications of AOP for aspect programming, and Method Swizzling is one of the most commonly used AOP ideas in iOS development

reference

IOS Runtime: Method Swizzling

IOS development · Runtime principles and practices

IOS Development Master class