iOS 底层探究:Method Swizzling

363 阅读5分钟

这是我参与8月更文挑战的第13天,活动详情查看:8月更文挑战

1.基本介绍

利用OC的Runtime特性,动态改变SEL(方法编号)和IMP(方法实现)的对应关系,达到OC方法调用流程改变的目的。主要用于OC方法 在OC中,SEL和IMP之间的关系,就好像一本书的“目录”

  • SEL是方法编号,就像是“标题”一样
  • IMP是方法实现的真实地址,就像“页码”一样
  • 他们是一一对应的关系 Runtime提供了交换两个SELIMP对应关系的函数
OBJC_EXPORT void 
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

通过这个函数交换两个SELIMP对应关系的技术,我们称之为Method Swizzle(方法欺骗)

image.png Runtime机制对于AOP面向切面编程提供良好的支持,可利用Method Swizzling实现AOP,其中AOP(Aspect Oriented Programming)是一种编程的思想,和面向对象编程OOP有本质的区别

  • OOP和AOP都是编程的思想
  • OOP编程思想更加倾向于对业务模块的封装,划分出更加清晰的逻辑单元
  • 而AOP是面向切面进行提取封装,提取各个模块中的公共部分,提高模块的复用率,降低业务之间的耦合性

2.API介绍

  • 通过SEL获取方法Method
    • class_getInstanceMethod:获取实例方法
    • class_getClassMethod:获取类方法
  • IMP的getter/setter方法
    • method_getImplementation:获取一个方法的实现
    • method_setImplemrntation:设置一个方法的实现
  • method_getTypeEncoding:获取方法实现的编码类型
  • class_addMethod:添加方法实现
  • class_replaceMethod:替换方法的IMP。如:A替换B,即:B指向A,A还是指向A
  • method_exchangeImplementations:交换两个方法的IMP。如:A交换B,即:B指向A,A指向B

3.注意事项

3.1 保证方法交换只执行一次

为了保证交换的代码可以优先执行,有时候会将其写在load方法中,但是load方法也能被主动调用,如果多次调用,交换后的方法可能被还原。 所以我们要保证方法只能交换一次,可以选择在单例模式下,让交换后的方法不会被还原

+ (void)load{ 
    static dispatch_once_t onceToken; 
    dispatch_once(&onceToken, ^{ 
        [self lg_methodSwizzlingWithClass:self oriSEL:@selector(lg_person_say) swizzledSEL:@selector(lg_student_say)]; 
    }); 
}
+ (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); 
}

3.2 父类未实现子类将要交换的方法

父类LGPerson中,实现lg_person_say方法

#import <Foundation/Foundation.h> 
@interface LGPerson : NSObject 
- (void)lg_person_say; 
@end 
@implementation LGPerson 
- (void)lg_person_say{ 
    NSLog(@"LGPerson:%s",__func__); 
} 
@end

子类LGStudent中,实现lg_student_say方法。在load方法中,和父类的lg_person_say方法交换

#import "LGPerson.h" 
#import <objc/runtime.h> 

@interface LGStudent : LGPerson 
@end 

@implementation LGStudent 
+ (void)load{ 
    static dispatch_once_t onceToken; 
    dispatch_once(&onceToken, ^{ 
        [self lg_methodSwizzlingWithClass:self oriSEL:@selector(lg_person_say) swizzledSEL:@selector(lg_student_say)]; 
    }); 
} 
+ (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); 
} 
- (void)lg_student_say{ 
    //lg_studentInstanceMethod -/-> personInstanceMethod 
    [self lg_student_say]; 
    NSLog(@"LGStudent:%s",__func__); 
} 
@end

子类正常调用,但父类找不到lg_student_say方法

子类调用 
LGPerson:-[LGPerson lg_person_say] 
LGStudent:-[LGStudent lg_student_say] 
父类调用 
-[LGPerson lg_student_say]: unrecognized selector sent to instance 0x28218c3f0

方法交换应该只影响当前类,但子类中交换的是父类方法,导致父类收到影响,其它继承于该父类的子类也会出现问题 解决方法:保证方法交换只对当前类生效

