This article Demo portal: MethodSwizzlingDemo
Abstract: Programming, only understand the principle of not, must be actual combat to know the application scenario. This article is a methodology interchange in a series that attempts to explain runtime theory and introduce some scenarios. In this paper, the first section will introduce method exchange and attention points, the second section will summarize the API related to method exchange, and the third section will introduce several practical scenarios of method exchange: counting VC loading times and printing, preventing UI controls from activating events for many times in a short time, and preventing burst processing (array out-of-bounds problem).
1. Principles and attention
The principle of
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.
usage
Add a Category to the class of the Method you want to replace, and then add the Method Swizzling Method to the +(void)load Method in the Category. The Method we used to replace is also in this Category.
Since the Load class method is a method that is called when the program is running when the class is loaded into memory, it executes early and does not require us to call it manually.
Pay attention to the point
- Swizzling should always be performed in +load
- Swizzling should always be performed in dispatch_once
- Do not call [super Load] when Swizzling is executed in +load. If [super Load] is called more than once, it may appear that “Swizzle is invalid”.
- To avoid Swizzling’s code being executed repeatedly, we can use GCD’s dispatch_once function to take advantage of the fact that the code is executed only once.
2. Method Swizzling related API
- Plan 1
class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name)
Copy the code
method_getImplementation(Method _Nonnull m)
Copy the code
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp,
const char * _Nullable types)
Copy the code
class_replaceMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp,
const char * _Nullable types)
Copy the code
- Scheme 2
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2)
Copy the code
3. Application scenarios and practices
3.1 Count the VC loading times and print them
- UIViewController+Logging.m
#import "UIViewController+Logging.h"
#import <objc/runtime.h>
@implementation UIViewController (Logging)
+ (void)load
{
swizzleMethod([self class], @selector(viewDidAppear:), @selector(swizzled_viewDidAppear:));
}
- (void)swizzled_viewDidAppear:(BOOL)animated
{
// call original implementation
[self swizzled_viewDidAppear:animated];
// Logging
NSLog(@"% @", NSStringFromClass([self class]));
}
void swizzleMethod(Class class, SEL originalSelector, SEL swizzledSelector)
{
// the method might not exist in the class, but in its superclass
Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
// class_addMethod will fail if original method already exists
BOOL didAddMethod = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
// the method doesn’t exist and we just added one
if (didAddMethod) {
class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
}
else{ method_exchangeImplementations(originalMethod, swizzledMethod); }}Copy the code
3.2 Preventing multiple activation events of UI controls in a short period of time
demand
Buttons written in the current project are not globally controlled for short periods of time when they are not clicked continuously (perhaps sporadically before some network request interface). Now comes the new requirement: all buttons of this APP cannot be clicked continuously within 1 second. What do you do? One by one? This kind of inefficiency and low maintenance is definitely not right.
plan
Add a category to the button, and add a click event interval attribute, execute the click event to determine whether the time is up, if not, then block the click event.
How do you block click events? In fact, the click event in the Runtime is to send a message, we can exchange the SEL of the message to be sent with the SEL written by ourselves, and then determine whether to execute the click event in the SEL written by ourselves.
practice
UIButton is a subclass of UIControl, so just create a new category based on UIControl
- UIControl+Limit.m
#import "UIControl+Limit.h"
#import <objc/runtime.h>
static const char *UIControl_acceptEventInterval="UIControl_acceptEventInterval";
static const char *UIControl_ignoreEvent="UIControl_ignoreEvent";
@implementation UIControl (Limit)
#pragma mark - acceptEventInterval
- (void)setAcceptEventInterval:(NSTimeInterval)acceptEventInterval
{
objc_setAssociatedObject(self,UIControl_acceptEventInterval, @(acceptEventInterval), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
-(NSTimeInterval)acceptEventInterval {
return [objc_getAssociatedObject(self,UIControl_acceptEventInterval) doubleValue];
}
#pragma mark - ignoreEvent
-(void)setIgnoreEvent:(BOOL)ignoreEvent{
objc_setAssociatedObject(self,UIControl_ignoreEvent, @(ignoreEvent), OBJC_ASSOCIATION_ASSIGN);
}
-(BOOL)ignoreEvent{
return [objc_getAssociatedObject(self,UIControl_ignoreEvent) boolValue];
}
#pragma mark - Swizzling
+(void)load {
Method a = class_getInstanceMethod(self,@selector(sendAction:to:forEvent:));
Method b = class_getInstanceMethod(self,@selector(swizzled_sendAction:to:forEvent:)); method_exchangeImplementations(a, b); } - (void)swizzled_sendAction:(SEL)action to:(id)targetforEvent:(UIEvent*)event
{
if(self.ignoreEvent){
NSLog(@"btnAction is intercepted");
return; }if(self.acceptEventInterval>0){
self.ignoreEvent=YES;
[self performSelector:@selector(setIgnoreEventWithNo) withObject:nil afterDelay:self.acceptEventInterval];
}
[self swizzled_sendAction:action to:target forEvent:event];
}
-(void)setIgnoreEventWithNo{
self.ignoreEvent=NO;
}
@end
Copy the code
- ViewController.m
-(void)setupSubViews{ UIButton *btn = [UIButton new]; BTN = [[UIButton alloc] initWithFrame: CGRectMake (100100100, 40)]; [btnsetTitle:@"btnTest"forState:UIControlStateNormal];
[btn setTitleColor:[UIColor redColor]forState:UIControlStateNormal];
btn.acceptEventInterval = 3;
[self.view addSubview:btn];
[btn addTarget:self action:@selector(btnAction)forControlEvents:UIControlEventTouchUpInside];
}
- (void)btnAction{
NSLog(@"btnAction is executed");
}
Copy the code
3.3 Anti-breach processing: The array is out of bounds
demand
In the real world, there may be some places (such as fetching network response data) where an array of NSArray fetching data is performed, and the previous little brother is not protected from crossing the boundary. The tester accidentally failed to detect an out-of-bounds array crash (because the data returned was dynamic), and assumed there was no problem, but there was a hidden risk of a production accident.
At this time, the person in charge of the APP said, even if the APP does not work, it cannot Crash, which is the lowest bottom line. So do you have a way to intercept this pair of arrays in case of an out-of-bounds crash?
Swizzling the objectAtIndex: method of NSArray instead of a method with processing logic. The problem, however, is that the Swizzling of class clusters is not that simple.
Class cluster
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 NSArray, you must obtain its “real body” ** to Swizzling, direct operation on 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;
The name of the class | The bard |
---|---|
NSArray | __NSArrayI |
NSMutableArray | __NSArrayM |
NSDictionary | __NSDictionaryI |
NSMutableDictionary | __NSDictionaryM |
practice
Ok, create a new category, directly use the code implementation, see how to retrieve the real body:
- NSArray+CrashHandle.m
@implementation NSArray (CrashHandle) // Swizzling core code // note that many students feedback the following code does not work, cause this problem is 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 fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(cm_objectAtIndex:)); method_exchangeImplementations(fromMethod, toMethod); } // to avoid collisions with the system method, I usually prefix the swizzling method with - (id)cm_objectAtIndex:(NSUInteger)index {// determine whether the subscript is out of bounds, if it is out of bounds into exception interceptionif (self.count-1 < index) {
@try {
return[self cm_objectAtIndex:index]; } @catch (NSException *exception) {// Crash information 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]; }}Copy the code
– (id)cm_objectAtIndex:(NSUInteger)index {call itself? Is it recursion? Not really. Cm_objectAtIndex IMP cm_objectAtIndex IMP cm_objectAtIndex IMP So it’s not recursion.
- ViewController.m
- (void)viewDidLoad { [super viewDidLoad]; NSArray *array = @[@0, @1, @2, @3]; [array objectAtIndex:3]; [array objectAtIndex:4]; }Copy the code
After running it, it found no crash and printed the relevant information, as shown below.