iOS Development·Runtime Principles and Practice: Method Swizzling (iOS "Black Magic", burying point statistics, prohibiting continuous clicking of UI controls, anti-crash processing)

This article's Demo portal: MethodSwizzlingDemo

Abstract : Programming, only understanding the principle is not enough, you must actually fight to know the application scenario. This series attempts to explain runtime-related theories while introducing some practical scenarios, and this article is the method exchange of this series . In this article , 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: count the number of VC loads and print them to prevent UI controls from being short. Time to activate the event multiple times, anti-crash processing (array out-of-bounds problem).

1. Principles and Notes

principle

Method Swizzing occurs at runtime, and is mainly used to exchange two Methods at runtime. We can write the Method Swizzling code anywhere, but the swap will only work after this Method Swizzling code is executed.

usage

First add a Category to the class of the method to be replaced, and then +(void)loadadd the Method Swizzling method to the method in the Category. The method we use to replace is also written in this Category.

Since the load class method is a method that is called when the class is loaded into memory when the program is running, it is executed earlier and does not need to be called manually.

Points to Note

  • Swizzling should always be executed in +load
  • Swizzling should always be executed in dispatch_once
  • Do not call [super load] when Swizzling is executed in +load. If [super load] is called multiple times, the illusion of "Swizzle is invalid" may appear.
  • In order to avoid the repeated execution of the Swizzling code, we can solve it through the dispatch_once function of GCD, using the feature that the code in the dispatch_once function will only be executed once.

2. APIs related to Method Swizzling

  • plan 1
class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name)
method_getImplementation(Method _Nonnull m) 
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, 
                const char * _Nullable types) 
class_replaceMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, 
                    const char * _Nullable types) 
  • Scenario 2
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2) 

3. Application scenarios and practices

3.1 Count the number of VC loads and print

  • 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);
    }
    
}

3.2 Prevent UI controls from activating events multiple times in a short time

need

The buttons written in the current project have not yet been globally controlled that they cannot be clicked continuously for a short period of time (maybe some control has been done sporadically before some network request interfaces). Now comes a new requirement: all buttons in this APP cannot be clicked continuously within 1 second. How do you do it? Change one by one? This kind of low efficiency and low maintenance is definitely inappropriate.

plan

Add a category to the button, and add an attribute of the click event interval. When executing the click event, judge whether the time is up. If the time is not enough, then intercept the click event.

How to intercept click events? In fact, the click event is to send a message in the runtime. We can exchange the SEL of the message to be sent with the SEL written by ourselves, and then judge whether to execute the click event in the SEL written by ourselves.

practice

UIButton is a subclass of UIControl, so you can create a new category according to 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)target forEvent:(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
  • ViewController.m
-(void)setupSubViews{
    
    UIButton *btn = [UIButton new];
    btn =[[UIButton alloc]initWithFrame:CGRectMake(100,100,100,40)];
    [btn setTitle:@"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");
}

3.3 Anti-crash handling: array out-of-bounds problem

need

In the actual project, the operation of fetching data from the array NSArray may be performed in some places (such as fetching network response data), and the previous little buddies did not perform anti-cross-border processing. The tester accidentally did not detect that the array crashed when the array was out of bounds (because the returned data was dynamic), and it turned out that there was no problem. In fact, it also hidden the risk of production accidents.

At this time, the person in charge of the APP said that even if the APP cannot work, it cannot crash, which is the lowest bottom line. So, do you have a way to intercept the crash when the array is out of bounds?

Idea: objectAtIndex:Swizzling the method of NSArray and replace a method with processing logic. However, there is still a problem at this time, that is, the Swizzling of class clusters is not so simple.

cluster

In iOS, these classes such as NSNumber, NSArray, NSDictionary, etc. are all class clusters (Class Clusters), and the implementation of an NSArray may be composed of multiple classes. Therefore, if you want to perform Swizzling on NSArray, you must obtain its "real body" for Swizzling, and it is invalid to operate on NSArray directly. This is because Method Swizzling does not work for clusters such as NSArray.

Because these clusters are actually a design pattern of abstract factories. There are many other subclasses that inherit from the current class in the abstract factory. The abstract factory class will create different abstract objects for use according to different situations. For example, when we call the method of NSArray objectAtIndex:, this class will judge inside the method, and create different abstract classes internally to operate.

So if we perform the Swizzling operation on the NSArray class, we only operate on the parent class , and other subclasses will be created inside the NSArray to perform the operation. It is not the NSArray itself that actually performs the Swizzling operation, so we should perform the operation on its "real body". operate.

The following lists the class names of the NSArray and NSDictionary classes, which can be retrieved through the Runtime function:

class name real body
NSArray __NSArrayI
NSMutableArray __NSArrayM
NSDictionary __NSDictionaryI
NSMutableDictionary __NSDictionaryM

practice

Well, create a new category, implement it directly with code, and see how to take out the real body:

  • NSArray+CrashHandle.m
@implementation NSArray (CrashHandle)

// Swizzling核心代码
// 需要注意的是,好多同学反馈下面代码不起作用,造成这个问题的原因大多都是其调用了super load方法。在下面的load方法中,不应该调用父类的load方法。
+ (void)load {
    Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
    Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(lxz_objectAtIndex:));
    method_exchangeImplementations(fromMethod, toMethod);
}

// 为了避免和系统的方法冲突,我一般都会在swizzling方法前面加前缀
- (id)lxz_objectAtIndex:(NSUInteger)index {
    // 判断下标是否越界,如果越界就进入异常拦截
    if (self.count-1 < index) {
        @try {
            return [self lxz_objectAtIndex:index];
        }
        @catch (NSException *exception) {
            // 在崩溃后会打印崩溃信息。如果是线上,可以在这里将崩溃信息发送到服务器
            NSLog(@"---------- %s Crash Because Method %s  ----------\n", class_getName(self.class), __func__);
            NSLog(@"%@", [exception callStackSymbols]);
            return nil;
        }
        @finally {}
    } // 如果没有问题,则正常进行方法调用
    else {
        return [self lxz_objectAtIndex:index];
    }
}

There may be a misunderstanding here, - (id)lxz_objectAtIndex:(NSUInteger)index {which calls itself? Is this recursion? Actually not. At this time, the method replacement is effective, and the SEL points to the IMP lxz_objectAtIndexof the original system . objectAtIndex:So not recursive.

  • ViewController.m
- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 测试代码
    NSArray *array = @[@0, @1, @2, @3];
    [array objectAtIndex:3];
    //本来要奔溃的
    [array objectAtIndex:4];
}

After running, it was found that there was no crash, and the relevant information was printed, as shown below.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325340282&siteId=291194637