美文网首页
iOS消息转发及其应用

iOS消息转发及其应用

作者: Wardw | 来源:发表于2021-12-29 15:04 被阅读0次

什么是方法

struct objc_method
{
  // 方法名:方法名为此方法的签名,有着相同函数名和参数名的方法有着相同的方法名。
  SEL method_name;
  // 方法类型:方法类型描述了参数的类型。
  char * method_types;
  // IMP: IMP即函数指针,为方法具体实现代码块的地址,可像普通C函数调用一样使用IMP。
  IMP method_imp;
};
typedef objc_method Method;

方法查找流程
方法查找慢流程

objc_class.png

方法是怎么调用的

  • 消息传递机制

在Objective-C中,方法调用形式如同 [person run],被称为消息发送,即“对person对象发送run消息”;简单来说,分为以下几步:

[person run]会被翻译成objc_msgSend(person, @selector(run))
从类Person的方法列表中查找到run 方法的信息(底层是method_t类型),此处信息是个结构体,包含方法名、类型(返回值和参数类型)、指向实际代码的指针

多啰嗦一句,@selector(run)返回的SEL类型其实是个字符串,Objective-C 方法查找是通过这个字符串匹配查找的,远远没有静态函数的调用高效,所以在源码层添加了一层缓存,缓解了多次查找低效问题

  • 调用

通常调用方法的方式是使用[实例 方法名]或[实例 方法名:参数]

[self sayHello];
或
[self sayOne:@"1"];

msgSend

((void (*)(id, SEL, id, id, id))(void *)objc_msgSend)(person, NSSelectorFromString(@"sayOne:two:three:"), @"one", @"two", @"three");

若该方法没有公开,可以使用NSObject的performSelector方法,但performSelector只支持调用最多两个入参且入参类型和返回类型为id的方法。

[person performSelector:@selector(sayHello)];
[person performSelector:@selector(sayOne:) withObject:@"1"];
[person performSelector:@selector(sayOne:two:) withObject:@"1" withObject:@"2"];

若入参的个数多于两个,可以使用NSInvocation来调用方法。

SEL sel = NSSelectorFromString(@"sayOne:two:three:");
    NSMethodSignature *signature = [person methodSignatureForSelector:sel];
//    NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:"v@:@:@:"];
//    创建 NSInvocation
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
    // 设置接收消息的对象
    [invocation setTarget:person];
   
    // 设置发送的方法名
    [invocation setSelector:sel];
    
    NSString *one = @"1";
    [invocation setArgument:&one atIndex:2];
    
    NSString *two = @"2";
    [invocation setArgument:&two atIndex:3];
    
    NSString *three = @"3";
    [invocation setArgument:&three atIndex:4];
   
    // 调用NSInvocation
    [invocation invoke];

NSInvocation的API

@interface NSInvocation : NSObject

//根据方法签名来初始化实例对象
//方法签名 可查看第三节
+ (NSInvocation *)invocationWithMethodSignature:(NSMethodSignature *)sig;
//对象的方法签名 只读
@property (readonly, retain) NSMethodSignature *methodSignature;
//强引用传入的参数,防止参数被释放
- (void)retainArguments;
//当前的参数是否为强引用 只读
@property (readonly) BOOL argumentsRetained;
//调用该方法的对象
@property (nullable, assign) id target;
//要调用的方法的选择器 
@property SEL selector;

//获取该方法的返回值
//retLoc 一个变量的地址,该变量会保存返回值
- (void)getReturnValue:(void *)retLoc;
//设置该方法的返回值,虽然方法会调用,但返回值则会被该值替换
//retLoc 一个变量的地址,该变量的值即为要设置的返回值
- (void)setReturnValue:(void *)retLoc;

//获取该方法对应索引的参数值
//argumentLocation 一个变量的地址,该变量会保存参数的值
//idx 第几个参数 从2开始 前两个分别被该方法的self与_cmd占用
- (void)getArgument:(void *)argumentLocation atIndex:(NSInteger)idx;
//设置该方法对应索引的参数值
//argumentLocation 一个变量的地址,该变量的值即为要设置的参数值
//idx 第几个参数 从2开始 前两个分别被该方法的self与_cmd占用
- (void)setArgument:(void *)argumentLocation atIndex:(NSInteger)idx;

//调用
- (void)invoke;
//调用 会替换属性target
- (void)invokeWithTarget:(id)target;

@end
  • IMP

上面我们知道,针对[person run]等价于objc_msgSend(person, @selector(run)),从类Person的方法列表中查找到run 方法的信息个结构体,里面包含了指向实际代码的指针,然后进行调用
如何修改呢?
既然是个结构体,我们便有办法修改指向实际代码的指针指向,使指调用到别处

Runtime 提供了相应的API,可以很方便的进行替换,这就是我们经常听说的 method swizzle

class_getInstanceMethod 获取Class的实例方法,即“-”号方法,返回Method结构
class_getClassMethod 获取Class的类方法,即“+”号方法,返回Method结构
method_exchangeImplementations 交换方法实现

void method_exchangeImplementations(Method m1, Method m2)
{
    if (!m1  ||  !m2) return;

    mutex_locker_t lock(runtimeLock);

    IMP m1_imp = m1->imp;
    m1->imp = m2->imp;
    m2->imp = m1_imp;
    ...
}
属性对应表.png
苹果官方手册

消息转发机制

iOS消息转发机制.png

消息转发的应用

#pragma mark - 消息机制的第一步 消息处理机制 判断是否能接受SEL

/**
 类:如果是类方法的调用,首先会触发该类方法
 
 @param sel 传递进入的方法
 @return 如果YES则能接受消息 NO不能接受消息 进入第二步
 */
