• Runtime的本质(三)objc_msgSend


    OC中的方法调用,其实都是转换为objc_msgSend函数的调用
    objc_msgSend的执行流程可以分为三大阶段:

    消息发送
    动态方法解析
    消息转发

    1.消息发送

    问:当空对象调用方法的时候,是怎么操作的?

    当调用方法的时候,执行的是objc_msgSend函数objc_msgSend(<#id _Nullable self#>, <#SEL _Nonnull op, ...#>)。
    第一个参数是消息接收者,第二个参数是方法名。
    我们在源码中找到objc_msgSend的具体实现,其是以汇编实现的。通过分析源码,可知,当消息接收者为空时,是执行类似以下这种操作:

    void objc_msgSend(id receiver, SEL selector)
    {
        if(receiver == nil) return;
        //1. 判断receiver是否为nil
    	
    	//2. 查找缓存
        
        //3. 执行后面的代码
    }
    

    也就是,当空对象调用方法的时候,直接return返回。

    消息发送阶段的流程大致为:

    在这里插入图片描述

    2. 动态方法解析

    当对象调用一个找不到的方法时,会进入动态方法解析阶段。

    动态方法解析阶段,首先会判断之前是否有过动态方法解析:
    如果之前已经进行了动态方法解析triedResolver = YES,则不执行动态方法解析;
    如果之前没有进行动态方法解析triedResolver = NO,则执行动态方法解析;
    在这里插入图片描述

    动态方法解析阶段会根据对象是否为 元类还是类,区分调用不同的方法:
    非元类对象(对象方法)调用:+(BOOL)resolveInstanceMethod:(SEL)sel
    元类对象(类方法)调用:+(BOOL)resolveClassMethod:(SEL)sel

    在这里插入图片描述

    举一个栗子:

    YZPerson.h
    #import <Foundation/Foundation.h>
    @interface YZPerson : NSObject
    - (void)run;
    @end
    
    YZPerson.m
    #import "YZPerson.h"
    @interface YZPerson()
    @end
    @implementation YZPerson
    + (BOOL)resolveInstanceMethod:(SEL)sel
    {
        NSLog(@"%s", __func__);
        return [super resolveInstanceMethod:sel];
    }
    @end
    
    YZPerson *person = [[YZPerson alloc] init];
    [person run];
    

    在这里插入图片描述

    说明,在找不到run方法的时候,会调用resolveInstanceMethod方法。

    上述例子,仅仅说明或者证明:在找不到run方法的时候,会调用resolveInstanceMethod方法。

    可以使用动态添加方法:

    在这里插入图片描述

    动态添加方法后,继续执行后面的goto retry,即继续执行消息发送
    由于已经动态添加方法,将方法添加到了当前类中的方法列表里面,这次就找的到method,然后正常执行了。

    动态方法解析流程:

    在这里插入图片描述

    动态方法解析最主要的作用与方式,就是通过动态添加一个方法,消息可以继续发送执行

    3. 消息转发

    当经过消息发送、动态方法解析过程后,仍没有找到方法名时,会进入第三个阶段:消息转发。即将自己处理不了的方法,转发给其他对象。

    在这里插入图片描述

    主要是调用__forwarding__方法,里面会调用下面两个方法:
    主要用到的方法是:

    • (id)forwardingTargetForSelector:(SEL)aSelector
    • (id)forwardingTargetForSelector:(SEL)aSelector
      其中,返回值是转发给其他对象。参数是处理不了的方法名。

    举个例子:

    YZCat.m文件
    
    #import "YZCat.h"
    
    @implementation YZCat
    - (void)run
    {
        NSLog(@"%s", __func__);
    }
    @end
    
    
    YZPerson.m文件
    #import "YZPerson.h"
    #import <objc/message.h>
    #import "YZCat.h"
    @interface YZPerson()
    @end
    @implementation YZPerson
    - (id)forwardingTargetForSelector:(SEL)aSelector
    {
        if (aSelector == @selector(run)) {
            return [[YZCat alloc] init];//返回可以处理的对象
    		//上句代码相当于执行了
    		//return objc_msgSend([[YZCat alloc] init], aSelector);
        }
        return [super forwardingTargetForSelector:aSelector];
    }
    @end
    
    运行结果:
    2020-05-23 12:16:49.554329+0800 runtime学习[8510:5105879] -[YZCat run]
    

    可以看到,当前两个阶段都没法处理的时候,第三个阶段依消息转发然可以处理。

    代码return [[YZCat alloc] init];当于执行了return objc_msgSend([[YZCat alloc] init], aSelector);

    如果

    • (id)forwardingTargetForSelector:(SEL)aSelector或者
    • (id)forwardingTargetForSelector:(SEL)aSelector
      两个方法也没有执行或者返回nil,系统会调用
    • (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector方法,返回一个方法签名。

    方法签名的目的是:得到返回值类型、参数类型

    如果没有实现签名方法,则会报方法找不到的错误
    在这里插入图片描述

    如果实现了方法签名,还需要调用- (void)forwardInvocation:(NSInvocation *)anInvocation函数。

    其目的是:将方法签名,封装到Invocation中去

    具体实现如下:

    在这里插入图片描述

    在forwardInvocation里面,可以随意做事情,你即使什么都不写,只有方法签名那步写好了,也不会崩溃

    消息转发流程:

    在这里插入图片描述

    除了对象方法可以做消息转发,类方法也可以做消息转发。

    + (id)forwardingTargetForSelector:(SEL)aSelector
    {
        if (aSelector == @selector(eat)) {
            return [YZCat class];
        }
        return [super forwardingTargetForSelector:aSelector];
    }
    
    结果:
    2020-05-24 11:02:53.707519+0800 runtime学习[11653:5343993] +[YZCat eat]
    

    需要注意的是:在消息转发的时候,即使原来调用的方法是个类方法,也可以转发给一个对象方法。

    [YZPerson eat];//类方法调用
    
    YZPerson.m文件
    + (id)forwardingTargetForSelector:(SEL)aSelector
    {
        if (aSelector == @selector(eat)) {
            return [[YZCat alloc] init];//转发给对象方法
        }
        return [super forwardingTargetForSelector:aSelector];
    }
    
    YZCat.m文件
    + (void)eat
    {
        NSLog(@"%s", __func__);
    }
    
    - (void)eat
    {
        NSLog(@"%s", __func__);
    }
    
    结果:
    2020-05-24 11:11:16.169383+0800 runtime学习[11852:5352885] -[YZCat eat]
    

    动态方法解析
    1、调用resolveInstanceMethod:方法。允许用户在此时为该Class动态添加实现。如果有实现了,则调用并返回YES,重新开始objc_msgSend流程。这次对象会响应这个选择器,一般是因为它已经调用过了class_addMethod。如果仍没有实现,继续下面的动作。

    消息转发
    2、调用forwardingTargetForSelector:方法,尝试找到一个能响应该消息的对象。如果获取到,则直接把消息转发给它,返回非nil对象。否则返回nil,继续下面的动作。注意这里不要返回self,否则会形成死循环。

    3、调用methodSignatureForSelector:方法,尝试获得一个方法签名。如果获取不到,则直接调用doesNotRecognizeSelector抛出异常。如果能获取,则返回非nil;传给一个NSInvocation并传给forwardInvocation:。

    4、调用forwardInvocation:方法,将第三步获取到的方法签名包装成Invocation传入,如何处理就在这里面了,并返回非nil。

    5、调用doesNotRecognizeSelector:,默认的实现是抛出异常。如果第三步没能获得一个方法签名,执行该步骤 。

    这句代码,系统帮我们做了什么事?
    @property (assign, nonatomic) int age;
    

    这句代码,为我们做了:set/get方法声明、set/get方法实现、生成成员变量

    1. set/get方法声明
    - (void)setAge:(int)age;
    - (int)age;
    
    1. set/get方法实现
    - (void)setAge:(int)age
    {
        _age = age;
    }
    
    - (int)age
    {
        return _age;
    }
    
    1. 生成成员变量
    {
    _age;
    }
    

    当我们使用了

    @property (assign, nonatomic) int age;//.h
    @dynamic age;//.m
    
    YZPerson *person = [[YZPerson alloc] init];
    person.age = 10;
    NSLog(@"%d", person.age);
    

    最后报reason: '-[YZPerson setAge:]: unrecognized selector sent to instance 0x100745230'错误。
    这是因为,使用@dynamic age;就不会自动生成age的setter/getter方法的实现,也不会自动生成成员变量(ivar)。
    需要注意的是,age的setter/getter方法的声明是不受影响的。

    所以,我们可以使用@dynamic age;来达到setter/getter方法的实现在运行过程中修改,例如:

    .h文件
    @property (assign, nonatomic) int age;
    
    .m文件
    @dynamic age;
    
    void setAge(id self, SEL _cmd, int age)
    {
        NSLog(@"age is %d", age);
    }
    
    int age(id self, SEL _cmd)
    {
        return 100;
    }
    
    + (BOOL)resolveInstanceMethod:(SEL)sel
    {
        NSLog(@"%s", __func__);
        if (sel == @selector(setAge:)) {
            class_addMethod(self, sel, (IMP)setAge,
                            "v@:i");
            return YES;
        }else if (sel == @selector(age)) {
            class_addMethod(self, sel, (IMP)age,
                            "i@:");
            return YES;
        }
        return [super resolveInstanceMethod:sel];
    }
    
    调用
    YZPerson *person = [[YZPerson alloc] init];
    person.age = 10;
    NSLog(@"%d", person.age);
    
    运行结果:
    2020-05-24 12:45:36.960569+0800 runtime学习[12661:5405426] +[YZPerson resolveInstanceMethod:]
    2020-05-24 12:45:36.961027+0800 runtime学习[12661:5405426] age is 10
    2020-05-24 12:45:36.961090+0800 runtime学习[12661:5405426] +[YZPerson resolveInstanceMethod:]
    2020-05-24 12:45:36.961163+0800 runtime学习[12661:5405426] 100
    
  • 相关阅读:
    spark 集合交集差集运算
    Scala学习笔记1(安装)
    shell脚本调用spark-sql
    R语言中判断是否是整数。以及读写excel
    el-table的type="selection"的使用
    el-mement表单校验-校验失败时自动聚焦到失败的input框
    "org.eclipse.wst.validation" has been removed 导入maven 项目出错。
    java compiler level does not match the version of the installed java project facet 解决方案
    Referenced file contains errors (http://www.springframework.org/schema/context). For more information, right click on the message in the Problems
    编译异常 Caused by: java.lang.UnsupportedClassVersionError:
  • 原文地址:https://www.cnblogs.com/r360/p/15891432.html
Copyright © 2020-2023  润新知