1. 基本介绍

利用OCRuntime特性,动态改变SEL(方法编号)和IMP(方法实现)的对应关系,达到OC方法调用流程改变的目的。主要用于OC方法

OC中,SELIMP之间的关系,就好像一本书的“目录
image.png

  • SEL是方法编号,就像“标题”一样
  • IMP是方法实现的真实地址,就像“页码”一样
  • 它们是一一对应的关系

Runtime提供了交换两个SELIMP对应关系的函数

  1. OBJC_EXPORT void
  2. method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2)
  3. OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

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

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

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

2. API介绍

  • 通过SEL获取方法Method

    • class_getInstanceMethod:获取实例方法

    • class_getClassMethod:获取类方法

  • IMPgetter/setter方法

    • method_getImplementation:获取一个方法的实现

    • method_setImplementation:设置一个方法的实现

  • method_getTypeEncoding:获取方法实现的编码类型

  • class_addMethod:添加方法实现

  • class_replaceMethod:替换方法的IMP。如:A替换B,即:B指向AA还是指向A

  • method_exchangeImplementations:交换两个方法的IMP。如:A交换B,即:B指向AA指向B

3. 坑点介绍

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

为了保证方法交换的代码可以优先执行,有时候会将其写在load方法中,但是load方法也能被主动调用,如果多次调用,交换后的方法可能被还原

所以我们要保证方法只能交换一次,可以选择在单例模式下,让交换后的方法不会被还原

  1. + (void)load{
  2. static dispatch_once_t onceToken;
  3. dispatch_once(&onceToken, ^{
  4. [self lg_methodSwizzlingWithClass:self oriSEL:@selector(lg_person_say) swizzledSEL:@selector(lg_student_say)];
  5. });
  6. }
  7. + (void)lg_methodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
  8. if (!cls) NSLog(@"传入的交换类不能为空");
  9. Method oriMethod = class_getInstanceMethod(cls, oriSEL);
  10. Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
  11. method_exchangeImplementations(oriMethod, swiMethod);
  12. }

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

父类LGPerson中,实现lg_person_say方法

  1. #import <Foundation/Foundation.h>
  2. @interface LGPerson : NSObject
  3. - (void)lg_person_say;
  4. @end
  5. @implementation LGPerson
  6. - (void)lg_person_say{
  7. NSLog(@"LGPerson:%s",__func__);
  8. }
  9. @end

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

  1. #import "LGPerson.h"
  2. #import <objc/runtime.h>
  3. @interface LGStudent : LGPerson
  4. @end
  5. @implementation LGStudent
  6. + (void)load{
  7. static dispatch_once_t onceToken;
  8. dispatch_once(&onceToken, ^{
  9. [self lg_methodSwizzlingWithClass:self oriSEL:@selector(lg_person_say) swizzledSEL:@selector(lg_student_say)];
  10. });
  11. }
  12. + (void)lg_methodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
  13. if (!cls) NSLog(@"传入的交换类不能为空");
  14. Method oriMethod = class_getInstanceMethod(cls, oriSEL);
  15. Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
  16. method_exchangeImplementations(oriMethod, swiMethod);
  17. }
  18. - (void)lg_student_say{
  19. //lg_studentInstanceMethod -/-> personInstanceMethod
  20. [self lg_student_say];
  21. NSLog(@"LGStudent:%s",__func__);
  22. }
  23. @end

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

  1. 子类调用
  2. LGPerson:-[LGPerson lg_person_say]
  3. LGStudent:-[LGStudent lg_student_say]
  4. 父类调用
  5. -[LGPerson lg_student_say]: unrecognized selector sent to instance 0x28218c3f0

方法交换应该只影响当前类,但子类中交换的是父类方法,导致父类受到影响,其他继承于该父类的子类也会出现问题

解决办法,保证方法交换只对当前类生效

  1. + (void)load{
  2. static dispatch_once_t onceToken;
  3. dispatch_once(&onceToken, ^{
  4. [self lg_betterMethodSwizzlingWithClass:self oriSEL:@selector(lg_person_say) swizzledSEL:@selector(lg_student_say)];
  5. });
  6. }
  7. + (void)lg_betterMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
  8. if (!cls) NSLog(@"传入的交换类不能为空");
  9. Method oriMethod = class_getInstanceMethod(cls, oriSEL);
  10. Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
  11. BOOL success = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(oriMethod));
  12. if (success) {
  13. class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
  14. }else{
  15. method_exchangeImplementations(oriMethod, swiMethod);
  16. }
  17. }
  • 使用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_sayimp
    • lg_student_say替换为父类lg_person_sayimp
    • 调用顺序,依然保持:子类lg_student_say→父类lg_person_say
    • 只会影响子类,不会影响父类

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

