About the Method Swizzling

Recently, I am interested in some APM systems of ios, so I have studied some related technologies. Let’s start with the most basic Method Swizzling.

Method Swizzling is a dynamic replacement Method implementation technique provided by THE OC Runtime. We can use it to replace the system or our custom class Method implementation for our special purpose.

Code address – Github: MethodSwizzling

Method Swizzling principle

Why can Method Swizzling replace a Method of a class? The first thing we need to understand is how the substitution works.

Methods in OC are defined as follows in runtime.h:

struct objc_method{
    SEL method_name                                          OBJC2_UNAVAILABLE;
    char *method_types                                       OBJC2_UNAVAILABLE;
    IMP method_imp                                           OBJC2_UNAVAILABLE;
}                                                            OBJC2_UNAVAILABLE;
}
Copy the code
  • Method_name: method name
  • Method_types: Method types that store the parameter types and return value types of the method
  • Method_imp: Implementation of a method, function pointer

We can also see that the OC method name does not include parameter types, which means that the following two methods are the same method as the Runtime:

- (void)viewWillAppear:(BOOL)animated;
- (void)viewWillAppear:(NSString *)string;
Copy the code

In principle, the Method name method_name and Method implementation method_IMP correspond one to one, and Method Swizzling’s principle is to dynamically change their correspondence to replace Method implementations.

Method Swizzlingapplication

Runtime functions associated with method replacement

class_getInstanceMethod

OBJC_EXPORT Method _Nullable
class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name);
Copy the code
  • Gets an instance method of a class
  • CLS: The class of the method
  • Name: The name of the selector (the selector is the method name)

class_getClassMethod

OBJC_EXPORT Method _Nullable
class_getClassMethod(Class _Nullable cls, SEL _Nonnull name);
Copy the code
  • Gets the class method of a class
  • CLS: The class of the method
  • Name: Selects the child name

method_getImplementation

OBJC_EXPORT IMP _Nonnull
method_getImplementation(Method _Nonnull m);
Copy the code
  • Gets a pointer to a method based on the method
  • M: method

method_getTypeEncoding

OBJC_EXPORT const char * _Nullable
method_getTypeEncoding(Method _Nonnull m);
Copy the code
  • Function: Gets the parameter and return value type description of a method
  • M: method

class_addMethod

OBJC_EXPORT BOOL
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, 
                const char * _Nullable types);
Copy the code
  • Adds a new method and its implementation to a class
  • Return value: yes, the port is added successfully. No: indicates that the add fails
  • CLS: The class to which methods will be added
  • Name: the name of the method to be added
  • Imp: a pointer that implements this method
  • Types: Return values and parameters of the method to be added

method_exchangeImplementations

OBJC_EXPORT void
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2);
Copy the code
  • Swap the two methods

class_replaceMethod

OBJC_EXPORT IMP _Nullable
class_replaceMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, 
                    const char * _Nullable types) ;
Copy the code
  • Action: Specifies the implementation of the replacement method
  • CLS: The class that will replace the method
  • Name: the name of the method to be replaced
  • Imp: Pointer to the new method
  • Types: Return values and parameter descriptions of the new method

Replace an instance method of a class

Eg: Replace the viewDidLoad method in UIViewController.

#import "UIViewController+MI.h"
#import <objc/runtime.h>@implementation UIViewController (MI) + (void)load{static dispatch_once_t onceToken;  dispatch_once(&onceToken, ^{ SEL origin_selector = @selector(viewDidLoad); SEL swizzed_selector = @selector(mi_viewDidLoad); Method origin_method = class_getInstanceMethod([self class], origin_selector); Method swizzed_method = class_getInstanceMethod([self class], swizzed_selector); BOOL did_add_method = class_addMethod([self class], origin_selector, method_getImplementation(swizzed_method), method_getTypeEncoding(swizzed_method));if (did_add_method) {
            NSLog(@"DebugMsg: ViewController class does not have viewDidLoad methods (probably in its parent H), so add and replace.");
            class_replaceMethod([self class],
                                swizzed_selector,
                                method_getImplementation(origin_method),
                                method_getTypeEncoding(origin_method));
        }else{
            NSLog(@"DebugMsg: Direct exchange method"); method_exchangeImplementations(origin_method, swizzed_method); }}); } - (void)mi_viewDidLoad { [self mi_viewDidLoad]; NSLog(@"DebugMsg: Successful replacement");
}
Copy the code

