What is Method Swizzling and what role does it play in iOS development?

In short, we mainly use Method Swizzling to swap the system methods for our own methods, thus adding some desired functionality to the system methods. This article mainly lists some practical use cases in the development of Method Swizzling, as well as some questions for readers. Those who wish to read this article can also provide some examples that have not been mentioned in this article, which will be updated continuously.

Currently updated instance summary:
  • Example 1: Replace the ViewController lifecycle method
  • Example 2: Solve the index, add, delete element out of bounds crash
  • Example 3: Prevent the button from being violently clicked repeatedly
  • Example 4: Global change control initial effect
  • Example 5: App hot fix
  • Example 6: App abnormally loads placeholder map generic class encapsulation
  • Example 7: Globally modify the navigation bar back (back) button
Method Swizzling General Method encapsulation

Before enumerating, we can encapsulate the Method Swizzling function as a class Method, as a class of NSObject, so that we can make subsequent calls easier.

#import 
#import 
@interface NSObject (Swizzling) 

+ (void)methodSwizzlingWithOriginalSelector:(SEL)originalSelector
                         bySwizzledSelector:(SEL)swizzledSelector;
@endCopy the code
#import "NSObject+Swizzling.h" @implementation NSObject (Swizzling) + (void)methodSwizzlingWithOriginalSelector:(SEL)originalSelector bySwizzledSelector:(SEL)swizzledSelector{ Class class = [self class]; OriginalMethod = class_getInstanceMethod(class, originalSelector); SwizzledMethod = class_getInstanceMethod(class, swizzledSelector); // Add IMP to SEL BOOL didAddMethod = class_addMethod(class,originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); If (didAddMethod) {// Add successfully: Note The source SEL does not implement IMP. Replace the IMP of the source SEL with the IMP class_replaceMethod(class,swizzledSelector, method_getImplementation(originalMethod) of the exchange SEL, method_getTypeEncoding(originalMethod)); Method_exchangeImplementations (originalMethod, swizzledMethod);} else {// Implementations implementations imPs. } } @endCopy the code
⚠️
  • SEL, Method, IMP meaning and difference

    At runtime, the Class maintains a message distribution list to address the proper delivery of messages. Each message list entry is a Method that maps a key-value pair where the key is the name of the Method (SEL) and the value points to the function pointer implementation (IMP) that the Method implements. Pseudocode representation:

    Class {Method{SEL:IMP; } Method {SEL: IMP; }); };Copy the code

    Method Swizzling changes the class’s message distribution list to correspond message parsing from one selector (SEL) to another implementation (IMP), while confusing the original Method implementation with a new selector (SEL).

  • Why did I add didAddMethod judgment?

    Trying to add the original SEL first is really just a layer of protection, because if the class doesn’t implement originalSelector, but its parent does, class_getInstanceMethod will return the parent’s method. So method_exchangeImplementations replaces that method of the parent class, which of course is not what we want. So we’ll try to add orginalSelector, if there is one, and then we’ll use method_exchangeImplementations to swap out the implementation of the old method with the new method implementation. If that’s not enough, go to Runtime. h and look at class_addMethod.

    /** * Adds a new method to a class with a given name and implementation. * * @param cls The class to which to add a method. * @param name A selector that specifies the name of the method being added. * @param imp A function which is the Implementation of the new method. The function must take at least two arguments -- self and _cmd. * @param types An array of characters that describe the types of the arguments to the method. * * @return YES if the method was added successfully, otherwise NO * (for example, the class already contains a method implementation with that name). * * @note class_addMethod will add an override of a superclass's implementation, * but will not replace an existing implementation in this class. * To change an existing implementation, use method_setImplementation. */Copy the code

    We can add methods (SEL and IMP) to a class using class_addMethod, and return BOOL indicating whether the method was successfully added. It is important to note that class_addMethod adds an implementation that overrides the parent class, but does not replace the implementation of the original class. That is, if class_addMethod returns YES, there is no originalSelector in the subclass, we add the method originalSelector to it by class_addMethod, and make its implementation (IMP) the implementation we want to replace.

    class_addMethod(class,originalSelector,
                                          method_getImplementation(swizzledMethod),
                                          method_getTypeEncoding(swizzledMethod));Copy the code

    At the same time, replace the original implementation (IMP) with the swizzledMethod method,

    class_replaceMethod(class,swizzledSelector,
                              method_getImplementation(originalMethod),
                              method_getTypeEncoding(originalMethod));Copy the code

    Thus, the method exchange is realized without affecting the implementation of the parent class method. If class_addMethod returns NO, the child class itself has an implementation of originalSelector, so you can call swap.

    method_exchangeImplementations(originalMethod, swizzledMethod);Copy the code

    This part of the content is quite a mouthful, I hope you can bear to read carefully and repeatedly.

— — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — instance list — — — — — — — — — — — — — — — — — — — — — — — — — — — — — — –
Example 1: Replace the ViewController lifecycle method

When an App jumps to an interface with network request, loading bar or progress bar is often added to display the current request or progress for user experience effect. The problem with these interfaces is that when the user manually exits the interface when the request is slow, the loading bar needs to be removed. Of course, you can add a method to the View disappear method of each interface in turn, but if there are too many similar interfaces, just copy and paste is not the way. This is where Method Swizzling comes in. We can replace the system’s viewWillDisappear Method so that the loading bar is automatically removed every time the Method is executed.

#import "UIViewController+Swizzling.h"
#import "NSObject+Swizzling.h"
@implementation UIViewController (Swizzling)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self methodSwizzlingWithOriginalSelector:@selector(viewWillDisappear:) bySwizzledSelector:@selector(sure_viewWillDisappear:)];
    });
}

- (void)sure_viewWillDisappear:(BOOL)animated {
    [self sure_viewWillDisappear:animated];
    [SVProgressHUD dismiss];
}Copy the code

Code above, so that the interface does not have to consider whether to remove the loading bar. As a side note, we also usually set the default background color in the lifecycle method, because if the background color defaults to transparent, it also has some impact on the performance of the App, which you can see in the article on UIKit performance optimization. But similar operations can also be written in generic classes, so it’s up to you to use them.

⚠️
  • Why is the method exchange call in the +load method?

    The Objective-C Runtime automatically calls two class methods, + Load and + Initialize. The +load method is called when the class is loaded, which means it must be called. The + Initialize method is called before the class or its subclasses receive the first message, which includes instance method and class method calls. That is, the +initialize method is lazy-loaded and will never be called if the program never sends a message to a class or its subclasses. Another important feature of the +load method is that the implementation of the +load method in subclasses, superclasses, and classes is treated differently. In other words, when the Objective-C Runtime automatically calls the +load method, the +load method in the class does not override the +load method in the main class. In summary, the +load Method is the best “place” to implement Method Swizzling logic. For more in-depth understanding, please refer toUnderstanding +load and +initialize in Objective-C.

  • Why is method exchange performed in dispatch_once?

    Method exchange should be thread-safe and guaranteed under any circumstances (in a multi-threaded environment, or if the +load method is manually called again by someone else)Swap only once to prevent the method from being swapped back again.Unless it is a temporary interchange, then switched back after completion. The most common solution is to use dispatch_once in the +load method to ensure that the exchange is secure. The reason why dispatch_once is necessary is that the +load method itself does not guarantee that the code will be executed only once.

  • Why didn’t the loop happen?

    There must be a lot of readers wondering whysure_viewWillDisappearThe code in the method does not loop recursively. The reason for this is simply that the method has already been swapped and called[self sure_viewWillDisappear:animated]It’s essentially calling the old methodviewWillDisappearInstead, if we call it in a method[self viewWillDisappear:animated]That’s when the loop really happens. Isn’t that tricky? Take a closer look.

Example 2: Solve the index, add, delete element out of bounds crash

For NSArray, NSDictionary, NSMutableArray, and NSMutableDictionary, there will inevitably be index access, add, and delete elements. The problem of crossing boundaries is also very common. In this case, we can use Method Swizzling to solve these problems. Out of bounds gives hints to prevent crashes.

NSMutableArray is used as an example

