preface

Method-swizzling was once referred to as the dark magic of ios, opening another window on ios development –AOP(faceted programming, unified maintenance of program functionality through precompilation and runtime dynamic proxies)

An aspect is a set of apis based on a method-Swizzling implementation

Method-swizzling principle is the use of Method exchange, can be implemented before and after the original Method embedded logic such as new implementation

Method and IMP

Before you look at method-Swizzling in ios, take a look at Method and IMP

Method is a Method structure, its structure is as follows, there is a Method name, Method type, IMP pointer, you can see that IMP is a parameter of Method, in fact, to point to the implementation of the function pointer

typedef struct objc_method *Method;

struct objc_method {
    SEL _Nonnull method_name                                 OBJC2_UNAVAILABLE;
    char * _Nullable method_types                            OBJC2_UNAVAILABLE;
    IMP _Nonnull method_imp                                  OBJC2_UNAVAILABLE;
} 
Copy the code

Method and IM obtaining methods

Before introducing method-Swizzling, we will introduce some methods to obtain Method and IMP

Class_getInstanceMethod is a function that obtains instance methods from the current class. The source code is as follows: lookUpImpOrNil imp is _objc_msgForward_impcache. If you don’t, you go to _class_getMethod to get a Method, and you end up in getMethod_nolock, where you use getMethodNoSuper_nolock to find out if the Method exists in the current class. If you don’t find it, you go to the parent class, so if you don’t find it, Returns nil

