iOS底层原理笔记 - Runtime应用03-替换方法实现

169 阅读2分钟

通过#import <objc/runtime.h>

1.我们可以找到class_getInstanceMethod方法(获取类的实例方法)

/** 
 * Returns a specified instance method for a given class.
 * 
 * @param cls The class you want to inspect.
 * @param name The selector of the method you want to retrieve.
 * 
 * @return The method that corresponds to the implementation of the selector specified by 
 *  \e name for the class specified by \e cls, or \c NULL if the specified class or its 
 *  superclasses do not contain an instance method with the specified selector.
 *
 * @note This function searches superclasses for implementations, whereas \c class_copyMethodList does not.
 */
OBJC_EXPORT Method _Nullable
class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

2.用method_exchangeImplementations来交换两个方法实现

/** 
 * Exchanges the implementations of two methods.
 * 
 * @param m1 Method to exchange with second method.
 * @param m2 Method to exchange with first method.
 * 
 * @note This is an atomic version of the following:
 *  \code 
 *  IMP imp1 = method_getImplementation(m1);
 *  IMP imp2 = method_getImplementation(m2);
 *  method_setImplementation(m1, imp2);
 *  method_setImplementation(m2, imp1);
 *  \endcode
 */
OBJC_EXPORT void
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

应用实例

1.hook所有按钮的点击事件

#import "UIControl+Extension.h"
#import <objc/runtime.h>

@implementation UIControl (Extension)

//只执行一次
+ (void)load
{
    //hook
    Method method1 = class_getInstanceMethod(self, @selector(sendAction:to:forEvent:));
    Method method2 = class_getInstanceMethod(self, @selector(new_sendAction:to:forEvent:));
    method_exchangeImplementations(method1, method2);
}

- (void)new_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event
{
    NSLog(@"%@ - %@ - %@", self, target, NSStringFromSelector(action));
    //在这里做些什么
    //..
    
    //    if ([self isKindOfClass:[UIButton class]]) {
    //        // 拦截了所有按钮的事件
    //
    //    }
    
    //调用系统原来的
    [self new_sendAction:action to:target forEvent:event];
    
}

@end

实际使用:

UIButton *btn = [[UIButton alloc] initWithFrame:(CGRectMake(50, 100, 100, 50))];
btn.backgroundColor = [UIColor grayColor];
[btn addTarget:self action:@selector(btAction) forControlEvents:(UIControlEventTouchUpInside)];
[self.view addSubview:btn];

- (void)btAction {
    NSLog(@"点击事件");
}

输出: <UIButton: 0x7fececb09f90; frame = (50 100; 100 50); opaque = NO; layer = <CALayer: 0x600002bfd280>> - <ViewController: 0x7fecec9079a0> - btAction

2.防止可变数组插入nil时,导致的crash

实例:

NSString *obj = nil;
NSMutableArray *array = [NSMutableArray array];
[array addObject:@"jack"];
[array insertObject:obj atIndex:0];

报错:*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[__NSArrayM insertObject:atIndex:]: object cannot be nil'
terminating with uncaught exception of type NSException

解决方法:

#import "NSMutableArray+Extension.h"
#import <objc/runtime.h>

@implementation NSMutableArray (Extension)

+ (void)load
{
		//保证方法只被替换一次
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class cls = NSClassFromString(@"__NSArrayM");
        Method method1 = class_getInstanceMethod(cls, @selector(insertObject:atIndex:));
        Method method2 = class_getInstanceMethod(cls, @selector(new_insertObject:atIndex:));
        method_exchangeImplementations(method1, method2);
    });
    
}

- (void)new_insertObject:(id)anObject atIndex:(NSUInteger)index
{
    if (anObject == nil) {
        return;
    }
    
    [self new_insertObject:anObject atIndex:index];
}

@end

3.防止可变字典插入的key或value为nil时,导致的crash

实例:

NSString *obj = nil;
NSMutableDictionary *mDict = [NSMutableDictionary dictionary];
mDict[@"name"] = @"jack";
mDict[obj] = @"lilei";
mDict[@"age"] = obj;
NSLog(@"%@",mDict);

报错:*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '*** -[__NSDictionaryM setObject:forKeyedSubscript:]: key cannot be nil'

解决方法:

#import "NSMutableDictionary+Extension.h"
#import <objc/runtime.h>

@implementation NSMutableDictionary (Extension)

+ (void)load
{
		//保证方法只被替换一次
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class cls = NSClassFromString(@"__NSDictionaryM");
        Method method1 = class_getInstanceMethod(cls, @selector(setObject:forKeyedSubscript:));
        Method method2 = class_getInstanceMethod(cls, @selector(new_setObject:forKeyedSubscript:));
        method_exchangeImplementations(method1, method2);
        
        Class cls2 = NSClassFromString(@"__NSDictionaryI");
        Method method3 = class_getInstanceMethod(cls2, @selector(objectForKeyedSubscript:));
        Method method4 = class_getInstanceMethod(cls2, @selector(new_objectForKeyedSubscript:));
        method_exchangeImplementations(method3, method4);
    });
}

- (void)new_setObject:(id)anObject forKeyedSubscript:(nonnull id<NSCopying>)key
{
    if (!key) {
        return;
    }
    [self new_setObject:anObject forKeyedSubscript:key];
}

- (id)new_objectForKeyedSubscript:(id)key
{
    if (!key) {
        return nil;
    }
    return [self new_objectForKeyedSubscript:key];
}

@end