Kevin的IOS开发专题iOS 底层面试

iOS 方法交换 Method Swizzling

2021-05-28  本文已影响0人  木扬音

什么是Method Swizzling

常用的API

【问题1】method-swizzling使用过程中重复交换

假如在load中实现了method-swizzling,由于load方法的多次调用,这样会导致方法的重复交换,使方法的sel又重新指向原来的imp

解决方案

通过单例来确保方法只被交换一次

+ (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [LGRuntimeTool lg_bestMethodSwizzlingWithClass:self oriSEL:@selector(helloword) swizzledSEL:@selector(lg_studentInstanceMethod)];
    });
}

【问题2】交换的方法子类没有实现,而父类实现了

运行下面这段代码会出现什么问题?

//*********LGPerson类*********
@interface LGPerson : NSObject
- (void)personInstanceMethod;
@end

@implementation LGPerson
- (void)personInstanceMethod{
    NSLog(@"person对象方法:%s",__func__);  
}
@end

//*********LGStudent类*********
@interface LGStudent : LGPerson
- (void)helloword;
+ (void)sayHello;
@end

@implementation LGStudent
@end

//*********调用*********
- (void)viewDidLoad {
    [super viewDidLoad];

    // 黑魔法坑点二: 子类没有实现 - 父类实现
    LGStudent *s = [[LGStudent alloc] init];
    [s personInstanceMethod];
    
    LGPerson *p = [[LGPerson alloc] init];
    [p personInstanceMethod];
}

我们通过分类来实现方法交换,和封装好的方法交换类

@implementation LGStudent (LG)

+ (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [LGRuntimeTool lg_methodSwizzlingWithClass:self oriSEL:@selector(personInstanceMethod) swizzledSEL:@selector(lg_studentInstanceMethod)];
    });
}

// personInstanceMethod 我需要父类的这个方法的一些东西
// 给你加一个personInstanceMethod 方法
// imp

- (void)lg_studentInstanceMethod{
    ////是否会产生递归?--不会产生递归,原因是lg_studentInstanceMethod 会走 oriIMP,即personInstanceMethod的实现中去
    [self lg_studentInstanceMethod];
    NSLog(@"LGStudent分类添加的lg对象方法:%s",__func__);
}

@end

//封装好的方法交换类
@implementation LGRuntimeTool
+ (void)lg_methodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
    
    if (!cls) NSLog(@"传入的交换类不能为空");

    Method oriMethod = class_getInstanceMethod(cls, oriSEL);
    Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
    method_exchangeImplementations(oriMethod, swiMethod);
}

运行上面代码,会发现在子类调用personInstanceMethod方法时会崩溃

image.png
解决方案

我们可以通过class_addMethod尝试添加你要交换的方法,来判断当前类是否有这样方法

+ (void)lg_betterMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
    
    if (!cls) NSLog(@"传入的交换类不能为空");
    
    Method oriMethod = class_getInstanceMethod(cls, oriSEL);
    Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
   
    // 一般交换方法: 交换自己有的方法 -- 走下面 因为自己有意味添加方法失败
    // 交换自己没有实现的方法:
    //   首先第一步:会先尝试给自己添加要交换的方法 :personInstanceMethod (SEL) -> swiMethod(IMP)
    //   然后再将父类的IMP给swizzle  personInstanceMethod(imp) -> swizzledSEL 

    BOOL success = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(oriMethod));

    if (success) {// 自己没有 - 交换 - 没有父类进行处理 (重写一个)
        class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
    }else{ // 自己有
        method_exchangeImplementations(oriMethod, swiMethod);
    }   
}

下面是class_replaceMethodclass_addMethodmethod_exchangeImplementations的源码实现

image.png

其中class_replaceMethodclass_addMethod中都调用了addMethod方法,区别在于bool值的判断,下面是addMethod的源码实现

image.png

【问题3】:子类和父类都没有实现改方法,下面的调用有什么问题?

//*********LGPerson类*********
@interface LGPerson : NSObject
- (void)personInstanceMethod;
@end

@implementation LGPerson
@end

//*********LGStudent类*********
@interface LGStudent : LGPerson
- (void)helloword;
+ (void)sayHello;
@end

@implementation LGStudent
@end

//*********调用*********
- (void)viewDidLoad {
    [super viewDidLoad];

    // 黑魔法坑点二: 子类没有实现 - 父类实现
    LGStudent *s = [[LGStudent alloc] init];
    [s personInstanceMethod];
    
    LGPerson *p = [[LGPerson alloc] init];
    [p personInstanceMethod];
}

运行奔溃


image.png

