美文网首页
课程笔记:第五章 Runtime相关面试问题

课程笔记:第五章 Runtime相关面试问题

作者: 飘摇的水草 | 来源:发表于2021-04-28 17:20 被阅读0次
    数据结构

    我们主要学习四种数据结构:

    • objc_object

    • objc_class

    • isa指针

    • method_t

      typedef struct objc_class *Class;
      typedef struct objc_object *id;

    1. objc_object

    我们平时所使用的对象都是id类型的,id类型的对象对应到 runtime 中实际上代表的就是 objc_object 的结构体,这个结构体主要包含以下几个部分

    其中 isa_t 是一个共用体。

    1. objc_class

    我们平时用到的 Class 就对应 objc_class 的数据结构,这也是一个结构体,objc_class 继承自 objc_object

    superClass 指向它的父类,cache 表示方法缓存,bits 里主要存放定义的属性等相关信息

    1. isa指针
    image.png image.png image.png image.png image.png image.png image.png

    数据类型总结


    image.png

    类对象与元类对象
    实例对象、类对象、元类对象
    类对象存储实例方法列表等信息
    元类对象存储类方法列表等信息


    image.png
    需要注意的地方:
    元类对象的isa指针,都是指向根元类对象。包括根元类对象本身,也是指向根元类对象
    根元类对象的superclass指针,指向的是根类对象

    问:元类对象的isa指针指向哪里?
    元类对象的isa指针,都是指向根元类对象。包括根元类对象本身,也是指向根元类对象

    问:如果一个类方法没有实现,但是有同名的对象方法实现,会崩溃?还是会调用?
    对象方法存储在类对象里面;
    类方法存储在元类对象里面;

    这个问题应该这样看:
    如果一个类方法没有实现,根据superclass指针,会去父类里面找同名类方法,直至到根元类对象里面去查找;
    如果中间找到了对应的同名类方法,则会调用。
    如果中间没有找到对应的同名类方法,则根元类对象的superclass指向的是根类对象
    如果根类对象里面有对应的同名实例方法(类对象(包括根类对象)只能存储实例方法),则会调用。
    如果根类对象里面没有对应的同名实例方法,则根据消息传递原则,进入动态方法解析阶段。

    举个例子:
    @interface YZPerson : NSObject

    • (void)run;
    • (void)run;
      @end

    import "YZPerson.h"

    @implementation YZPerson
    //+ (void)run
    //{
    // NSLog(@"类方法-run");
    //}

    • (void)run
      {
      NSLog(@"实例方法-run");
      }
      @end

    • (void)viewDidLoad {
      [super viewDidLoad];
      [YZPerson run];
      }
      结果:崩溃


      image.png

      也就是说,person里面虽然有同名的对象方法-(void)test;,但是,不好意思,superclass指针最多也就到达根类对象里面,到达不了类对象person里面,所以,还是找不到-(void)test;

    换句话说,如果类方法没有实现,只有在根类对象里面有同名实例方法,才能调用。

    根类对象一般指的是NSObject,如果需要在根类对象添加,也就是给NSObject添加自定义实例方法,这就需要分类了。分类可以给系统添加方法。

    @interface YZPerson : NSObject

    • (void)run;
    • (void)run;
      @end

    import "YZPerson.h"

    @implementation YZPerson
    //+ (void)run
    //{
    // NSLog(@"类方法-run");
    //}

    • (void)run
      {
      NSLog(@"实例方法-run");
      }
      @end

    NSObject+test.h文件
    @interface NSObject (test)

    • (void)run;
      @end

    NSObject+test.m文件

    import "NSObject+test.h"

    @implementation NSObject (test)

    • (void)run
      {
      NSLog(@"NSObject-实例方法-run");
      }
      @end
    • (void)viewDidLoad {
      [super viewDidLoad];
      [YZPerson run];
      }
      打印结果:
      NSObject-实例方法-run
      消息传递


      image.png

    消息传递
    void objc_msgSend(void /* id self, SEL op, ... */ )
    里面是两个默认参数,self和SEL。
    [self class]经过编译器转换为objc_msgSend(self, @selector(class));

    void objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
    里面是两个默认参数,super和SEL。
    [super class]经过编译器转换为objc_msgSendSuper(super, @selector(class));

    而,其中struct objc_super的数据结构是:
    struct objc_super {
    /// Specifies an instance of a class.
    __unsafe_unretained _Nonnull id receiver;

    if !defined(__cplusplus) && !OBJC2

    /* For compatibility with old objc-runtime.h header */
    __unsafe_unretained _Nonnull Class class;
    

    else

    __unsafe_unretained _Nonnull Class super_class;
    

    endif

    /* super_class is the first class to search */
    

    };

    里面有句话:super_class is the first class to search
    也就是,搜索方法,是从super_class开始的

    objc_msgSendSuper({self, super_class}, @selector(class));
    receiver是当前对象,也就是self,才是真正的消息接收者

    也就是,不论是[self class]还是[super class],消息接收者都是当前对象self。
    因此,上面的图片中,打印结果是:
    Phone
    Phone

    可以看出,在调用前,会先进行缓存操作。

    缓存操作有两种可能:
    1.缓存到receiver的cache_t的bucket_t中
    2.缓存到父类的cache_t的bucket_t中

    首先,肯定是参数cls的
    那么,参数cls是谁?
    注释倒是有一句:
    从上面代码可以看出,父类中缓存中有或者方法列表中有,则将方法缓存到原类中的cache_t的bucket_t中。

    正确流程图:


    image.png

    1.当前缓存中查找
    2.当前类中查找
    3.父类缓存中查找
    4.父类类中查找

    1.当前缓存中查找

    根据给定的值key,找到bucket_t里面的IMP


    image.png

    是一个hash查找

    2.当前类中查找

    对于已经排序好的列表,采用二分查找算法查找对应的执行函数
    对于没有排序好的列表,采用一般遍历查找方法对应的执行函数

    3.父类缓存中查找

    4.父类中查找

    image.png

    消息转发


    image.png

    动态添加方法

    动态添加方法
    BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)

    Method_Swizzing方法交换

    image.png

    void method_exchangeImplementations(Method m1, Method m2)

    更多学习请参考:
    iOS-探究Runtime

    +(BOOL)resolveInstanceMethod:(SEL)sel;//为对象方法进行决议
    +(BOOL)resolveClassMethod:(SEL)sel;//为类方法进行决议
    -(id)forwardingTargetForSelector:(SEL)aSelector;//方法转发目标
    -(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
    -(void)forwardInvocation:(NSInvocation *)anInvocation;
    那么最后消息未能处理的时候,还会调用到

    • (void)doesNotRecognizeSelector:(SEL)aSelector这个方法,我们也可以在这个方法中做处理,避免掉crash,但是只建议在线上环境的时候做处理,实际开发过程中还要把异常抛出来
    image.png

    动态添加方法

    动态添加方法
    BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)

    Method_Swizzing方法交换

    image.png

    void method_exchangeImplementations(Method m1, Method m2)

    更多学习请参考:
    iOS-探究Runtime
    动态方法解析
    @dynamic
    这句话写上,编译器将不再生成属性值的setter和getter方法,也不会生成成员变量,而在运行时我们用到的时候再给它动态添加get方法和set方法
    然后,我们可以在运行时+ (BOOL)resolveInstanceMethod:(SEL)sel方中,动态的实现setter和getter方法

    在MJ课程中,动态方法解析指的是runtime中的第二个流程+ (BOOL)resolveInstanceMethod:(SEL)sel

    在于海本节课程中,动态方法解析是一个具体例子@dynamic,动态实现setter和getter方法

    都是指的+ (BOOL)resolveInstanceMethod:(SEL)sel的方法调用

    动态运行时语言将函数决议推迟到运行时(指在运行时再给对象添加函数)
    编译时语言在编译期进行函数决议
    问:[obj foo]与objc_msgSend()函数之间有什么关系?
    [obj foo]编译后,就转换为了objc_msgSend()类型

    问:runtime如何通过selector找到对应的IMP的?
    其实就是objc_msgSend()的消息传递流程

    问:能否向编译后的类中增加实例变量?
    实例变量就是ivar
    (成员变量 = 实例变量 + 基本数据类型的变量)

    不能

    可以向动态添加的类中增加实例变量
    只需在注册之前加就可以

    相关文章

      网友评论

          本文标题:课程笔记:第五章 Runtime相关面试问题

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