Method SwizzlingRelevant concepts

Method Swizzling is objective-C dark magic implemented using the Runtime. Used for method interchange, which, as the name implies, swaps implementations of two methods. For example, the implementation of methodA is impA, the implementation of methodB is impB, and then the call to methodA in response to the impB, and the call to methodB in response to the impA.

Method Swizzing occurs at runtime. Because each class maintains a method list method, method contains method number SEL and its implementation OF IMP, method exchange is to disconnect the original SEL and IMP corresponding relationship, and the new IMP generation corresponding relationship.

Method Swizzling API

method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2) 
Copy the code

Swizzling Method Swizzling API is very simple, just need to pass in two methods to exchange, its source code implementation is as follows:

void method_exchangeImplementations(Method m1, Method m2) { if (! m1 || ! m2) return; mutex_locker_t lock(runtimeLock); M1_imp = m1-> IMP; m1->imp = m2->imp; m2->imp = m1_imp; // Update method cache flushCaches(nil); // when method changes its IMP, update the custom RR bit and AWZ bit. // RR means retain/release AWZ means allocWithZone updateCustomRR_AWZ(nil, m1); updateCustomRR_AWZ(nil, m2); }Copy the code

Method SwizzlingThe use of the

Now that you know the concepts and APIS of Method Swizzling, let’s take a look at how it is used.

Create a TPerson class:

@interface TPerson : NSObject

- (void)eat;
- (void)drink;

@end

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


@implementation TPerson

+ (void)load {
    Method oriMethod = class_getInstanceMethod([self class], @selector(eat));
    Method swiMethod = class_getInstanceMethod([self class], @selector(drink));
    method_exchangeImplementations(oriMethod, swiMethod);
}

- (void)eat {
    NSLog(@"====eat====");
}

- (void)drink {
    NSLog(@"====drink====");
}

@end
Copy the code

The result is as follows:

We call the eat method first, and because of the swap, we respond with drink first.

Method SwizzlingThe attention of the points

1. Related use of class clusters

@interface NSArray (addition) @end #import "NSArray+addition.h" #import <objc/runtime.h> @implementation NSArray (addition) + (void)load{ Method oriMethod = class_getInstanceMethod([self class], @selector(objectAtIndex:)); Method swiMethod = class_getInstanceMethod([self class], @selector(customObjectAtIndex:)); method_exchangeImplementations(oriMethod, swiMethod); } // custom exchange method - (id)customObjectAtIndex:(NSUInteger)index{if (index > self.count-1) {NSLog(@"--customObjectAtIndex-- Array out of bounds --"); return nil; } return [self customObjectAtIndex:index]; } @end _dataArray = @[@"AAA", @"BBB", @"CCC", @"DDD"]; NSLog (@ "- objectAtIndex - the fifth element - % @ --", [_dataArray objectAtIndex: 4]);Copy the code

If you run the above code, you will find that the program crashes and the console output array is out of bounds:

Terminating app due to uncaught exception 'NSRangeException', reason: '*** __boundsFail: index 4 beyond bounds [0 .. 3]'
Copy the code

And our custom exchange method does not print, indicating that the method is not executed, that is, the exchange method failed. So what went wrong? NSArray is a class whose method is in the __NSArrayI class. The NSArray swap will not succeed. Change the [self class] in the load method to objc_getClass(“__NSArrayI”) and continue to run the program. Program running normally, console output:

2020-02-21 14:59:32.359366+0800 005-- Runtime Application [52032:1924598] --customObjectAtIndex-- Array out of bounds -- 2020-02-21 14:59:32.359565+0800 005-- Runtime application [52032:1924598] --objectAtIndex-- 5th element --(null)--Copy the code

This indicates that the exchange method succeeded. So what happens when you output the following?

NSLog(@"-- literal -- 5th element --%@--", _dataArray[4]);Copy the code

Run program, program crashes, console output:

*** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSArrayI objectAtIndexedSubscript:]: index 4 beyond bounds [0 .. 3]'
Copy the code