Method class_getInstanceMethod(Class cls, SEL sel) { if (! cls || ! sel) return nil; // Search method lists, try method resolver, etc. lookUpImpOrNil(cls, sel, nil, NO/*initialize*/, NO/*cache*/, YES/*resolver*/); return _class_getMethod(cls, sel); } static method_t * getMethod_nolock(Class cls, SEL sel) { method_t *m = nil; runtimeLock.assertLocked(); while (cls && ((m = getMethodNoSuper_nolock(cls, sel))) == nil) { cls = cls->superclass; } return m; }Copy the code

Class_getClassMethod is a function that obtains the method of a class directly from its metaclass. The source code is as follows

Method class_getClassMethod(Class cls, SEL sel) { if (! cls || ! sel) return nil; return class_getInstanceMethod(cls->getMeta(), sel); }Copy the code

Class_getMethodImplementation class_getMethodImplementation class_getMethodImplementation class_getMethodImplementation class_getMethodImplementation class_getMethodImplementation class_getMethodImplementation class_getMethodImplementation

IMP class_getMethodImplementation(Class cls, SEL sel) { IMP imp; if (! cls || ! sel) return nil; imp = lookUpImpOrNil(cls, sel, nil, YES/*initialize*/, YES/*cache*/, YES/*resolver*/); // Translate forwarding function to C-callable external version if (! imp) { return _objc_msgForward; } return imp; }Copy the code

Class_getMethodImplementation class_getMethodImplementation class_getMethodImplementation class_getMethodImplementation class_getMethodImplementation class_getMethodImplementation class_getMethodImplementation class_getMethodImplementation class_getMethodImplementation

IMP 
method_getImplementation(Method m)
{
    return m ? m->imp : nil;
}
Copy the code

AddMethod is like a class to add or replace a new method, if you add this class existing method will add failure (not a parent class), if the replacement method, then directly replace the original method imp implementation, its source code implementation is as follows

static IMP addMethod(Class cls, SEL name, IMP imp, const char *types, bool replace) { IMP result = nil; runtimeLock.assertLocked(); checkIsKnownClass(cls); assert(types); assert(cls->isRealized()); method_t *m; if ((m = getMethodNoSuper_nolock(cls, name))) { // already exists if (! replace) { result = m->imp; } else { result = _method_setImplementation(cls, m, imp); } } else { // fixme optimize method_list_t *newlist; newlist = (method_list_t *)calloc(sizeof(*newlist), 1); newlist->entsizeAndFlags = (uint32_t)sizeof(method_t) | fixed_up_method_list; newlist->count = 1; newlist->first.name = name; newlist->first.types = strdupIfMutable(types); newlist->first.imp = imp; prepareMethodLists(cls, &newlist, 1, NO, NO); cls->data()->methods.attachLists(&newlist, 1); flushCaches(cls); result = nil; } return result; }Copy the code

Class_addMethod adds a method to a class, and eventually the addMethod method is called. Replace passes No, that is, if the class has this method, the replacement fails and the parent class is not searched

Class_replaceMethod replaces one method of a class with another, eventually calling addMethod. Replace passes YES, directly replacing IMP

Method_exchangeImplementations directly swaps imPs of two functions

The source code is as follows:

BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types) { if (! cls) return NO; mutex_locker_t lock(runtimeLock); return ! addMethod(cls, name, imp, types ? : "", NO); } IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types) { if (! cls) return nil; mutex_locker_t lock(runtimeLock); return addMethod(cls, name, imp, types ? : "", YES); } void method_exchangeImplementations(Method m1, Method m2) { if (! m1 || ! m2) return; mutex_locker_t lock(runtimeLock); IMP m1_imp = m1->imp; m1->imp = m2->imp; m2->imp = m1_imp; // RR/AWZ updates are slow because class is unknown // Cache updates are slow because class is unknown // fixme build list of classes whose Methods are known externally? flushCaches(nil); updateCustomRR_AWZ(nil, m1); updateCustomRR_AWZ(nil, m2); }Copy the code

Method – Swizzling

Now that you’ve seen some basic method implementation logic, let’s try swapping methods to understand some of the pitfalls

Let’s say we have a class LSPerson inherited from NSObject, LSStudent inherited from LSPerson, LSPerson’s classification, LSPerson(Category)

For convenience, swap methods in LSStudent’s load method

Be careful before method-Swizzling exchange

Load pit: There is a situation in the load method where there is a class and a class load that will be swapped back if swapped

Initialize the pit: In addition, to optimize the startup speed of your application, you may want to put the method swap in the initialize method. Note that this method will be called repeatedly during the normal method lookup process if the method is a parent class. So the exchange here needs to use a singleton to do the exchange (actual applications can actively call methods to do the exchange when needed)

Method-swizzling exchange implementation

First, use the class_getInstanceMethod method to get the old method and the new method to be swapped. According to the source logic of the method, if the subclass does not get the new method, it gets it from the parent class method, and the parent class does not get it, so it returns nil

Method oriMethod = class_getInstanceMethod([self class], @selector(print));
Method swiMethod = class_getInstanceMethod([self class], @selector(ls_print));
Copy the code

Since the exchange of new methods, new method for the user the default implementation, there must be, at this time there could be a scene, by switching method for some reason (changed a derived class, deleted method, did not achieve a certain agreement, etc.), lead to the original method is not implemented, so the need for judgment (after all, the purpose of exchange is usually in order to use the original method, and can have new features)

If the original method does not exist, in order to ensure the normal use of the function, still need to exchange methods, then will be implemented for the original method updated to the user implementation of the new method, and the new method is given an empty logic IMP implementation, the implementation code is as follows

// Check if the original method exists if (! OriMethod) {// The old method does not exist, assign a new method to the old method, New methods give empty methods class_addMethod([self class], @selector(print), method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod)); method_setImplementation(swiMethod, imp_implementationWithBlock(^{})); return; }Copy the code

If the original method exists, then according to the class_getInstanceMethod method logic, maybe the current class does not implement the method, but the subclass does, and then you can get it

At this point, if the direct exchange, the new method of the subclass will be exchanged with the original method of the parent class, which will cause a serious problem. Calling the original method of the parent class is equivalent to calling the new method of the subclass, and the original method of the parent class becomes invalid.

One of the pitfalls: If the new method calls another method of the subclass, and the parent class does not have another method called by the subclass, the method is called from the instance variable of the parent class, and it crashes because it cannot find the method

Solution:

Add the method to the current subclass by class_addMethod, which is the implementation of the new method. Its source code gets the implementation of the method (excluding the parent class) of the current class, and returns false if it exists

So if the parent class does not implement the method, calling class_addMethod assigns a new method to the original method. All you need to do is point the new method directly to the original parent method implementation through the class_replaceMethod implementation. If there is no implementation, we can simply use method_exchangeImplementations imp implementations of both

// Add the original method to the subclass and implement it as a new method. BOOL isAdd = class_addMethod([self class], @selector(print), method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod)); If (isAdd) {if (isAdd) {if (isAdd) {if (isAdd) { Does not exchange class_replaceMethod([self class], @selector(ls_print), method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod)); }else {method_exchangeImplementations(oriMethod, swiMethod); }Copy the code

The overall implementation logic is shown below

+ (void)initialize { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ Method oriMethod = class_getInstanceMethod([self class], @selector(print)); Method swiMethod = class_getInstanceMethod([self class], @selector(ls_print)); // Check if the original method exists if (! OriMethod) {// The old method does not exist, assign a new method to the old method, New methods give empty methods class_addMethod([self class], @selector(print), method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod)); method_setImplementation(swiMethod, imp_implementationWithBlock(^{})); return; } // Add the original method to the subclass and implement it as a new method. BOOL isAdd = class_addMethod([self class], @selector(print), method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod)); If (isAdd) {if (isAdd) {if (isAdd) {if (isAdd) { Does not exchange class_replaceMethod([self class], @selector(ls_print), method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod)); }else {method_exchangeImplementations(oriMethod, swiMethod); }}); }Copy the code

Method-swizzling application case

We use NSArray to add contents to the elements of an array in a shortcut way, such as:

NSArray *list = @[@" Stephen "]; NSLog(@"%@", [list objectAtIndex:2]); NSLog(@"%@", list[2]);Copy the code

At this point, you will find a crash due to overstepping the boundary, and the crash content is as follows

*** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[__NSSingleObjectArrayI objectAtIndex:]: index 2 beyond bounds [0 .. 0]'
*** First throw call stack:
(
	0   CoreFoundation                      0x00007fff399f7d07 __exceptionPreprocess + 250
	1   libobjc.A.dylib                     0x00007fff727625bf objc_exception_throw + 48
	2   CoreFoundation                      0x00007fff39923b66 -[__NSSingleObjectArrayI objectAtIndex:] + 112
	3   LSTest                              0x0000000100002d97 main + 119
	4   libdyld.dylib                       0x00007fff73909cc9 start + 1
)
libc++abi.dylib: terminating with uncaught exception of type NSException
Copy the code

So we can’t hook objectAtIndex as NSArray when we swap method-Swizzling classes, The __NSSingleObjectArrayI class creates the classification in the form of the old method to handle

Note: in order to test the convenience of simplifying the case, the direct exchange, can be completed in the appropriate position

The brief implementation is as follows:

@interface NSArray (extersion) @end@implementation NSArray (extersion) + (void)load {//  = class_getInstanceMethod([self class], @selector(ls_objectAtIndex:)); Method swiMethod = class_getInstanceMethod(NSClassFromString(@"__NSSingleObjectArrayI"), @selector(objectAtIndex:)); method_exchangeImplementations(oriMethod, swiMethod); } - (id)ls_objectAtIndex:(NSUInteger)index {if (index >= self.count) {NSLog(@" I don't exist, I have "); return nil; } return [self ls_objectAtIndex:index]; } @endCopy the code

It will find that it does not crash, and print out the latest transgression prompt log, you can try to hook other operations

[19362:206621] I don't exist [19362:206621] I don't exist, slip awayCopy the code

So much for the case