A brief explanation of the above code:

  • In the category of+(void)loadMethod, which is called automatically when the class is initially loaded.
  • Dispath_once ensures that it is executed only once
  • First callclass_addMethodMethod to ensure that the replacement succeeds even if a method exists in the parent class

The replacement succeeded. Console information:

The 2019-04-17 17:25:16. 937849 + 0800 MethodSwizzling [4975-639584] debugMsg: MethodSwizzling[4975:639584] : The replacement is successfulCopy the code

Replace an instance method of one class with another

When we have a private class library (we don’t know the header file of the class, we know the class exists and we know one of the methods in the class), we need to hook the methods of this class into a new class.

Eg: We need to hook the person class with a speak: method method:

#import "Person.h"

@implementation Person

- (void)speak:(NSString *)language
{
    NSLog(@"person speak language: %@",language);
}

+ (void)sleep:(NSUInteger)hour
{
    NSLog(@"person sleep: %lu",hour);
}

@end
Copy the code

We create ChinesePerson, hook Speak: method into ChinesePerson.

#import "ChinesePerson.h"
#import <objc/runtime.h>

@implementation ChinesePerson

+ (void)load
{
    Class origin_class  = NSClassFromString(@"Person");
    Class swizzed_class = [self class];
    
    SEL origin_selector = NSSelectorFromString(@"speak:");
    SEL swizzed_selector = NSSelectorFromString(@"mi_speak:");
    
    Method origin_method = class_getInstanceMethod(origin_class, origin_selector);
    Method swizzed_method = class_getInstanceMethod(swizzed_class, swizzed_selector);
    
    BOOL add_method = class_addMethod(origin_class,
                                      swizzed_selector,
                                      method_getImplementation(swizzed_method),
                                      method_getTypeEncoding(swizzed_method));
    if(! add_method) {return;
    }
    
    swizzed_method = class_getInstanceMethod(origin_class, swizzed_selector);
    if(! swizzed_method) {return;
    }
    
    BOOL did_add_method = class_addMethod(origin_class,
                                          origin_selector,
                                          method_getImplementation(swizzed_method),
                                          method_getTypeEncoding(swizzed_method));
    if (did_add_method) {
        class_replaceMethod(origin_class,
                            swizzed_selector,
                            method_getImplementation(origin_method),
                            method_getTypeEncoding(origin_method));
    }else{
        method_exchangeImplementations(origin_method, swizzed_method);
    }
    
}

- (void)mi_speak:(NSString *)language
{
    if ([language isEqualToString:@"Chinese"]) { [self mi_speak:language]; }}Copy the code

The replacement succeeded. Console information (Chinese only) :

[4975:639584] Person Speak Language: ChineseCopy the code

Substitution class method

Eg: We replace the sleep: method in the Person class:

#import "Person+MI.h"
#import <objc/runtime.h>

@implementation Person (MI)
+ (void)load
{
    Class class = [self class];
    SEL origin_selector  = @selector(sleep:);
    SEL swizzed_selector = @selector(mi_sleep:);
    
    Method origin_method = class_getClassMethod(class, origin_selector);
    Method swizzed_method = class_getClassMethod(class,swizzed_selector);
    
    if(! origin_method || ! swizzed_method) {return; } IMP origin_imp = method_getImplementation(origin_method); IMP swizzed_imp = method_getImplementation(swizzed_method); const char* origin_type = method_getTypeEncoding(origin_method); const char* swizzed_type = method_getTypeEncoding(swizzed_method); // add method to MetaClass Class meta_class = objc_getMetaClass(class_getName(Class)); class_replaceMethod(meta_class, swizzed_selector, origin_imp, origin_type); class_replaceMethod(meta_class, origin_selector, swizzed_imp, swizzed_type); } + (void)mi_sleep:(NSUInteger)hour {if (hour >= 7) {
        [self mi_sleep:hour];
    }
}
@end