+ (BOOL)resolveClassMethod:(SEL)sel{

    if ([NSStringFromSelector(sel) isEqualToString:@"testClassFunction"]) {
        /**
         对类进行添加类方法 需要将方法添加进入元类内
         */
        return YES;
    }
    return [super resolveClassMethod:sel];
}

/**
 对象:在接受到无法解读的消息的时候 首先会调用所属类的类方法

 @param sel 传递进入的方法
 @return 如果YES则能接受消息 NO不能接受消息 进入第二步
 */
+ (BOOL)resolveInstanceMethod:(SEL)sel{
    //判断是否为外部调用的方法
    if ([NSStringFromSelector(sel) isEqualToString:@"sayHello12"]) {
        /**
         对类进行对象方法 需要把方法添加进入类内
         */
        return YES;
    }
    BOOL a = [super resolveInstanceMethod:sel];
    return a;
}

#pragma mark - 消息机制的第二步 消息转发机制

/**
 转发SEL去对象内部的其他可以响应的对象

 @param aSelector 需要被响应的方法SEL
 @return 返回一个可以被响应的该SEL的对象 如果返回self或者nil,则说明没有可以响应的目标 则进入第三步
 */
- (id)forwardingTargetForSelector:(SEL)aSelector{
    if ([NSStringFromSelector(aSelector) isEqualToString:@"testFunction"]) {
        // 这里返回一个可以想要的目标
        return [Dog new];
    }
    return [super forwardingTargetForSelector:aSelector];
}


#pragma mark - 消息机制的第三步 完整的消息转发机制

// 第三步的消息转发机制本质上跟第二步是一样的都是切换接受消息的对象
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    
    /**
    1.手动创建签名 但是尽量少使用 因为容易创建错误 可以按照这个规则来创建
    https://blog.csdn.net/ssirreplaceable/article/details/53376915
    根据OBJC的编码类别进行编写后面的char (但是容易写错误,所以建议使用下面的方法)
    NSMethodSignature *sign = [NSMethodSignature signatureWithObjCTypes:"v@:"];
     //写法例子
     //例子"v@:@"
     //v@:@ v:返回值类型void;@ id类型,执行sel的对象;:SEL;@参数
     //例子"@@:"
     //@:返回值类型id;@id类型,执行sel的对象;:SEL
    2.自动创建签名
     BackupTestMessage * backUp = [BackupTestMessage new];
     NSMethodSignature * sign = [backUp methodSignatureForSelector:aSelector];
     使用对象本身的methodSignatureForSelector自动获取该SEL对应类别的签名
    */
    
    // 如果返回为nil则进行手动创建签名
    if ([super methodSignatureForSelector:aSelector] == nil) {
        NSMethodSignature * sign = [NSMethodSignature signatureWithObjCTypes:"v@:"];
        return sign;
    }
    return [super methodSignatureForSelector:aSelector];
}

// 上方方法如果调用返回有签名 则进入消息转发最后一步
// JSPatch 就是使用了该方法 来做了动态热更新
- (void)forwardInvocation:(NSInvocation *)anInvocation {
    
//    id argument2;
//    [anInvocation getArgument:&argument2 atIndex:0];
    // 创建备用对象
    Dog *dog = [Dog new];
    SEL sel = anInvocation.selector;
    // 判断备用对象是否可以响应传递进来等待响应的SEL
    if ([dog respondsToSelector:sel]) {
        [anInvocation invokeWithTarget:dog];
    } else {
        // 如果备用对象不能响应 则抛出异常
        [self doesNotRecognizeSelector:sel];
    }
}

JSPatch 的工作原理

JSContext *context = [[JSContext alloc] init];
context[@"_methodFunc"] = ^(id obj, NSString *clsName, NSString *methodName, NSArray *args, BOOL isSuper) {
        id temp_obj;
        if (obj) {
            if (args.count == 0) {
                temp_obj = ((id (*)(id, SEL))(void *)objc_msgSend)(obj, NSSelectorFromString(methodName));
            } else if (args.count == 1) {
                temp_obj = ((id (*)(id, SEL, id))(void *)objc_msgSend)(obj, NSSelectorFromString(methodName), args[0]);
            }
        } else {
            temp_obj = ((id (*)(id, SEL))(void *)objc_msgSend)(NSClassFromString(clsName), NSSelectorFromString(methodName));
        }
        
        return [JSValue valueWithObject:@{@"__clsName": clsName, @"__obj": temp_obj ?: obj} inContext:weakContext];
    };
NSString *path = [[NSBundle bundleForClass:[self class]] pathForResource:@"JSPatch" ofType:@"js"];
    NSString *jsCore = [[NSString alloc] initWithData:[[NSFileManager defaultManager] contentsAtPath:path] encoding:NSUTF8StringEncoding];
    
    if ([context respondsToSelector:@selector(evaluateScript:withSourceURL:)]) {
        [context evaluateScript:jsCore withSourceURL:[NSURL URLWithString:@"JSPatch.js"]];
    } else {
        [context evaluateScript:jsCore];
    }

· 要点
1.保存js上下文的JSContext,根据JS的调用,执行相应的OC代码
2.JS存储OC对象及运行结果,执行相关逻辑和代码
3.方法替换和交互,实现原生开发的动态性

OC和JS类型的对应关系
Objective-C type  |   JavaScript type
 --------------------+---------------------
         nil         |     undefined
        NSNull       |        null
       NSString      |       string
       NSNumber      |   number, boolean
     NSDictionary    |   Object object
       NSArray       |    Array object
        NSDate       |     Date object
       NSBlock (1)   |   Function object (1)
          id (2)     |   Wrapper object (2)
        Class (3)    | Constructor object (3)

参考资料

相关文章

网友评论

      本文标题:iOS消息转发及其应用

      本文链接:https://www.haomeiwen.com/subject/nruxqrtx.html