前言
“这是我参与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);
}
}
复制代码