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 Swizzling
application
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)load
Method, which is called automatically when the class is initially loaded. - Dispath_once ensures that it is executed only once
- First call
class_addMethod
Method 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 to
class_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