Swizzling Method Swizzling Method Swizzling Method Swizzling Method Swizzling Method Swizzling Method Swizzling

I. Introduction to Method Swizzling

The essence of Method Swizzling is to swap Method implementations (IMP) at run time, such as hook system methods, with their own business requirements inserted into the existing Method.

1. Method Swizzling principle
  • Objective-c messaging: Calling a method in Objective-C actually sends a message underneath through objc_msgSend(). And the only way to find the message is by the name of the selector method.

    // call method [obj doSomething]; //[obj doSomething] essentially sends a doSomething message to obj objc_msgSend(obj,@selector(doSomething))Copy the code
  • Each OC instance object holds an ISA pointer and an instance variable, where the ISA pointer belongs to a class that maintains a MethodLists that can be received at runtime; MethodLists hold the mapping between the selector Method name and the Method implementation (IMP, a pointer to the Method implementation). At run time, through selecter to find matching IMP, to find the specific implementation function.





MethodLists schematic. PNG

  • Development can use the dynamic characteristics of Objective-C, at run time to replace the selector corresponding method implementation (IMP), to achieve the purpose of hook. The following figure shows a list of methods using Method Swizzling instead of selector IMP.





Hook after MethodLists schematic. PNG

2. Method Swizzling

Method Swizzling is essentially an IMP that skips a selector. Here is a simple example of Swizzle NSObject’s description Method:

#import <objc/runtime.h> @implementation NSObject (Swizzle) + (void)load{// transpose IMP Method originalMethod = class_getInstanceMethod([NSObject class], @selector(description)); Method myMethod = class_getInstanceMethod([NSObject class], @selector(qs_description)); method_exchangeImplementations(originalMethod, myMethod); } - (void)qs_description{NSLog(@"description is Swizzle "); return [self qs_description]; } @endCopy the code

Description: Call description hook method, before obtaining the content, will print “description was Swizzle” such a log.

3. Problems existing in Method Swizzling
  • Thread-safe (Method swizzling is not atomic)
  • Changes to the behavior of unowned code
  • Possible Naming conflicts
  • Swizzling changes the method’s arguments
  • The Order of Swizzles Matters
  • It is Difficult to understand.
  • Difficult to debug

Second, RSSwizzle: Method Swizzling elegant scheme

RSSwizzle is a thread-safe Method Swizzling solution, which can help us solve the problem of using Method Swizzling. The introduction is as follows:

1. Method swizzling is not atomic
  • Method implementations are usually swapped in the LOAD method, and thread-safety concerns may need to be considered if they are swapped at other times.

  • RSSwizzle makes use of the spin lock OSSpinLock to ensure thread safety. The method implementation can be swapped at any time.

Changes to the behavior of unowned code
  • That’s what Swizzle is aiming for. But in the Swizzle method, we avoided most of the problems by keeping the good habit of * calling the original implementation. We typically use Swizzle to add some of our own business needs to the original implementation without intentionally breaking it.

  • RSSwizzle provides a call to the original implementation of the macro RSSWCallOriginal, very convenient.

3, Possible naming conflicts #####
  • Naming conflicts can be largely avoided by prefixing the name of the replacement method.

  • RSSwizzle completes method substitution in the static method of custom swizzle, completely avoiding naming conflicts.

Swizzling changes the method’s arguments
  • The _cmd argument has been tampered with, and the normal method for calling Swizzle is defective.

    // Call the method [self qs_setFrame:frame]; Objc_msgSend (self, @selector(qs_setFrame:), frame);Copy the code

    At runtime, look for qs_setFrame: method implementation, _cmd argument although qs_setFrame:, but actually find the original setFrame: method implementation.

  • RSSwizzle’s custom swizzle static method solves this problem.

5. The Order of Swizzles Matters
  • When the object Swizzle of multiple inherited classes starts with the parent object. This ensures that the subclass method gets the implementation of the Swizzle in the parent class.

  • Swizzle doesn’t have to worry about this in load because the load class methods are called from the parent class by default.

