1. 基本介绍
利用OC的Runtime特性,动态改变SEL(方法编号)和IMP(方法实现)的对应关系,达到OC方法调用流程改变的目的。主要用于OC方法
在OC中,SEL和IMP之间的关系,就好像一本书的“目录”
SEL是方法编号,就像“标题”一样IMP是方法实现的真实地址,就像“页码”一样- 它们是一一对应的关系
Runtime提供了交换两个SEL和IMP对应关系的函数
OBJC_EXPORT voidmethod_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2)OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
通过这个函数交换两个SEL和IMP对应关系的技术,我们称之为Method Swizzle(方法欺骗)
Runtime机制对于AOP面向切面编程提供良好的支持。在OC中,可利用Method Swizzling实现AOP,其中AOP(Aspect Oriented Programming)是一种编程的思想,和面向对象编程OOP有本质的区别
OOP和AOP都是编程的思想OOP编程思想更加倾向于对业务模块的封装,划分出更加清晰的逻辑单元- 而
AOP是面向切面进行提取封装,提取各个模块中的公共部分,提高模块的复用率,降低业务之间的耦合性
2. API介绍
通过
SEL获取方法Methodclass_getInstanceMethod:获取实例方法class_getClassMethod:获取类方法
IMP的getter/setter方法method_getImplementation:获取一个方法的实现method_setImplementation:设置一个方法的实现
method_getTypeEncoding:获取方法实现的编码类型class_addMethod:添加方法实现class_replaceMethod:替换方法的IMP。如:A替换B,即:B指向A,A还是指向Amethod_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_say的imp - 返回值为
YES,证明当前类中未实现lg_person_say方法 - 如果方法添加成功,使用
class_replaceMethod,将lg_student_say方法,替换为lg_person_say的imp
上述方式:
- 如果子类实现
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_say的imp - 由于
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
总结:
基本介绍:
- 利用
OC的Runtime特性,动态改变SEL(方法编号)和IMP(方法实现)的对应关系,达到OC方法调用流程改变的目的。主要用于OC方法
API介绍:
通过
SEL获取方法Methodclass_getInstanceMethod:获取实例方法class_getClassMethod:获取类方法
IMP的getter/setter方法method_getImplementation:获取一个方法的实现method_setImplementation:设置一个方法的实现
method_getTypeEncoding:获取方法实现的编码类型class_addMethod:添加方法实现class_replaceMethod:替换方法的IMP。如:A替换B,即:B指向A,A还是指向Amethod_exchangeImplementations:交换两个方法的IMP。如:A交换B,即:B指向A,A指向B
保证方法交换只执行一次:
- 可以选择在单例模式下,让交换后的方法不会被还原
父类未实现子类将要交换的方法:
- 保证方法交换只对当前类生效
父类和子类都未实现原始方法:
- 对原始方法增加是否实现的判断条件
类方法的交换:
- 不能直接使用
Class,而是要使用当前Class所属的MetaClass
数组、字典的方法交换:
在iOS中,NSArray和NSDictionary等类,都有类簇的存在。所以对NSArray、NSDictionary进行方法交换,必须对其真身进行操作