Copy the code

Console printing (sleep is greater than or equal to 7 hours to print —- call for healthy sleep) :

MethodSwizzling[4975.639584] Person sleep: 8Copy the code

There are two differences between a class method hook and an instance method hook:

  • The Method to get Method is changed toclass_getClassMethod(Class cls, SEL name), notclass_getInstanceMethod(Class cls, SEL name);
  • For dynamic addition of class methods, methods need to be added to MetaClass because instance methods are recorded in the method-list of the class and class methods are recorded in the method-list of the meta-class.

Replace methods in a class cluster

#import "MIMutableDictionary.h"
#import <objc/runtime.h>

@implementation MIMutableDictionary

+ (void)load
{
    Class origin_class = NSClassFromString(@"__NSDictionaryM");
    Class swizzed_class = [self class];
    SEL origin_selector = @selector(setObject:forKey:);
    SEL swizzed_selector = @selector(mi_setObject:forKey:);
    Method origin_method = class_getInstanceMethod(origin_class, origin_selector);
    Method swizzed_method = class_getInstanceMethod(swizzed_class, swizzed_selector);
    IMP origin_imp = method_getImplementation(origin_method);
    IMP swizzed_imp = method_getImplementation(swizzed_method);
    const char* origin_type = method_getTypeEncoding(origin_method);
    const char* swizzed_type = method_getTypeEncoding(swizzed_method);

    class_replaceMethod(origin_class, swizzed_selector, origin_imp, origin_type);
    class_replaceMethod(origin_class, origin_selector, swizzed_imp, swizzed_type);
}

- (void)mi_setObject:(id)objContent forKey:(id<NSCopying>)keyContent
{
    if (objContent && keyContent) {
        NSLog(@"Executed in.");
        [self mi_setObject:objContent forKey:keyContent];
    }
}


@end
Copy the code

application

It is not recommended to use Method Swizzling too much in the project, otherwise the native classes will be very messy hook, the project will be very difficult to locate the problem. There was an article that said it was a cancer on ios. Cancer of iOS -MethodSwizzling

Nonetheless, let’s get a feel for the technology by learning and combing through its application scenarios.

Prevent array values from crashing

Not only arrays, NSDictionary uses runtime to prevent crashes in the same way.

#import "NSArray+Safe.h"
#import <objc/runtime.h>@implementation NSArray (Safe) + (void)load { // objectAtIndex: Origin_method = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
    Method replaced_method = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(safeObjectAtIndex:));
    method_exchangeImplementations(origin_method, replaced_method);
    
    Method origin_method_muta = class_getInstanceMethod(objc_getClass("__NSArrayM"), @selector(objectAtIndex:));
    Method replaced_method_muta = class_getInstanceMethod(objc_getClass("__NSArrayM"), @selector(safeMutableObjectAtIndex:)); method_exchangeImplementations(origin_method_muta, replaced_method_muta); Method origin_method_sub = class_getInstanceMethod(objc_getClass())"__NSArrayI"), @selector(objectAtIndexedSubscript:));
    Method replaced_method_sub = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(safeObjectAtIndexedSubscript:));
    method_exchangeImplementations(origin_method_sub, replaced_method_sub);
    
    Method origin_method_muta_sub = class_getInstanceMethod(objc_getClass("__NSArrayM"), @selector(objectAtIndexedSubscript:));
    Method replaced_method_muta_sub = class_getInstanceMethod(objc_getClass("__NSArrayM"), @selector(safeMutableObjectAtIndexedSubscript:));
    method_exchangeImplementations(origin_method_muta_sub, replaced_method_muta_sub);
    
}

- (id)safeObjectAtIndex:(NSUInteger)index
{
    if (self.count > index && self.count) {
        return [self safeObjectAtIndex:index];
    }
    NSLog(@"ErrorMsg: Array [NSArray] out of bounds...");
    return nil;
}

- (id)safeMutableObjectAtIndex:(NSUInteger)index
{
    if (self.count > index && self.count) {
        return [self safeMutableObjectAtIndex:index];
    }
    NSLog(@"ErrorMsg: Array [NSMutableArray] out of bounds...");
    return nil;
}