ObjectAtIndexedSubscript = __NSArrayI objectAtIndexedSubscript = __NSArrayI objectAtIndexedSubscript = __NSArrayI objectAtIndexedSubscript = __NSArrayI objectAtIndexedSubscript

Method oriMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndexedSubscript:));
Method swiMethod = class_getInstanceMethod([self class], @selector(customObjectAtIndexedSubscript:));
method_exchangeImplementations(oriMethod, swiMethod);
Copy the code

And implement customObjectAtIndexedSubscript method:

- (id)customObjectAtIndexedSubscript:(NSUInteger)index{ if (index > self.count-1) { NSLog (@ "-- customObjectAtIndexedSubscript - the literal an array -"); return nil; } return [self customObjectAtIndexedSubscript:index]; }Copy the code

Run the program, console output:

The 2020-02-21 15:09:48. 163626 + 0800 005 - the Runtime application (52139-1930192) - customObjectAtIndexedSubscript - literal an array - 2020-02-21 15:09:48.163754+0800 005-- Runtime application [5239:1930192] -- literal -- 5th element --(null)--Copy the code

One of the things that needs to be improved in this example is that if we call NSArray’s load method again, it will swap the method back, so we only need to swap the method once. The commutative method can be written as a singleton to remedy this defect.

NSArray, NSMutableArray, NSDictionary, NSMutableDictionary

  • NSArray -> __NSArrayI
  • NSMutableArray -> __NSArrayM
  • NSDictionary -> __NSDictionaryI
  • NSMutableDictionary -> __NSDictionaryM

2. Interchange of parent and subclass methods

@interface TPerson : NSObject

- (void)eat;

@end

@implementation TPerson

- (void)eat {
    NSLog(@"==TPerson==eat====");
}

@end


@interface TStudent : TPerson

@end

@implementation TStudent

+ (void)load {
    Method oriMethod = class_getInstanceMethod([self class], @selector(study));
    Method swiMethod = class_getInstanceMethod([self class], @selector(eat));
    method_exchangeImplementations(oriMethod, swiMethod);
}

- (void)study {
    [self study];
}

@end
Copy the code

It crashes when we call the following methods:

    TStudent *stu = [[TStudent alloc] init];
    [stu eat];
    
    TPerson *person = [[TPerson alloc] init];
    [person eat];
Copy the code
Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[TPerson study]: unrecognized selector sent to instance 0x6000035905e0'
Copy the code

Since the TPerson eat method has been swapped with the TStudent study method, when we execute [PERSON eat], we execute the TPerson eat method. However, when we execute [Person eat], the IMP implementation has been changed to study. TPerson has no study method, so it crashes.

Solution: exchange before you give the class to add to exchange method, if can add success, means that the class without the implementation of this method, can directly replace the original method, if you add failed implementation of this class with this method, call the parent class of time will not collapse, can be directly.

The implementation is as follows:

Method oriMethod = class_getInstanceMethod(cls, oriSEL); Method swiMethod = class_getInstanceMethod(cls, swizzledSEL); // Try adding BOOL success = class_addMethod(CLS, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(oriMethod)); Class_replaceMethod (CLS, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod)); }else{// Implementations(oriMethod, swiMethod); }Copy the code

3. Neither parent class nor child class is implemented

If none of the class’s original methods are implemented, the handling of the above example is still problematic, because a circular call will occur when there is no implementation. We can add an empty implementation to the original method when it is no longer implemented, which prevents circular calls.

Method oriMethod = class_getInstanceMethod(cls, oriSEL); Method swiMethod = class_getInstanceMethod(cls, swizzledSEL); if (! OriMethod) {// If oriMethod is nil, replace swizzledSEL with an empty implementation that does nothing as follows: class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod)); method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){ })); } // If you want to change a method, you can change it. Swizzle BOOL didAddMethod = class_addMethod(CLS, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod)); if (didAddMethod) { class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod)); }else{ method_exchangeImplementations(oriMethod, swiMethod); }Copy the code

Conclusion:

Method Swizzling is the best manifestation of objective-C dynamics. Its main point is to exchange imPs of two methods, so as to achieve the purpose of exchanging methods.