1. 基本介绍
利用OC
的Runtime
特性,动态改变SEL
(方法编号)和IMP
(方法实现)的对应关系,达到OC
方法调用流程改变的目的。主要用于OC
方法
在OC
中,SEL
和IMP
之间的关系,就好像一本书的“目录
”
SEL
是方法编号,就像“标题
”一样IMP
是方法实现的真实地址,就像“页码
”一样- 它们是一一对应的关系
Runtime
提供了交换两个SEL
和IMP
对应关系的函数
OBJC_EXPORT void
method_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
获取方法Method
class_getInstanceMethod
:获取实例方法class_getClassMethod
:获取类方法
IMP
的getter/setter
方法method_getImplementation
:获取一个方法的实现method_setImplementation
:设置一个方法的实现
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_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
获取方法Method
class_getInstanceMethod
:获取实例方法class_getClassMethod
:获取类方法
IMP
的getter/setter
方法method_getImplementation
:获取一个方法的实现method_setImplementation
:设置一个方法的实现
method_getTypeEncoding
:获取方法实现的编码类型class_addMethod
:添加方法实现class_replaceMethod
:替换方法的IMP
。如:A
替换B
,即:B
指向A
,A
还是指向A
method_exchangeImplementations
:交换两个方法的IMP
。如:A
交换B
,即:B
指向A
,A
指向B
保证方法交换只执行一次:
- 可以选择在单例模式下,让交换后的方法不会被还原
父类未实现子类将要交换的方法:
- 保证方法交换只对当前类生效
父类和子类都未实现原始方法:
- 对原始方法增加是否实现的判断条件
类方法的交换:
- 不能直接使用
Class
,而是要使用当前Class
所属的MetaClass
数组、字典的方法交换:
在iOS
中,NSArray
和NSDictionary
等类,都有类簇的存在。所以对NSArray
、NSDictionary
进行方法交换,必须对其真身进行操作