ios 运行时动态添加方法避免崩溃 respondsToSelector

373 阅读5分钟

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种方式去补救:

  1. 调用resolveInstanceMethod给个机会让类添加这个实现这个函数
  2. 调用forwardingTargetForSelector让别的对象去执行这个函数
  3. 调用forwardInvocation(函数执行器)灵活的将目标函数以及其他形式执行。

如果都不行,系统才会调用doesNotRecognizeSelector抛出异常。

既然可以补救,我们完全也可以利用消息转发机制来做文章,但是我们选择哪一步比较合适呢?

  1. resolveInstanceMethod需要在类的本身动态的添加它本身不存在的方法,这些方法对于该类本身来说是冗余的
  2. forwardInvocation可以通过NSInvocation的形式将消息转发给多个对象,但是其开销比较大,需要创建新的NSInvocation对象,并且forwardInvocation的函数经常被使用者调用来做消息的转发选择机制,不适合多次重写
  3. 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…