原因是递归死循环导致栈溢出---- 由于personInstanceMethod方法没有实现,在进行方法交换的时候找不到oriMethod,导致交换方法失败,当我们调用personInstanceMethod(oriMethod)方法时,oriMethod会进入到分类中的lg_studentInstanceMethod方法,然而lg_studentInstanceMethod方法中又调用了lg_studentInstanceMethod,但是现在lg_studentInstanceMethod并没有指向oriMethod,导致自己调用自己直接递归死循环

解决方案
+ (void)lg_bestMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
    
    if (!cls) NSLog(@"传入的交换类不能为空");
    
    Method oriMethod = class_getInstanceMethod(cls, oriSEL);
    Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
    
    if (!oriMethod) {
        // 在oriMethod为nil时,替换后将swizzledSEL复制一个不做任何事的空实现,代码如下:
        class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
        method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){ }));
    }
    
    // 一般交换方法: 交换自己有的方法 -- 走下面 因为自己有意味添加方法失败
    // 交换自己没有实现的方法:
    //   首先第一步:会先尝试给自己添加要交换的方法 :personInstanceMethod (SEL) -> swiMethod(IMP)
    //   然后再将父类的IMP给swizzle  personInstanceMethod(imp) -> swizzledSEL
    //oriSEL:personInstanceMethod

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

method-swizzling --- 类方法

在分类的load方法中实现类方法的方法交换

+ (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
         [LGRuntimeTool lg_bestClassMethodSwizzlingWithClass:self oriSEL:@selector(sayHello) swizzledSEL:@selector(lg_studentClassMethod)];
    });
}
+ (void)lg_studentClassMethod{
    NSLog(@"LGStudent分类添加的lg类方法:%s",__func__);
   [[self class] lg_studentClassMethod];
}
//封装的method-swizzling方法
+ (void)lg_bestClassMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
    
    if (!cls) NSLog(@"传入的交换类不能为空");

    Method oriMethod = class_getClassMethod([cls class], oriSEL);
    Method swiMethod = class_getClassMethod([cls class], swizzledSEL);
    
    if (!oriMethod) { // 避免动作没有意义
        // 在oriMethod为nil时,替换后将swizzledSEL复制一个不做任何事的空实现,代码如下:
        class_addMethod(object_getClass(cls), oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
        method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){
            NSLog(@"来了一个空的 imp");
        }));
    }
    
    // 一般交换方法: 交换自己有的方法 -- 走下面 因为自己有意味添加方法失败
    // 交换自己没有实现的方法:
    //   首先第一步:会先尝试给自己添加要交换的方法 :personInstanceMethod (SEL) -> swiMethod(IMP)
    //   然后再将父类的IMP给swizzle  personInstanceMethod(imp) -> swizzledSEL
    //oriSEL:personInstanceMethod

    BOOL didAddMethod = class_addMethod(object_getClass(cls), oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
    
    if (didAddMethod) {
        class_replaceMethod(object_getClass(cls), swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
    }else{
        method_exchangeImplementations(oriMethod, swiMethod);
    }
    
}

Method Swizzling的应用

最常用的应用是防止数组、字典等越界崩溃

在iOS 中NSNumberNSArrayNSDictionary这些类都是类簇,例如NSArray的实现是由多个类组成的,所有如果想对NSArray进行方法交换,必须获取到其“真身”进行Swizzling,直接对NSArray进行操作是无效的

image.png
NSArray方法交换

创建NSArray的一个分类YP

@implementation NSArray (YP)
//如果下面代码不起作用,造成这个问题的原因大多都是其调用了super load方法。在下面的load方法中,不应该调用父类的load方法。这样会导致方法交换无效
+ (void)load{
    Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
    Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(yp_objectAtIndex:));
    
    method_exchangeImplementations(fromMethod, toMethod);
}

//如果下面代码不起作用,造成这个问题的原因大多都是其调用了super load方法。在下面的load方法中,不应该调用父类的load方法。这样会导致方法交换无效
- (id)yp_objectAtIndex:(NSUInteger)index{
    //判断下标是否越界,如果越界就进入异常拦截
    if (self.count-1 < index) {
        // 这里做一下异常处理,不然都不知道出错了。
#ifdef DEBUG  // 调试阶段
        return [self yp_objectAtIndex:index];
#else // 发布阶段
        @try {
            return [self yp_objectAtIndex:index];
        } @catch (NSException *exception) {
            // 在崩溃后会打印崩溃信息,方便我们调试。
            NSLog(@"---------- %s Crash Because Method %s  ----------\n", class_getName(self.class), __func__);
            NSLog(@"%@", [exception callStackSymbols]);
            return nil;
        } @finally {
            
        }
#endif
    }else{ // 如果没有问题,则正常进行方法调用
        return [self yp_objectAtIndex:index];
    }
}

@end
上一篇 下一篇

猜你喜欢

热点阅读