runtime运行时
- 是OC面向对象编程语言的运行环境,类似java的虚拟机
- runtime是OC的底层实现,OC代码最终转换为runtime的C语言库的东西
- runtime能实现的功能:
- 获取类中的成员变量
- 为类动态的增加成员变量
- 动态改变类的方法实现
- 为类动态的增加新的方法
- 字典转模型,动态获取类的属性数组,通过KVC设置数值
一、Method Swizzling
在iOS中,运行时动态添加方法可以通过Method Swizzling实现,但是直接给类添加方法有可能导致崩溃,因为类的实例可能不会响应这个新添加的方法。这时,可以使用respondsToSelector:方法来判断实例是否能响应某个方法。
以下是一个简单的例子,演示如何在运行时动态添加方法,并使用respondsToSelector:来避免崩溃:
#import <objc/runtime.h>
#import <Foundation/Foundation.h>
@implementation NSObject (DynamicMethodExample)
+ (void)load {
// 获取类,并为其添加方法
Class class = [self class];
// 创建方法签名
SEL originalSelector = @selector(originalMethod);
Method originalMethod = class_getInstanceMethod(class, originalSelector);
// 判断是否有需要被替换的方法
if (originalMethod) {
SEL swizzledSelector = @selector(swizzledMethod);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
// 如果没有swizzledMethod,则添加它
if (!swizzledMethod) {
// 实现并注册swizzledMethod
class_addMethod(class, swizzledSelector, (IMP)swizzledMethodIMP, "v@:");
}
// 交换方法
if (swizzledMethod && originalMethod) {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
}
// 新方法的空实现
static void swizzledMethodIMP(id self, SEL _cmd) {
// 方法体
NSLog(@"This is the swizzled method.");
}
- (void)originalMethod {
// 原始方法实现
NSLog(@"This is the original method.");
}
- (void)swizzledMethod {
// 新方法实现
if ([self respondsToSelector:@selector(originalMethod)]) {
[self originalMethod];
} else {
// 没有原始方法的情况
NSLog(@"No original method to call.");
}
}
@end
在这个例子中,我们使用了+load方法来在类被加载时进行方法交换。如果原始方法存在,我们将其与swizzledMethodIMP方法实现进行交换。如果swizzledMethod第一次被调用,并且原始方法不存在,我们通过respondsToSelector:来避免崩溃。这种方式可以在不破坏现有代码的前提下动态添加方法,并且提供了一个回退方案以防止方法不存在时的崩溃。
二、消息转发机制
@implementation HelloClass
//这里没啥用
-(BOOL)respondsToSelector:(SEL)aSelector{
bool a= [super respondsToSelector:aSelector];
return a;
}
//如果方法没有实现,默认返回false
//如果返回false,就会走消息转发
+(BOOL)resolveInstanceMethod:(SEL)sel{
bool a = [super resolveInstanceMethod:sel];
return a;
}
//默认返回空
//又被称为快速消息转发。
// 如果为空,走慢速消息转发,继续转发消息
-(id)forwardingTargetForSelector:(SEL)aSelector{
id a = [super forwardingTargetForSelector:aSelector];
return a;
}
//默认实现是崩溃
//并且不能用try-catch捕获
-(void)forwardInvocation:(NSInvocation *)anInvocation{
[super forwardInvocation:anInvocation];
NSLog(@"");
}
// 默认一般普通方法是返回空的。
// 如果是协议方法,没有实现,不会反回空。
//反回空,到这里就会崩溃了
//如果这里返回了签名,会再次调用resolveInstanceMethod:(SEL)sel判断是否实现
//如果仍然没有实现,就会走到fowardInvocation:
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
NSMethodSignature *a =[super methodSignatureForSelector:aSelector];
return a;
}
@end
拦截调用:
拦截调用就是,在找不到调用的方法程序崩溃之前,你有机会通过重写NSObject的四个方法来处理:
+ (BOOL)resolveClassMethod:(SEL)sel; //动态类方法决议机制,决议类方法
+ (BOOL)resolveInstanceMethod:(SEL)sel;//动态的对象方法决议,决议对象方法
//后两个方法需要转发到其他的类处理
- (id)forwardingTargetForSelector:(SEL)aSelector;//转发给其他的一个对象处理函数
- (void)forwardInvocation:(NSInvocation *)anInvocation;//灵活的将目标函数以其他形式执行
拦截调用的整个流程即Objective-C的消息转发机制,我们不难发现,runtime提供了3种方式去补救:
- 调用resolveInstanceMethod给个机会让类添加这个实现这个函数
- 调用forwardingTargetForSelector让别的对象去执行这个函数
- 调用forwardInvocation(函数执行器)灵活的将目标函数以及其他形式执行。
如果都不行,系统才会调用doesNotRecognizeSelector抛出异常。
既然可以补救,我们完全也可以利用消息转发机制来做文章,但是我们选择哪一步比较合适呢?
- resolveInstanceMethod需要在类的本身动态的添加它本身不存在的方法,这些方法对于该类本身来说是冗余的
- forwardInvocation可以通过NSInvocation的形式将消息转发给多个对象,但是其开销比较大,需要创建新的NSInvocation对象,并且forwardInvocation的函数经常被使用者调用来做消息的转发选择机制,不适合多次重写
- forwardingTargetForSelector可以将消息转发给一个对象,开销较小,并且被重写的概率较低,适合重写
我们可以重写NSObject的该方法,可以做以下几步的处理:
- 第一步:为类动态的创建一个桩类
- 第二步:为类动态为桩类添加对应的Selector,用一个通用的返回0的函数来实现该SEL的IMP
- 第三步:将消息直接转发到这个桩类对象上。
具体的代码实现如下:
- (id)yh_forwardingTargetForSelector:(SEL)aSelector {
if(class_respondsToSelector([self class], @selector(forwardInvocation:))) {
IMP impOfNSObject = class_getMethodImplementation([NSObject class], @selector(forwardInvocation:));
IMP imp = class_getMethodImplementation([self class], @selector(forwardInvocation:));
if (imp != impOfNSObject) {
NSLog(@"class has implemented invocation");
return nil;
}
}
YHUnrecognizedSelectorSolveObject * solveObject = [YHUnrecognizedSelectorSolveObject new];
solveObject.objc = self;
return solveObject;
}
注意如果对象的类本身如果重写了forwardInvocation方法的话,就不应该对forwardingTargetForSelector进行重写了,否则会影响到该类型的对象原本的消息转发流程。
参考: github.com/lsmakethebe… github.com/chenfanfang… github.com/lizilong198… juejin.cn/post/684490… www.jianshu.com/p/f8396fa2c…