#import "NSMutableArray+Swizzling.h"
#import "NSObject+Swizzling.h"
@implementation NSMutableArray (Swizzling)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [objc_getClass("__NSArrayM") methodSwizzlingWithOriginalSelector:@selector(removeObject:) bySwizzledSelector:@selector(safeRemoveObject:) ];
        [objc_getClass("__NSArrayM") methodSwizzlingWithOriginalSelector:@selector(addObject:) bySwizzledSelector:@selector(safeAddObject:)];
        [objc_getClass("__NSArrayM") methodSwizzlingWithOriginalSelector:@selector(removeObjectAtIndex:) bySwizzledSelector:@selector(safeRemoveObjectAtIndex:)];
        [objc_getClass("__NSArrayM") methodSwizzlingWithOriginalSelector:@selector(insertObject:atIndex:) bySwizzledSelector:@selector(safeInsertObject:atIndex:)];
        [objc_getClass("__NSArrayM") methodSwizzlingWithOriginalSelector:@selector(objectAtIndex:) bySwizzledSelector:@selector(safeObjectAtIndex:)];
    });
}
- (void)safeAddObject:(id)obj {
    if (obj == nil) {
        NSLog(@"%s can add nil object into NSMutableArray", __FUNCTION__);
    } else {
        [self safeAddObject:obj];
    }
}
- (void)safeRemoveObject:(id)obj {
    if (obj == nil) {
        NSLog(@"%s call -removeObject:, but argument obj is nil", __FUNCTION__);
        return;
    }
    [self safeRemoveObject:obj];
}
- (void)safeInsertObject:(id)anObject atIndex:(NSUInteger)index {
    if (anObject == nil) {
        NSLog(@"%s can't insert nil into NSMutableArray", __FUNCTION__);
    } else if (index > self.count) {
        NSLog(@"%s index is invalid", __FUNCTION__);
    } else {
        [self safeInsertObject:anObject atIndex:index];
    }
}
- (id)safeObjectAtIndex:(NSUInteger)index {
    if (self.count == 0) {
        NSLog(@"%s can't get any object from an empty array", __FUNCTION__);
        return nil;
    }
    if (index > self.count) {
        NSLog(@"%s index out of bounds in array", __FUNCTION__);
        return nil;
    }
    return [self safeObjectAtIndex:index];
}
- (void)safeRemoveObjectAtIndex:(NSUInteger)index {
    if (self.count <= 0) {
        NSLog(@"%s can't get any object from an empty array", __FUNCTION__);
        return;
    }
    if (index >= self.count) {
        NSLog(@"%s index out of bound", __FUNCTION__);
        return;
    }
    [self safeRemoveObjectAtIndex:index];
}
@endCopy the code

Corresponding we can draw parallels, the corresponding implementation of add, delete, etc., as well as NSArray, NSDictionary and other operations, because the code is large, here will not be written. Instead of calling self, objc_getClass(“__NSArrayM”) is called. Because NSMutableArray real classes can only be retrieved from the latter, not from [self class], and method Swizzling only works on real classes. Here is a little knowledge: class cluster. Add the corresponding class cluster table of the above objects.




Class cluster table. PNG

Example 3: Prevent the button from being violently clicked repeatedly

A large number of buttons in the program have not been verified for continuous response, and there are a lot of unnecessary problems when they are clicked continuously. For example, when a user clicks a post for many times, the same post will be published for many times.

#define defaultInterval 1 @interface UIButton (Swizzling) @property (nonatomic, assign) NSTimeInterval timeInterval; // Set a single button that does not need to be assigned by hook @property (nonatomic, assign) BOOL isIgnore; @endCopy the code
#import "UIButton+Swizzling.h" #import "NSObject+Swizzling.h" @implementation UIButton (Swizzling) + (void)load { static  dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [self methodSwizzlingWithOriginalSelector:@selector(sendAction:to:forEvent:) bySwizzledSelector:@selector(sure_SendAction:to:forEvent:)]; }); } - (NSTimeInterval)timeInterval{ return [objc_getAssociatedObject(self, _cmd) doubleValue]; } - (void)setTimeInterval:(NSTimeInterval)timeInterval{ objc_setAssociatedObject(self, @selector(timeInterval), @(timeInterval), OBJC_ASSOCIATION_RETAIN_NONATOMIC); } // sure_SendAction - (void)sure_SendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event{ If (self.isignore) {hook [self sure_SendAction: Action to:target forEvent:event]; return; } if ([NSStringFromClass(self.class) isEqualToString:@"UIButton"]) { self.timeInterval =self.timeInterval == 0 ? defaultInterval:self.timeInterval; if (self.isIgnoreEvent){ return; }else if (self.timeInterval > 0){ [self performSelector:@selector(resetState) withObject:nil afterDelay:self.timeInterval]; }} methodA = methodB; methodA = methodB; So there is no infinite loop self.isIgnoreEvent = YES; [self sure_SendAction:action to:target forEvent:event]; } //runtime dynamic binding attribute - (void)setIsIgnoreEvent:(BOOL)isIgnoreEvent{// note that the BOOL type is OBJC_ASSOCIATION_RETAIN_NONATOMIC. Objc_setAssociatedObject (self, @selector(isIgnoreEvent), @(isIgnoreEvent), OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (BOOL)isIgnoreEvent{ //_cmd == @select(isIgnore); Return [objc_getAssociatedObject(self, _cmd) boolValue]; } - (void)setIsIgnore:(BOOL)isIgnore{// Objc_setAssociatedObject (self, @selector(isIgnore), @(isIgnore), OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (BOOL)isIgnore{ //_cmd == @select(isIgnore); Return [objc_getAssociatedObject(self, _cmd) boolValue]; } - (void)resetState{ [self setIsIgnoreEvent:NO]; } @endCopy the code
Example 4: Global change control initial effect