当父类和子类都未实现原始方法,上述方式将引发子类方法的递归调用,最终造成堆栈溢出

原因在于:

  • 子类添加的lg_person_say,关联lg_student_sayimp
  • 父类未实现lg_person_say方法,子类使用class_replaceMethod,一定会替换失败,所以子类的lg_student_sayimp未发生改版

解决办法,对原始方法增加是否实现的判断条件

  1. + (void)lg_betterMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
  2. if (!cls) NSLog(@"传入的交换类不能为空");
  3. Method oriMethod = class_getInstanceMethod(cls, oriSEL);
  4. Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
  5. if (!oriMethod) {
  6. IMP imp = imp_implementationWithBlock(^(id self, SEL _cmd){
  7. NSLog(@"伪装lg_person_say方法,其实什么都没做");
  8. });
  9. class_addMethod(cls, oriSEL, imp, method_getTypeEncoding(swiMethod));
  10. }
  11. BOOL success = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(oriMethod));
  12. if (success) {
  13. class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
  14. }else{
  15. method_exchangeImplementations(oriMethod, swiMethod);
  16. }
  17. }
  • 判断如果当前类未实现原始方法,添加lg_person_say方法,关联一个空方法的imp
  • 使用class_addMethod,对当前类添加lg_person_say方法,关联lg_student_sayimp
  • 由于lg_person_say已添加,此时返回值一定为NO,添加失败
  • 使用method_exchangeImplementations,直接将两个方法进行交换

4. 类方法的交换

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

  1. + (void)lg_betterClassMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
  2. if (!cls) NSLog(@"传入的交换类不能为空");
  3. Class metaClass = objc_getMetaClass(NSStringFromClass(cls).UTF8String);
  4. Method oriMethod = class_getInstanceMethod(metaClass, oriSEL);
  5. Method swiMethod = class_getInstanceMethod(metaClass, swizzledSEL);
  6. if (!oriMethod) {
  7. IMP imp = imp_implementationWithBlock(^(id self, SEL _cmd){
  8. NSLog(@"伪装lg_person_say方法,其实什么都没做");
  9. });
  10. class_addMethod(metaClass, oriSEL, imp, method_getTypeEncoding(swiMethod));
  11. }
  12. BOOL success = class_addMethod(metaClass, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(oriMethod));
  13. if (success) {
  14. class_replaceMethod(metaClass, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
  15. }else{
  16. method_exchangeImplementations(oriMethod, swiMethod);
  17. }
  18. }

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

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

类名 类簇
NSArray __NSArrayI
NSMutableArray __NSArrayM
NSDictionary __NSDictionaryI
NSMutableDictionary __NSDictionaryM

替换NSArrayobjectAtIndex方法,避免数组越界

  1. @implementation NSArray (LG)
  2. + (void)load{
  3. Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
  4. Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(lg_objectAtIndex:));
  5. method_exchangeImplementations(fromMethod, toMethod);
  6. }
  7. - (id)lg_objectAtIndex:(NSUInteger)index{
  8. if (self.count-1 < index) {
  9. #ifdef DEBUG
  10. // 调试阶段
  11. return [self lg_objectAtIndex:index];
  12. #else
  13. // 发布阶段
  14. @try {
  15. return [self lg_objectAtIndex:index];
  16. } @catch (NSException *exception) {
  17. NSLog(@"lg_objectAtIndex crash:%@", [exception callStackSymbols]);
  18. return nil;
  19. } @finally {
  20. }
  21. #endif
  22. }else{
  23. return [self lg_objectAtIndex:index];
  24. }
  25. }
  26. @end

总结:

基本介绍:

  • 利用OCRuntime特性,动态改变SEL(方法编号)和IMP(方法实现)的对应关系,达到OC方法调用流程改变的目的。主要用于OC方法

API介绍:

  • 通过SEL获取方法Method

    • class_getInstanceMethod:获取实例方法

    • class_getClassMethod:获取类方法

  • IMPgetter/setter方法

    • method_getImplementation:获取一个方法的实现

    • method_setImplementation:设置一个方法的实现

  • method_getTypeEncoding:获取方法实现的编码类型

  • class_addMethod:添加方法实现

  • class_replaceMethod:替换方法的IMP。如:A替换B,即:B指向AA还是指向A

  • method_exchangeImplementations:交换两个方法的IMP。如:A交换B,即:B指向AA指向B

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

  • 可以选择在单例模式下,让交换后的方法不会被还原

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

  • 保证方法交换只对当前类生效

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

  • 对原始方法增加是否实现的判断条件

类方法的交换:

  • 不能直接使用Class,而是要使用当前Class所属的MetaClass

数组、字典的方法交换:

iOS中,NSArrayNSDictionary等类,都有类簇的存在。所以对NSArrayNSDictionary进行方法交换,必须对其真身进行操作