-(id)safeObjectAtIndexedSubscript:(NSUInteger)index
{
    if (self.count > index && self.count) {
        return [self safeObjectAtIndexedSubscript:index];
    }
    NSLog(@"ErrorMsg: Array [NSArray] out of bounds...");
    return nil;
}

- (id)safeMutableObjectAtIndexedSubscript:(NSUInteger)index
{
    if (self.count > index && self.count) {
        return [self safeMutableObjectAtIndexedSubscript:index];
    }
    NSLog(@"ErrorMsg: Array [NSMutableArray] out of bounds...");
    return nil;
}

@end

Copy the code

Use:

- (void)test2
{
    NSArray *arr = @[@"a"The @"b"The @"c"The @"d"The @"e"The @"f"];
    NSLog(@"AtIndex mode: %@",[arr objectAtIndex:10]);
    NSLog(@"Subscript mode: %@",arr[10]);
}
Copy the code

Console output log:

[25379:1703659] errorMsg: Array [NSArray] is out of line... MethodSwizzling[25379:1703659] atIndex method: (null) 2019-04-18 19:14:18.139793+0800 MethodSwizzling[25379:1703659] errorMsg: Array [NSArray] is out of line... 139868+0800 MethodSwizzling[25379:1703659] subscript: (null)Copy the code

Change the size of all buttons in your app

The general approach is to iterate through all the subviews of a view, changing all the buttons as a whole. At this point we use Runtime to resize the button.

#import "UIButton+Size.h"
#import <objc/runtime.h>@implementation UIButton (Size) + (void)load { static dispatch_once_t onceToken; Origin_method = class_getInstanceMethod([self class], @selector(dispatch_once(&oncetoken, ^{origin_method = class_getInstanceMethod([self class], @selector(setFrame:));
        Method replaced_method = class_getInstanceMethod([self class], @selector(miSetFrame:));
        method_exchangeImplementations(origin_method, replaced_method);
        
    });
   
}

- (void)miSetFrame:(CGRect)frame
{
    frame = CGRectMake(frame.origin.x, frame.origin.y, frame.size.width+20, frame.size.height+20);
    NSLog(@"Set button size to take effect");
    [self miSetFrame:frame];
}

@end
Copy the code

Handle button repeated clicks

If you click the same button repeatedly too quickly, the event bound to the button will be triggered multiple times. There are many ways to deal with this case, and Method Swillzing can also solve this problem.

.h files:

#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface UIButton (QuickClick)
@property (nonatomic,assign) NSTimeInterval delayTime;
@end

NS_ASSUME_NONNULL_END
Copy the code
#import "UIButton+QuickClick.h"
#import <objc/runtime.h>

@implementation UIButton (QuickClick)
static const char* delayTime_str = "delayTime_str";

+ (void)load
{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Method originMethod =   class_getInstanceMethod(self, @selector(sendAction:to:forEvent:));
        Method replacedMethod = class_getInstanceMethod(self, @selector(miSendAction:to:forEvent:));
        method_exchangeImplementations(originMethod, replacedMethod);
    });
}

- (void)miSendAction:(nonnull SEL)action to:(id)target forEvent:(UIEvent *)event
{
    if (self.delayTime > 0) {
        if (self.userInteractionEnabled) {
            [self miSendAction:action to:target forEvent:event];
        }
        self.userInteractionEnabled = NO;
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
                                     (int64_t)(self.delayTime * NSEC_PER_SEC)),
                                     dispatch_get_main_queue(), ^{
                                         self.userInteractionEnabled = YES;
                                     });
    }else{
        [self miSendAction:action to:target forEvent:event];
    }
}

- (NSTimeInterval)delayTime
{
    return [objc_getAssociatedObject(self, delayTime_str) doubleValue];
}

- (void)setDelayTime:(NSTimeInterval)delayTime
{
    objc_setAssociatedObject(self, delayTime_str, @(delayTime), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

@end
Copy the code

code

Making: MethodSwizzling

reference

  • Method Swizzling’s various poses
  • Objective-c Method Swizzling’s best practices
  • Method Swizzling
  • Cancer of iOS -MethodSwizzling