Take UILabel as an example. On the basis of a mature project, new fonts need to be introduced into the application and the default fonts of all labels need to be changed. However, some labels with special fonts need not be changed at the same time. At first glance, this problem is really very tricky. First of all, the project is relatively large, and the font workload of setting all the used labels one by one is huge. Moreover, in many dynamic display interfaces, some labels may be missed, resulting in bugs. Second, the label source in the project is not unique. It is created with code in xiB and storyBoard, which also wastes a lot of effort. Method Swizzling can solve this problem and avoid tedious operation.

#import "UILabel+Swizzling.h"
#import "NSObject+Swizzling.h"
@implementation UILabel (Swizzling)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self methodSwizzlingWithOriginalSelector:@selector(init) bySwizzledSelector:@selector(sure_Init)];
        [self methodSwizzlingWithOriginalSelector:@selector(initWithFrame:) bySwizzledSelector:@selector(sure_InitWithFrame:)];
        [self methodSwizzlingWithOriginalSelector:@selector(awakeFromNib) bySwizzledSelector:@selector(sure_AwakeFromNib)];
    });
}
- (instancetype)sure_Init{
    id __self = [self sure_Init];
    UIFont * font = [UIFont fontWithName:@"Zapfino" size:self.font.pointSize];
    if (font) {
        self.font=font;
    }
    return __self;
}
- (instancetype)sure_InitWithFrame:(CGRect)rect{
    id __self = [self sure_InitWithFrame:rect];
    UIFont * font = [UIFont fontWithName:@"Zapfino" size:self.font.pointSize];
    if (font) {
        self.font=font;
    }
    return __self;
}
- (void)sure_AwakeFromNib{
    [self sure_AwakeFromNib];
    UIFont * font = [UIFont fontWithName:@"Zapfino" size:self.font.pointSize];
    if (font) {
        self.font=font;
    }
}
@endCopy the code

In this case, I think the utilization rate may not be high. As for the design of the product, these points have been determined, and the probability of change is very low. We can also use appearance for uniform Settings.

Example 5: App hot fix

Because it takes a long time for AppStore to launch and review, and it is difficult to fix bugs in online versions, App hot repair can solve this problem. Hot fixes are updates to the online version or even modules without changing the online version. Better domestic thermal repair technology: JSPatch. The fundamental reason why JSPatch can call and rewrite OC methods through JS is that Objective-C is a dynamic language. All method calls/classes are generated at Runtime through Objective-C Runtime. We can get corresponding classes and methods through class name/method name reflection. Then replace buggy methods or add methods and so on. Bang’s blog has a detailed description for those interested, but I won’t go into it here.

Example 6: App error loading placeholder Generic Class Encapsulation (updated at: 2016/12/01)

In this function module, Runtime Method Swizzling is used to replace the reloadData Method of tableView and collectionView, so that whenever the refresh operation is performed, Automatic detection of the current number of groups and lines, so as to achieve zero code to determine whether the placeholder map display function, also applicable to network exceptions, such as detailed Settings can be read.

Example 7: Globally Modify navigation Back button (updated on: 2016/12/05)

In the real project development, a control style will be unified globally. For example, the back (back) button in the navigation bar will be fixed as the word back or displayed with pictures.

The default style of the iOS back button is as follows. By default, it is a blue left arrow and the text is the title text of the previous page.




Returns button style by default

We can still use the Runtime Method Swizzling to implement this requirement. Before using the Method Swizzling to make changes, we must take into account the precautions that do not affect the original operation as much as possible. For example, for the system default back button, The corresponding one has the function of right slide back to the edge of the interface, so we cannot make its function obsolete after unified change.

Without further ado, we create a category based on UINavigationItem and replace the method backBarButtonItem in its load method with the following code

#import "UINavigationItem+Swizzling.h" #import "NSObject+Swizzling.h" static char *kCustomBackButtonKey; @implementation UINavigationItem (Swizzling) + (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [self methodSwizzlingWithOriginalSelector:@selector(backBarButtonItem) bySwizzledSelector:@selector(sure_backBarButtonItem)]; }); } - (UIBarButtonItem*)sure_backBarButtonItem { UIBarButtonItem *backItem = [self sure_backBarButtonItem]; if (backItem) { return backItem; } backItem = objc_getAssociatedObject(self, &kCustomBackButtonKey); if (! backItem) { backItem = [[UIBarButtonItem alloc] initWithTitle:@"" style:UIBarButtonItemStylePlain target:nil action:NULL]; objc_setAssociatedObject(self, &kCustomBackButtonKey, backItem, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } return backItem; } @endCopy the code

Now run the program again and you’ll find that all the back buttons are left with the left arrow and the right swipe gesture is still valid. As is shown in




Global Unified Settings back button

I’ll write it here for now, some of it is from the Internet, and it will be updated later. Finally, or hope to see the friends can provide some of their own development examples to supplement.