6. It is Difficult to understand.
  • It’s basically calling the original implementation, which looks recursive and kind of muddled.

  • The macro RSSWCallOriginal provided by RSSwizzle makes it easier to call the original implementation and make the code more readable.

7. Difficult to debug
  • Debug backtrace(backtrace) printed, mixed with the method name swizzle, looks messy, so it is important to name it clearly.

  • RSSwizzle print out of the name is very clear, in addition to what Swizzle, the best documented.

Three, the basic use of RSSwizzle

RSSwizzle provides two ways to use, one is to call the class method to achieve the function replacement, the other is to use RSSwizzle defined macro to carry out the function replacement.

1. Replace instance method implementations with class methods
/** Argument 1: the function selector to be replaced 2: the class argument of the function to be replaced 3: the method to be replaced is returned in the block argument. The function to be replaced must be of the same type as the original function. By default, all functions in a class have an id parameter called self. Parameter 4: SwizzleInstanceMethod :@selector(touchesBegan:withEvent:) inClass:[ViewController class] newImpFactory:^id(RSSwizzleInfo *swizzleInfo) { return ^(__unsafe_unretained id self,NSSet* touches,UIEvent* event){ NSLog(@"touchesBegan:withEvent: Swizzle ");};} mode:RSSwizzleModeAlways key:NULL];Copy the code
2. Use macro to replace instance method
/* Argument 1: class argument 2 of the function to be replaced: Function selector to be replaced Parameter 3: return value type, parameter 4: Parameter list Parameter 5: code block to be replaced, parameter 6: execution mode, parameter 7: Key value identification, RSSwizzleModeOncePerClass mode is used, NULL */ RSSwizzleInstanceMethod([ViewController class], @selector(touchesEnded:withEvent:), RSSWReturnType(void), RSSWArguments(NSSet<UITouch *> *touches,UIEvent *event),RSSWReplacement({NSLog(@"touchesEnded:withEvent is Swizzle "); RSSWCallOriginal(touches,event); }), RSSwizzleModeAlways, NULL);Copy the code
3. Replace class method implementations with class methods
/* Argument 1: the function selector argument to be replaced 2: the class argument to be replaced 3: the method to be replaced is returned in the block argument. The function to be replaced must be the same type as the original function. By default, all functions in a class have an id parameter called self */ [RSSwizzle swizzleClassMethod:@selector(testClass1) inClass:[ViewController class] newImpFactory:^id(RSSwizzleInfo *swizzleInfo) { return ^(__unsafe_unretained id self){ NSLog(@"Class testClassMethod1 Swizzle"); }; }];Copy the code
4. Use macros to replace class methods
/* Parameter 1: the class parameter of the method to be replaced 2: the method selector parameter to be replaced 3: the return value type parameter of the method 4: the parameter list of the method 5: Method block to replace */ RSSwizzleClassMethod(NSClassFromString(@"ViewController"), NSSelectorFromString(@" testClass2 "), RSSWReturnType(void), RSSWArguments(), RSSWReplacement({// Do the original method RSSWCallOriginal(); NSLog(@"Class testClassMethod2 Swizzle"); }));Copy the code

RSSwizzle also provides Swizzle mode, the use of Swizzle instance method needs to be used. The Swizzle class method, default RSSwizzleModeAlways, is defined as follows:

Typedef NS_ENUM(NSUInteger, RSSwizzleMode) {// Always replace RSSwizzleModeAlways = 0, / / the same key logo replace operation will be performed only once RSSwizzleModeOncePerClass = 1, / / replace operation in a subclass of the same key logo will be executed only once in the parent class RSSwizzleModeOncePerClassAndSuperclasses = 2};Copy the code

4. A typical wrong case of using Swizzling

Many blogs on the web have introduced using Swizzling to prevent repeated clicks on UIButton, but most have problems.

Error code

Replace the sendAction:to:forEvent: method in load with the following code:

+ (void)load {
    Method before   = class_getInstanceMethod(self, @selector(sendAction:to:forEvent:));
    Method after    = class_getInstanceMethod(self, @selector(qs_sendAction:to:forEvent:));
    method_exchangeImplementations(before, after);
}

- (void)qs_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
    if ([NSDate date].timeIntervalSince1970 - self.qs_acceptEventTime < self.qs_acceptEventInterval) {
        return;
    }

    if (self.qs_acceptEventInterval > 0) {
        self.qs_acceptEventTime = [NSDate date].timeIntervalSince1970;
    } 
    [self qs_sendAction:action to:target forEvent:event];
 }
Copy the code

Error symptoms:

[UITabBarButton qs_acceptEventTime]: unrecognized selector sent to instance… .

Error cause:

UITabBarButton is a private property of UITabBar. UITabBarButton’s parent is UIControl, and UIButton’s parent is UIControl, SendAction :to:forEvent: is an instance method of UIControl;

SendAction :to:forEvent: is not implemented in UIButton class. Class_getInstanceMethod () is the Method object of the parent class. Using method_exchangeImplementations() swaps the original implementation of the superclass (IMP) with my own Swizzle implementation. This causes other subclasses of UIControl, such as UITabBarButton, to call the Swizzle implementation of UIButton when clicked, causing severe Crash problems.

Note: Although the load method in UIControl classification exchange method implementation, can solve the problem, we will Swizzling the impact of many times, is not ideal practice. Here is a solution.

2. Solutions

Using method_exchangeImplementations directly in a project is dangerous and could even cause Crash, so that’s not recommended in a project. There are two possible solutions:

Method A

Principle: If you don’t have an Original Selector in your class, you add a Swizzle implementation to the Original Selector using class_addMethod, Change the implementation of Swizzle Selector to the implementation of Original by class_replaceMethod; Method_exchangeImplementations is only used if there is already a method corresponding to the Original Selector (adding with the class_addMethod method fails).

The code is as follows:

+ (void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ Class class = [self class]; SEL originalSelector = @selector(sendAction:to:forEvent:); SEL swizzledSelector = @selector(qs_sendAction:to:forEvent:); Method originalMethod = class_getInstanceMethod(class, originalSelector); Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector); BOOL success = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); if (success) { class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else { method_exchangeImplementations(originalMethod, swizzledMethod); }}); }Copy the code

