Method Swizzling
Relevant 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 Swizzling
The 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 Swizzling
The 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.