1、概述
利用 OC 的Runtime
特性,动态改变SEL
(方法编号)和IMP
(方法实现)的对应关系,达到 OC方法调用流程改变 的目的,主要用于OC方法
1.1、关系类比
类比关系为书的目录页,SEL是标题、IMP是页码
- 看到标题(SEL)我们能大致知道这几页讲的是什么;根据页码(IMP)我们能快速找到内容位置
- 它们之间是一一对应的,但我们也可以将它们的对应关系进行修改(书印错了我们改一下不犯法~)
1.2、交换方法
-
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); 复制代码
/* OBJC_AVAILABLE: shorthand for all-OS availability */ # if !defined(OBJC_AVAILABLE) # define OBJC_AVAILABLE(x, i, t, w, b) __OSX_AVAILABLE(x) __IOS_AVAILABLE(i) __TVOS_AVAILABLE(t) \ __WATCHOS_AVAILABLE(w) # endif 复制代码
OBJC_AVAILABLE(10.5, 2.0, 9.0, 2.0)
表示这个这个API在哪个系统哪个版可用:__OSX_AVAILABLE(x)
:Mac OS的版本__IOS_AVAILABLE(i)
:iOS系统的版本__TVOS_AVAILABLE(t)
:苹果电视系统的版本__WATCHOS_AVAILABLE(w)
:苹果手表系统的版本
-
通过这个函数交换两个 SEL 和 IMP 对应关系的技术,我们就称之为
Method Swizzle
(方法欺骗)
1.3、AOP面向切面编程
- Runtime机制对于
AOP
面向切面编程提供良好的支持,在OC中,可利用 Method Swizzling 实现 AOP - 其中 AOP(
Aspect Oriented Programming
)是一种编程的思想,和面向对象编程 的OOP
有本质的区别:- OOP 和 AOP 都是编程的思想
- OOP 编程思想更加倾向于 对业务模块的封装,划分出更加清晰的逻辑单元
- 而 AOP 是面向切面进行提取封装,提取各个模块中的公共部分,提高模块的复用率,降低业务之间的耦合性
2、API
2.1、通过 SEL 获取方法Method
// 获取实例方法
OBJC_EXPORT Method _Nullable
class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name);
// 获取类方法
OBJC_EXPORT Method _Nullable
class_getClassMethod(Class _Nullable cls, SEL _Nonnull name);
复制代码
2.2、IMP 的getter/setter方法
// 获取一个方法的实现
OBJC_EXPORT IMP _Nonnull
method_getImplementation(Method _Nonnull m);
// 设置一个方法的实现
OBJC_EXPORT IMP _Nonnull
method_setImplementation(Method _Nonnull m, IMP _Nonnull imp):
复制代码
2.3、替换方法
// 获取方法实现的编码类型
OBJC_EXPORT const char * _Nullable
method_getTypeEncoding(Method _Nonnull m);
// 添加方法实现
OBJC_EXPORT BOOL
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp,
const char * _Nullable types);
// 替换方法的 IMP,如:A替换B(B指向A,A还是指向A)
OBJC_EXPORT IMP _Nullable
class_replaceMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp,
const char * _Nullable types);
// 交换两个方法的 IMP,如:A交换B(B指向A,A指向B)
OBJC_EXPORT void
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2);
复制代码
3、坑点
3.1、保证方法交换只执行一次
为了保证方法交换的代码可以优先执行,有时候会将其写在+load
方法中,但是 +load 方法也能被主动调用,如果多次调用,交换后的方法可能被还原;所以我们要保证方法只交换一次,可以选择在单例模式下
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self lz_methodSwizzlingWithClass:self oriSEL:@selector(study) swizzledSEL:@selector(play)];
});
}
+ (void)lz_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、父类未实现子类将要交换的方法
-
父类 LZPerson 中,实现 study 方法
#import <Foundation/Foundation.h> @interface LZPerson : NSObject - (void)study; @end @implementation LZPerson - (void)study{ NSLog(@"LZPerson:%s",__func__); } @end 复制代码
-
子类 LZStudent 中,实现 play 方法,在 +load 方法中,和父类的 study 方法交换
#import "LZPerson.h" #import <objc/runtime.h> @interface LZStudent : LZ`Person @end @implementation LZStudent + (void)load{ static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [self lz_methodSwizzlingWithClass:self oriSEL:@selector(study) swizzledSEL:@selector(play)]; }); } + (void)lz_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)play{ [self play]; NSLog(@"LZStudent:%s",__func__); } @end 复制代码
-
子类正常调用,但父类找不到 play 方法
// 子类调用 LZPerson :-[LZPerson study] LZStudent:-[LZStudent play] // 父类调用 -[LZPerson play]: unrecognized selector sent to instance 0x28218c3f0 复制代码
方法交换应该只影响当前类,但子类中交换的是父类方法,导致父类受到影响,其他继承于该父类的子类也会出现问题
-
解决办法:保证方法交换只对当前类生效
+ (void)load{ static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ [self lz_betterMethodSwizzlingWithClass:self oriSEL:@selector(study) swizzledSEL:@selector(play)]; }); } + (void)lz_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
,对当前类添加 study 方法,关联 play方法 的 imp - 返回值为 YES,证明当前类中未实现 study 方法
- 如果方法添加成功,使用
class_replaceMethod
,将 play方法 替换为 study 的 imp
- 使用
上述方式需要注意:
-
如果子类实现 study 方法
- 添加失败,直接交换
- 不会影响父类
-
如果子类未实现 study 方法
- 添加成功,新方法关联 play 的 imp
- 将 play 替换为 父类 study 的 imp
- 调用顺序,依然保持:子类play --> 父类study
- 只会影响子类,不会影响父类
3.3、父类和子类都未实现原始方法
当 父类和子类都未实现原始方法,上述方式将引发子类方法的递归调用,最终造成 堆栈溢出
-
原因在于:
- 子类添加的 study,关联 play 的imp
- 父类未实现study方法,子类使用
class_replaceMethod
,一定会替换失败,所以 子类的play的imp未发生改变
-
解决办法,对原始方法增加是否实现的判断条件
+ (void)lz_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(@"伪装study方法,其实什么都没做"); }); 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); } } 复制代码
- study 方法,关联一个空方法的imp
- 使用
class_addMethod
,对 当前类添加 study 方法,关联play的imp - 由于 study 已添加,此时返回值一定为
NO
,添加失败 - 使用
method_exchangeImplementations
,直接将两个方法进行交换
4、类方法的交换
类方法和实例方法的区别,类方法存储在元类的方法列表中,所以对类方法的添加和替换,不能直接使用Class,而是要使用当前Class所属的 MetaClass
+ (void)lz_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(@"伪装study方法,其实什么都没做");
});
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 (LZ)
+ (void)load{
Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(lzl_objectAtIndex:));
method_exchangeImplementations(fromMethod, toMethod);
}
- (id)lz_objectAtIndex:(NSUInteger)index{
if (self.count-1 < index) {
#ifdef DEBUG
// 调试阶段
return [self lz_objectAtIndex:index];
#else
// 发布阶段
@try {
return [self lz_objectAtIndex:index];
} @catch (NSException *exception) {
NSLog(@"lz_objectAtIndex crash:%@", [exception callStackSymbols]);
return nil;
} @finally {
}
#endif
}else{
return [self lz_objectAtIndex:index];
}
}
@end
复制代码
6、Runtime方法使用汇总
- Runtime的使用可参考:Runtime方法汇总