前言
“这是我参与8月更文挑战的第10天,活动详情查看:8月更文挑战”
基本原理
我们都知道,类的最终父类是NSObject,在程序编译后,在底层可以发现类就是一个结构体,每个类都有一个 isa 指针,能够访问到结构体里面的数据。方法查找的是时候,是在类的方法列表里面,通过SEL查找对应的IMP。
大家或多或少听到过iOS黑魔法,也就是方法交换。同时苹果的运行时 runtime 也提供了一个很好的环境。利用OC的Runtime特性,动态改变SEL(方法编号)和IMP(方法实现)的对应关系,达到OC方法调用流程改变的目的。主要用于OC方法。下面用两个图例来展示下:
- 交换前(正常情况):
- 交换后:
根据这两张图,我们能稍微明白些这个交换的是怎么一回事。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);
Runtime机制对于AOP面向切面编程提供良好的支持。在OC中,可利用Method Swizzling实现AOP,其中AOP(Aspect Oriented Programming)是一种编程的思想,同样面向对象编程OOP也一种编程的思想,但是AOP和OOP有本质的区别:
OOP编程思想,他更加倾向于对业务模块的封装,同时也能够划分出更为清晰的业务逻辑单元;AOP编程思想,是面向切面进行提取封装,提取各个模块中的公共部分,这样能提高模块的复用率,降低业务之间的耦合性;
API 介绍
- 通过
SEL获取方法Method:
获取实例方法
OBJC_EXPORT Method _Nullable
class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name) OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
获取类方法
OBJC_EXPORT Method _Nullable
class_getClassMethod(Class _Nullable cls, SEL _Nonnull name) OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
IMP的getter/setter方法:
获取某个方法的实现
OBJC_EXPORT IMP _Nonnull
method_getImplementation(Method _Nonnull m) OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
设置一个方法的实现
OBJC_EXPORT IMP _Nonnull
method_setImplementation(Method _Nonnull m, IMP _Nonnull imp) OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
- 获取方法实现的编码类型
OBJC_EXPORT const char * _Nullable
method_getTypeEncoding(Method _Nonnull m) OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
- 添加方法实现
OBJC_EXPORT void
class_addMethods(Class _Nullable, struct objc_method_list * _Nonnull) OBJC2_UNAVAILABLE;
- 替换方法的
IMP。
OBJC_EXPORT IMP _Nullable
class_replaceMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types) OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
- 交换两个方法的
IMP。
OBJC_EXPORT void
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2) OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
- 尽量放在单利里面,这样能保证只调用一次,保证安全。
案例分析
交换方法的使用
创建一个类LGPerson,然后创建LGTeacher继承LGPerson,使用如下代码:
// LGPerson .h
@interface LGPerson : NSObject
- (void)person_instanceMethod;
@end
// LGPerson.m
@implementation LGPerson
- (void)person_instanceMethod {
NSLog(@"\n 打印 person_instanceMethod: %s\n", __func__);
}
@end
// LGTeacher.h
@implementation LGTeacher
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{ [LGRuntimeUtil
lg_methodSwizzlingWithClass:self
oriSEL:@selector(person_instanceMethod)
swizzledSEL:@selector(teacher_instanceMethod)];
});
}
- (void)teacher_instanceMethod {
NSLog(@"\n 打印 teacher_instanceMethod: %s\n", __func__);
}
@end
// 封装LGRuntimeUtil.m
+ (void)lg_methodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL {
if (!cls) NSLog(@"传入的交换类不能为空");
Method oriMethod = class_getInstanceMethod(cls, oriSEL);
Method swizzleMethod = class_getInstanceMethod(cls, swizzledSEL);
method_exchangeImplementations(oriMethod, swizzleMethod);
}
在 main 文件里面初始化这两个类,都调用 person_instanceMethod 方法:
LGPerson *person = [LGPerson alloc] init];
[person person_instanceMethod];
LGTeacher *teacher = [LGTeacher alloc] init];
[teacher person_instanceMethod];
但是,打印出来的方法名,却都是 teacher_instanceMethod 。那么就说明替换成功了。
-
因为
person_instanceMethod的SEL找到的是teacher_instanceMethod的IMP,所以找到的就是teacher_instanceMethod方法; -
而
teacher_instanceMethod的SEL找到的却是person_instanceMethod的IMP,但IMP对应的是person_instanceMethod方法,再继续根据person_instanceMethod方法的SEL找到的是交换后的IMP,所以找到了teacher_instanceMethod方法。
递归问题
就是在 teacher_instanceMethod 方法里面,再次调用 teacher_instanceMethod,代码如下:
- (void)teacher_instanceMethod {
[self teacher_instanceMethod];
NSLog(@"\n 打印 teacher_instanceMethod: %s\n", __func__);
}
运行之后,直接报错:
为什么会这样了?
-
对于
LGTeacher而言调用person_instanceMethod就是调用LGTeacher:teacher_instanceMethod-> LGPerson:person_instanceMethod。 -
对于
LGPerson调用person_instanceMethod是调用LGTeacher:teacher_instanceMethod -> LGPerson:teacher_instanceMethod。而LGPerson没有实现teacher_instanceMethod,所以报错。
所以交换方法一定是去交换自己的方法。
-
为什么要调用自己呢? 因为有时候,在做一些处理的时候,需要保持原来的逻辑,所以需要再次调用本类。
-
那怎样才能避免这类的情况了? 可以通过
class_addMethod去尝试添加要交换的方法。
性能优化一
-
class_addMethod方法的使用,我们可以使用这个方法来添加要交换的方法:-
如果
添加成功,说明在本类中没有这个方法,但是可以通过class_replaceMethod进行替换,其内部会调用class_addMethod进行添加的方法; -
如果添加不成功,就说明类里面有这个方法,则通过
method_exchangeImplementations进行交换
-
-
代码如下:
+ (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);
}
}
性能优化二
根据上面的使用案例,如果子类和父类都没有实现person_instanceMethod这个方法,在子类里面调用[self teacher_instanceMethod]时,就会产生递归,如果不处理,就回报错。
怎么解决了?如果该方法不存在,可以在添加方法后,再给此方法添加一个空的实现,也就是相当于增加一个不做任何事情的IMP,代码如下:
+ (void)ssl_bestMethodSwizzlingWithClass:(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) {
// 在 oriMethod 为 nil 时,替换后将swizzledSEL复制一个不做任何事的空实现
class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){ NSLog(@"来了一个空的 imp"); }));
}
// 尝试添加你要交换的方法
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);
}
}