iOS消息转发及其应用

什么是方法

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)

参考资料

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 221,888评论 6 515
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,677评论 3 399
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 168,386评论 0 360
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,726评论 1 297
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,729评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,337评论 1 310
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,902评论 3 421
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,807评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,349评论 1 318
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,439评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,567评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 36,242评论 5 350
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,933评论 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,420评论 0 24
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,531评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,995评论 3 377
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,585评论 2 359

推荐阅读更多精彩内容