Note 1: The class_addMethod method can add a new method implementation (IMP) to a class. Otherwise, return NO. If the select already has a corresponding method implementation (IMP), the addition also fails. Use this to check whether the active method implementation is possible. If the swizzledSelector and originalMethod are not set properly using class_replaceMethod, the swizzledSelector and originalMethod are set properly.

Note 2:.class_replacemethod replaces the method implementation in a class, calling the class_addMethod and method_setImplementation methods (setting the IMP of a method directly).

Method B

RSSwizzle perfectly avoids the awkward use of method_exchangeImplementations swap method in load, and controls the implementation of the replacement method based on Swizzle mode and class_replaceMethod.

The code is as follows:

+ (void)load{
    RSSwizzleInstanceMethod([UIButton class], @selector(sendAction:to:forEvent:), RSSWReturnType(void), RSSWArguments(SEL action,id target,UIEvent *event), RSSWReplacement({
           UIButton *btn = self;
            if ([NSDate date].timeIntervalSince1970 - btn.qs_acceptEventTime < btn.qs_acceptEventInterval) {
                return;
            }      
            if (btn.qs_acceptEventInterval > 0) {
                btn.qs_acceptEventTime = [NSDate date].timeIntervalSince1970;
            }        
            RSSWCallOriginal(action,target,event);    
    }), RSSwizzleModeAlways, NULL);
}
Copy the code

RSSwizzleInstanceMethod macro implementation method to achieve the replacement, the code is easier to read.

End

  • The Demo address QSSwizzleKitDemo

  • Swizzling Objective-C Method Swizzling Objective-C Method Swizzling

  • I am a South China coder, a beginner iOS programmer. IOS notes is a bit of my study notes, inadequacies, hope criticism and correction.