1 Overview
Using the characteristics of OC , dynamically change the corresponding relationship between (method number) and (method implementation) to achieve the purpose of changing the OC method calling process , mainly used for OC methodsRuntime
SEL
IMP
1.1, relational analogy
The analogy is the table of contents pages of a book, where SEL is the title and IMP is the page number
- Seeing the title (SEL), we can roughly know what these pages are talking about; according to the page number (IMP), we can quickly find the content location
- There is a one-to-one correspondence between them, but we can also modify their corresponding relationship (if the book is wrong, we will change it and not break the law~)
1.2, exchange method
-
Runtime provides a function to exchange correspondence between two SEL and 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)
Indicates which system and version this API is available on:__OSX_AVAILABLE(x)
: Mac OS version__IOS_AVAILABLE(i)
: The version of the iOS system__TVOS_AVAILABLE(t)
: version of Apple TV system__WATCHOS_AVAILABLE(w)
: The version of the Apple Watch system
-
The technique of exchanging the correspondence between two SEL and IMP through this function, we call it
Method Swizzle
( method cheating )
1.3, AOP aspect-oriented programming
- The Runtime mechanism
AOP
provides good support for aspect-oriented programming. In OC, Method Swizzling can be used to implement AOP - Among them, AOP (
Aspect Oriented Programming
) is a programming idea, which is essentially different from object-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方法汇总