+ (void)load{ 
static dispatch_once_t onceToken; 
dispatch_once(&onceToken, ^{ 
[self lg_betterMethodSwizzlingWithClass:self oriSEL:@selector(lg_person_say) swizzledSEL:@selector(lg_student_say)]; 
}); 
} 
+ (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);
    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_addMethod,对当前类添加lg_person_say方法,关联lg_student_sayimp
  • 返回值为YES,证明当前类中未实现lg_person_say方法
  • 如果方法添加成功,使用class_replaceMethod,将lg_student_say方法,替换为lg_person_sayimp 上述方式:
  • 如果子类实现lg_person_say方法
    • 添加失败,直接交换
    • 不会影响父类
  • 如果子类未实现lg_person_say方法
    • 添加成功,新方法关联lg_student_say的imp
    • 将lg_student_say替换为父类lg_person_say的imp
    • 调用顺序,依然保持:子类lg_student_say->父类lg_person_say
    • 智慧影响子类,不会影响父类

3.3 父类和子类都未实现原始方法

当父类和子类都未实现原始方法,上述方式将引发子类方法的递归调用,最终造成对战益处 原因在于:

  • 子类添加的lg_person_say,关联lg_student_say的imp
  • 父类未实现lg_person_say方法,子类使用class_replaceMethod,一定会替换失败,所以子类的lg_student_say的imp未发生改变 解决办法,对原始方法增加是否实现的判断条件
+ (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); 
    if (!oriMethod) { 
        IMP imp = imp_implementationWithBlock(^(id self, SEL _cmd){ 
            NSLog(@"伪装lg_person_say方法,其实什么都没做"); 
        }); 
        class_addMethod(cls, oriSEL, imp, method_getTypeEncoding(swiMethod)); 
    } 
    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); 
    } 
}
  • 判断如果当前未实现原始方法,添加lg_person_say方法,关联一个空方法的imp
  • 使用class_addMethod,对当前类添加lg_person_say方法,关联lg_student_sayimp
  • 由于lg_person_say已添加,此时返回值一定为NO,添加失败
  • 使用method_exchangeImplementations,直接将两个方法进行交换

4. 类方法的交换

类方法和实例方法的区别:类方法存储在元类的方法列表中,所以对类方法的添加和替换,不能直接使用Class,而是眼使用当前Class所属的MetaClass

+ (void)lg_betterClassMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{ 
    if (!cls) NSLog(@"传入的交换类不能为空"); 
    Class metaClass = objc_getMetaClass(NSStringFromClass(cls).UTF8String); 
    Method oriMethod = class_getInstanceMethod(metaClass, oriSEL); 
    Method swiMethod = class_getInstanceMethod(metaClass, swizzledSEL); 
    if (!oriMethod) { 
        IMP imp = imp_implementationWithBlock(^(id self, SEL _cmd){ 
            NSLog(@"伪装lg_person_say方法,其实什么都没做"); 
        }); 
        class_addMethod(metaClass, oriSEL, imp, method_getTypeEncoding(swiMethod)); 
    } 
    BOOL success = class_addMethod(metaClass, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(oriMethod)); 
    if (success) { 
        class_replaceMethod(metaClass, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod)); 
    }else{ 
        method_exchangeImplementations(oriMethod, swiMethod); 
    } 
}

5.数组、字典的方法交换

在ios中,NSArray和NSDictionary等类,都有类簇的存在。因为一个NSArray的实现,可能有多个类组成。所以对NSArray、NSDictionary进行方法交换,必须对齐真身进行操作

类名类簇
NSArray__NSArrayI
NSMutableArray__NSArrayM
NSDictionary__NSDictionaryI
NSMutableDictionary__NSDictionaryM

替换NSArray的objectAtIndex方法,避免数组越界

@implementation NSArray (LG) 
+ (void)load{ 
    Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:)); 
    Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(lg_objectAtIndex:)); 
    method_exchangeImplementations(fromMethod, toMethod); 
} 
- (id)lg_objectAtIndex:(NSUInteger)index{ 
    if (self.count-1 < index) { 
#ifdef DEBUG 
    // 调试阶段 
    return [self lg_objectAtIndex:index]; 
#else 
    // 发布阶段 
    @try { 
        return [self lg_objectAtIndex:index]; 
    } @catch (NSException *exception) { 
        NSLog(@"lg_objectAtIndex crash:%@", [exception callStackSymbols]); 
        return nil; 
    } @finally { 
    
    } 
#endif 
    }else{ 
        return [self lg_objectAtIndex:index]; 
    } 
} 
@end