美文网首页底层
iOS-OC底层16:Method-Swizzling 方法交换

iOS-OC底层16:Method-Swizzling 方法交换

作者: MonKey_Money | 来源:发表于2020-10-26 14:30 被阅读0次

Method swizzling是什么

Method swizzling 用于改变一个已经存在的 selector 的实现。这项技术使得在运行时通过改变 selector 在类的消息分发列表中的映射从而改变方法的掉用成为可能。Method swizzling也就是我们常说的“黑魔法”
一般定义一个工具类如下

@interface LGRuntimeTool : NSObject
/**
 交换方法
 @param cls 交换对象
 @param oriSEL 原始方法编号
 @param swizzledSEL 交换的方法编号
 */
+ (void)lg_methodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL;
@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);
}
@end

我们定义一个类如下

@interface LGPerson : NSObject
- (void)personInstanceMethod;
-(void)swizzlingMethod;
+ (void)personClassMethod;

@end
@implementation LGPerson

+(void)load {    
    [LGRuntimeTool lg_methodSwizzlingWithClass:[self class] oriSEL:@selector(personInstanceMethod) swizzledSEL:@selector(swizzlingMethod)];    
}
- (void)personInstanceMethod{
    NSLog(@"person对象方法:%s",__func__);
}
-(void)swizzlingMethod { 
    NSLog(@"person交换后的方法对象方法:%s",__func__);

}
@end
    LGPerson *s = [[LGPerson alloc] init];
    [s personInstanceMethod];
    [s swizzlingMethod];

我们调用之后打印如下

person对象方法:-[LGPerson personInstanceMethod]
 person交换后的方法对象方法:-[LGPerson swizzlingMethod]

被重复调用很多次

我们交换方法写到load方法里,但是有可能load方法被手动调用,

  LGPerson *s = [[LGPerson alloc] init];
    [s personInstanceMethod];
    [LGPerson load];
    [s swizzlingMethod];
person对象方法:-[LGPerson personInstanceMethod]
person交换后的方法对象方法:-[LGPerson swizzlingMethod]

如果方法交换方法被多次调用,我们就无法控制方法到底调用情况
解决方案单利

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

子类中交换父类方法,父类调用该方法

@interface LGStudent : LGPerson
@end
@implementation LGStudent
+ (void)load {
    static dispatch_once_t studenDispatchonce;
    dispatch_once(&studenDispatchonce, ^{
        
        [LGRuntimeTool lg_methodSwizzlingWithClass:[self class] oriSEL:@selector(personInstanceMethod) swizzledSEL:@selector(studentInstanceMethod)];
    }); 
}
-(void)studentInstanceMethod {
 [self studentInstanceMethod];
    NSLog(@"student对象方法%s",__func__);
}
@end
   LGPerson *person = [[LGPerson alloc] init];
    [person personInstanceMethod];

崩溃日志

[LGPerson studentInstanceMethod]: unrecognized selector sent to instance 0x6000036cc420'

这是因为studentInstanceMethod内调用[self studentInstanceMethod],而[self studentInstanceMethod]在子类中能访问到。
解决方案,把需要更改的方法加到本类中,防止污染父类
LGRuntimeTool添加方法

+ (void)lg_betterMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
    
    if (!cls) NSLog(@"传入的交换类不能为空");
    // oriSEL       personInstanceMethod
    // swizzledSEL  lg_studentInstanceMethod
    
    Method oriMethod = class_getInstanceMethod(cls, oriSEL);
    Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
   
    // 尝试添加你要交换的方法 - lg_studentInstanceMethod
    BOOL success = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(oriMethod));

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

打印日志

person对象方法:-[LGPerson personInstanceMethod]

交换未实现的的方法

我们把LGPerson方法中personInstanceMethod的实现注释调
运行

warning: `self' is not accessible (substituting 0)

死循环解决方案
我们在交换方法里对要交换的方法进行判断,如果未实现我们可以添加一个空实现


    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){
            NSLog(@"来了一个空的 imp");
        }));
    }
    
    // 一般交换方法: 交换自己有的方法 -- 走下面 因为自己有意味添加方法失败
    // 交换自己没有实现的方法:
    //   首先第一步:会先尝试给自己添加要交换的方法 :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);
    }

类方法交换

//给LGStudent添加类方法
+(void)firstClassMethod {
    NSLog(@"---%s---%@-----%@",__func__,self,NSStringFromSelector(_cmd));
}

+(void)secondClassMethod{
    
    NSLog(@"--%s---%@-----%@",__func__,self,NSStringFromSelector(_cmd));
}
//实现方法的交换
        [LGRuntimeTool lg_bestClassMethodSwizzlingWithClass:self oriSEL:@selector(firstClassMethod) swizzledSEL:@selector(secondClassMethod)];

+(void)lg_bestClassMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL {  
    Method oriMethod = class_getClassMethod([cls class], oriSEL);
    Method swiMethod = class_getClassMethod([cls class], swizzledSEL);
    Class metalClass = objc_getMetaClass(NSStringFromClass(cls).UTF8String);
    if (!oriMethod) { // 避免动作没有意义
        // 在oriMethod为nil时,替换后将swizzledSEL复制一个不做任何事的空实现,代码如下:
        class_addMethod(metalClass, 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(metalClass, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod)); 
    if (didAddMethod) {
        class_replaceMethod(metalClass, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
    }else{
        method_exchangeImplementations(oriMethod, swiMethod);
    }    
}
//调用
    [LGStudent firstClassMethod];

打印结果

[LGStudent(LG) secondClassMethod]---LGStudent-----firstClassMethod

method-swizzling的应用

method-swizzling最常用的应用是防止数组、字典等越界崩溃
在iOS中NSNumber、NSArray、NSDictionary等这些类都是类簇,一个NSArray的实现可能由多个类组成。所以如果想对NSArray进行Swizzling,必须获取到其“真身”进行Swizzling,直接对NSArray进行操作是无效的。

下面列举了NSArray和NSDictionary本类的类名,可以通过Runtime函数取出本类。

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

//如果下面代码不起作用,造成这个问题的原因大多都是其调用了super load方法。在下面的load方法中,不应该调用父类的load方法。这样会导致方法交换无效
- (id)cjl_objectAtIndex:(NSUInteger)index{
    //判断下标是否越界,如果越界就进入异常拦截
    if (self.count-1 < index) {
        // 这里做一下异常处理,不然都不知道出错了。
#ifdef DEBUG  // 调试阶段
        return [self cjl_objectAtIndex:index];
#else // 发布阶段
        @try {
            return [self cjl_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 cjl_objectAtIndex:index];
    }
}

@end

相关文章

网友评论

    本文标题:iOS-OC底层16:Method-Swizzling 方法交换

    本文链接:https://www.haomeiwen.com/subject/ymqamktx.html