美文网首页iOS 底层原理 iOS 进阶之路
OC底层原理十: 类的深入理解

OC底层原理十: 类的深入理解

作者: markhetao | 来源:发表于2020-09-15 19:39 被阅读0次

    OC底层原理 学习大纲

    上一节我们分析了类的结构成员变量属性实例方法类方法的读取。

    今天我深入拓展一下:

      1. 怎么知道成员变量、属性存放位置的?
      1. v24@0:8@16这种符号啥意思?(熟悉编码表)
      1. 类的归属

    1. 怎么知道成员变量、属性存放位置的?

    当我们只知道objc_class结构的时候,无法确定某类属性或方法存放在哪里时:

    • 可以给一个特殊的名称
    • 通过clang将文件静态编译成cpp文件
    • 搜索特殊名称。查看系统存放位置。

    例如: 我们不知道成员变量存放在哪。

    • 定义一个hobby成员变量
    @interface HTPerson : NSObject {
       NSString * hobby;
    }
    
    • main.m中实例化HTPerson对象

    • clang -rewrite-objc main.m -o main.cpp 编译成cpp静态文件。

    • 打开main.cpp文件,搜索hobby

      image.png
      image.png
      image.png
    • 因为是特殊名称(系统中基本不会出现的名称),所以一下就可以搜索出来。

    • 上面就是这个名称出现的所有位置。 我们对比objc_classdata()数据格式来看:

      image.png

    发现class_ro_t类型的只有ro()函数。

    • 进入class_ro_t查看格式。
      image.png

    顺利找到ivar_list_t。定位了成员变量位置。
    实践过程可查看上一节

    这里介绍的是通用定位方法。 不局限于成员变量

    2. v24@0:8@16这种符号啥意思?

    我们查看cpp文件时,发现函数都有v24@0:8@16这样的符号。

    image.png
    SEL 和 IMP

    每个方法都有SELIMP

    • SEL: 方法编号
    • IMP: 函数指针地址
    • SEL和IMP: 组成键值对

    我们调用函数,是调用OC上层封装好的函数。 通过SEL方法编号 -> 找到对应的IMP指针地址 -> 返回指针指向底层实现

    • 打开Xcode开发者文档
      开发者文档.png

    搜索ivar_getTypeEncoding

    image.png

    点击进入官方文档,查看所有类型编码 👉 快捷通道

    image.png

    这些就是各类型简写。 以后看到这些就清晰明朗了。😃

    v24@0:8@16 :
    v: void , @: 一个对象 ,:SEL 函数选择器

    描述:定义一个返回值是void,总内存大小为24字节的函数,第一个入参是对象,从0字节开始,第二个入参SEL,从8字节开始,第三个入参是对象,从16字节开始

    对比下面实际函数看就一目了然了:

    • static void I_HTPerson_setName(HTPerson *self, SEL _cmd, NSString *name)
      第一个入参HTPerson实例对象, 第二个入参SEL方法编号, 第三个入参name字符串

    类似属性值也可以参看相关文档了解
    {"name","T@\"NSString\",&,N,V_name"
    文档中搜索property_getAttributes, 找到👉 官方描述

    3. 类的归属

    上一节在结束时,我们验证了

    • 对象方法存在中,的方法存在元类

    面试题一: 类的归属

    准备测试数据

    @interface HTPerson : NSObject
    - (void)sayHello;
    + (void)sayHappy;
    @end
    
    @implementation HTPerson
    
    - (void)sayHello{
        NSLog(@"HTPerson say : Hello!!!");
    }
    
    + (void)sayHappy{
        NSLog(@"HTPerson say : Happy!!!");
    }
    
    @end
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
    
            HTPerson *person = [HTPerson alloc];
            Class pClass     = object_getClass(person);
    
            // 这里加测试函数
            // Objc_copyMethodList(pClass);                 // 问题1
            // InstanceMethod_classToMetaclass(pClass);     // 问题2
            // ClassMethod_classToMetaclass(pClass);        // 问题3
            // IMP_classToMetaclass(pClass);                // 问题4
        }
        return 0;
    }
    
    - 问题1:Objc_copyMethodList打印的函数有哪些?
    void Objc_copyMethodList(Class pClass){
        unsigned int count = 0;
        Method *methods = class_copyMethodList(pClass, &count);
        for (unsigned int i=0; i < count; i++) {
            Method const method = methods[i];
            //获取方法名
            NSString *key = NSStringFromSelector(method_getName(method));
            
            NSLog(@"Method, name: %@", key);
        }
        free(methods);
    }
    
    • 这里需要弄清楚class_copyMethodList的原理。

    月月这位优秀童鞋那学来的学习方法,分享下:

    • 不懂的先查官方文档,清楚功能。再去源码究其根源。

    官方解释:
    打开帮助文档:


    官方文档

    搜索 class_copyMethodList

    class_copyMethodList

    大致意思是说:

    • 如果这个类有实现了的实例函数,就返回一个包含所有实例函数的数组。最后你必须free释放这个数组。
    • 如果当前类没有实例函数,或者当前类为,返回Null.并且outCount为0

    进入objc4源码中,进入class_copyMethodList方法。

    image.png

    我们可以看到完整的流程。 从HTPerson类中读取data()methods()函数。并将其另辟空间result存储后,返回result

    • 这个result就是类的Methods函数数组。学完上一节我们知道,类的Methods中存储的是所有实例方法类方法是存储在元类中的。

    • 看完源码,你应该要知道代码最后为何要加上free了吧。 因为result是新开辟的空间,需要手动释放。

    • HTPerson只有sayHello一个实例方法。所以class_copyMethodList打印的只有是sayHello

    • image.png
    - 问题2:InstanceMethod_classToMetaclass中哪些函数不是0x0?
    void InstanceMethod_classToMetaclass(Class pClass){
        
        const char *className = class_getName(pClass);
        Class metaClass = objc_getMetaClass(className);
        
        Method method1 = class_getInstanceMethod(pClass, @selector(sayHello));
        Method method2 = class_getInstanceMethod(metaClass, @selector(sayHello));
    
        Method method3 = class_getInstanceMethod(pClass, @selector(sayHappy));
        Method method4 = class_getInstanceMethod(metaClass, @selector(sayHappy));
        
        NSLog(@"%s:", __func__);
        NSLog(@"%p-%p-%p-%p",method1,method2,method3,method4);
        
    }
    
    • 这个需要弄清楚class_getInstanceMethod的原理

    官方解释:

    image.png

    大致意思是说:

    • 如果传入的或它的父类不包含这个实例方法,就返回Null

    源码分析:

    image.png

    进入lookUpImpOrForward。 寻找imp

    image.png
    • 进入_class_getMethod
    _class_getMethod
    • 进入getMethod_nolock
    image.png

    通过熟悉源码和文档,我们可以知道:

    • lookUpImpOrForward,就是使用一切办法(本类、父类、消息转发等)将Imp找到。
    • class_getInstanceMethod, 因为上面lookUpImpOrForward已经将imp找到了。所以class_getInstanceMethod就只需要走类继承(superclass)这条线来常规读取imp了。先在自己本类找,找不到再到superclass中寻找。如果运行到根类, 还找不到就条件终止(根类父类nil),返回nil

    通过上一节学习,我们知道,实例方法存在中,而类方法是存储在元类中的。

    • 这里sayHello是实例方法,sayHappy是类方法。 所以打印结果中,第一个第四个有值,第二个第三个

    • image.png
    - 问题3:ClassMethod_classToMetaclass中哪些函数不是0x0?
    void ClassMethod_classToMetaclass(Class pClass){
        
        const char *className = class_getName(pClass);
        Class metaClass = objc_getMetaClass(className);
        
        Method method1 = class_getClassMethod(pClass, @selector(sayHello));
        Method method2 = class_getClassMethod(metaClass, @selector(sayHello));
    
        Method method3 = class_getClassMethod(pClass, @selector(sayHappy));
        Method method4 = class_getClassMethod(metaClass, @selector(sayHappy));
        
        NSLog(@"%s", __func__);
        NSLog(@"%p-%p-%p-%p",method1,method2,method3,method4);
    }
    
    • 这个需要弄清楚class_getClassMethod的原理

    官方解释:

    image.png

    大致意思是说:

    • 如果传入的或它的父类不包含这个实例方法,就返回Null

    源码分析:

    image.png

    发现这个函数跟问题2进入的一样,都是class_getInstanceMethod

    • 不同的是这里入参是getMeta, 我们进入查看:
    image.png

    发现个有意思的事情。 我们传入cls

    • 代码判断isMetaClass是否为元类,如果是元类就返回cls本类,如果不是, 我就返回本类的isa

    用大白话解释:
    兄弟,类方法都放在元类里。如果你给我的是元类,我可以直接操作。 如果不是元类,我就免费帮你转成元类(类的isa指向元类),再去操作。总之,我这只办理元类的业务。

    • CC++的层面不存在类方法对象方法,一切方法的处理,在它确定对象(类还是元类)之后,都交给class_getInstanceMethod来处理。 所以后续方法就跟问题2的处理一样了。

    因为这题是获取类方法。 题中类方法只有sayHappy,所以第一个第二个都是不存在。 类方法是存在元类中,所以第四个存在的。

    • 第三个呢? 第三个上面说了,class_getClassMethod只处理类方法,如果不是元类,就帮你转成元类
    • 所以第三个实际上是转成了元类,再调用元类中储存的类方法

    所以打印结果如下:

    image.png
    - 问题4:IMP_classToMetaclass中哪些函数不是0x0?
    void class_getMethodImplementation(Class pClass){
        
        const char *className = class_getName(pClass);
        Class metaClass = objc_getMetaClass(className);
    
        IMP imp1 = class_getMethodImplementation(pClass, @selector(sayHello));
        IMP imp2 = class_getMethodImplementation(metaClass, @selector(sayHello));
    
        IMP imp3 = class_getMethodImplementation(pClass, @selector(sayHappy));
        IMP imp4 = class_getMethodImplementation(metaClass, @selector(sayHappy));
    
        NSLog(@"%s:",__func__);
        NSLog(@"%p-%p-%p-%p",imp1,imp2,imp3,imp4);
    }
    
    • 这个需要弄清楚class_getMethodImplementation的原理

    官方解释:

    image.png

    大致意思是说:

    • 如果传入的或它的父类都不包含这个实例方法,就使用运行时的消息转发机制。

    源码分析:

    image.png

    我们发现,如果我们找不到方法对应的imp,就使用运行时的消息转发。让能处理这个消息的对象接收处理。

    我们知道:

    • 第一个实例方法第四个类方法,本身是可找到对应的imp的,所以是存在的。

    • 第二个第三个,我们会调用消息转发机制。

    我们打印查看,最终都是存在的。


    image.png

    总结

    • class_getInstanceMethod: 获取实例方法
      自身类->父类->...->根类->nil这一条线,只要能找到对应实例方法,就返回方法imp;否则,返回null

    • class_getClassMethod: 获取类方法
      如果传入的不是元类,就转成元类再调用class_getInstanceMethod方法。 总之,类方法只存在元类中

    • class_getMethodImplementation: 获取方法的实现
      如果未找到,就进行消息转发

    面试题二:isKindOfClass 与 isMemberOfClass

    打印小技巧:

    #ifdef DEBUG
    #define HTLog(format, ...) printf("%s\n", [[NSString stringWithFormat:format, ## >__VA_ARGS__] UTF8String]);
    #else
    #define HTLog(format, ...);
    #endif
    
    • 面试题:
    @interface HTPerson : NSObject
    @end
    
    @implementation HTPerson
    @end
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            
            BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
            BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
            BOOL re3 = [(id)[HTPerson class] isKindOfClass:[HTPerson class]];
            BOOL re4 = [(id)[HTPerson class] isMemberOfClass:[HTPerson class]];
            HTLog(@" re1 :%hhd        re2 :%hhd        re3 :%hhd        re4 :%hhd",re1,re2,re3,re4);
    
            BOOL re5 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]];
            BOOL re6 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]];
            BOOL re7 = [(id)[HTPerson alloc] isKindOfClass:[HTPerson class]];
            BOOL re8 = [(id)[HTPerson alloc] isMemberOfClass:[HTPerson class]];
            HTLog(@" re5 :%hhd        re6 :%hhd        re7 :%hhd        re8 :%hhd",re5,re6,re7,re8);
        }
        return 0;
    }
    

    官方文档

    image.png image.png

    源码分析

    image.png

    isKindOfClass

    • 类方法: 类的Isa指向的是元类,沿着父类(superclass)这条线寻找,检查是否存在和入参cls相等的
    • 实例方法: 对象的Isa指向的是本类,沿着父类(superclass)这条线寻找,检查是否存在和入参cls相等的

    isMemberOfClass

    • 类方法: 判断Isa指向的元类,是否与入参cls相等。
    • 实例方法: 判断Isa指向的本类是否与入参cls相等。

    💣 💥

    当我们以为一切仅在掌控之中时,却发现isKindOfClass类方法实例方法压根没调用😭

    加入断点

    image.png

    打开汇编模式

    image.png image.png
    • 为什么要调用objc_opt_class呢?我们在objc源码层找不到调用依据,打开llvm搜索objc_opt_isKindOfClass
    image.png

    我们看到了熟悉的objc_alloc。你是否记得NSObject的alloc方法也没走objc4源码的alloc类方法? 而是llvm在编译层就将其处理好了。

    • 在这个表中,objc_opt_isKindOfClass静静地跟着objc_alloc一起躺着,我们看注释,可以知道苹果官方设计的原因,因为这些函数极少被改变,所以为了加速性能,苹果在llvm编译层就已经将其优化处理。 比如isKindOfClass如果没有被外部重写,在被调用时都是直接消息转发执行objc_opt_isKindOfClass

    • objc4源码中查看objc_opt_isKindOfClass内部实现:

    image.png

    发现内部实现就跟isKindOfClass的方法一样。

    特点

    1. 类方法的初始值是元类实例方法的初始值是本类

    2. objc_opt_isKindOfClass: 是底层实现,有遍历操作

    • 类方法实例方法在底层统称为方法。(入参是实例对象,初始值为本类; 入参是,初始值是元类)
    1. isMemberOfClass: 类方法实例方法无遍历操作,直接比较

    在开始之前,我们请上经典isa指向superclass继承图:

    • isa指向和superclass继承

    掌握秘诀,答题开始:

    • re1:
    BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]];
    
    • isKindOfClass类方法,调用objc_opt_isKindOfClass,初始值为元类,拥有遍历操作
    1. 获取[NSObject class]isa指向的类: NSObject元类
    2. 判断NSObject元类NSObject类,不相等。
    3. 继续寻找NSObject元类superclass: NSObject类
    4. [NSObject class]相等。 返回true

    答案: True

    • re2:
    BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]];
    
    • isMemberOfClass类方法,初始值为元类,无遍历操作,直接比较
    1. 获取[NSObject class]isa指向的类: NSObject元类
    2. 判断NSObject元类NSObject类,不相等。 返回false

    答案: False

    • re3:
    BOOL re3 = [(id)[HTPerson class] isKindOfClass:[HTPerson class]];
    

    -isKindOfClass类方法,调用objc_opt_isKindOfClass,初始值为元类,拥有遍历操作

    1. 获取[HTPerson class]isa指向的类: HTPerson元类
    2. 判断HTPerson元类HTPerson类,不相等。
    3. 继续层层寻找HTPerson元类superclass
    4. 依次找到: NSObject元类NSObject类nil。都与HTPerson类不相等,返回false

    答案: False

    • re4:
    BOOL re4 = [(id)[HTPerson class] isMemberOfClass:[HTPerson class]];
    
    • isMemberOfClass类方法,初始值为元类,无遍历操作,直接比较
    1. 获取[HTPerson class]isa指向的类: HTPerson元类
    2. 判断HTPerson元类HTPerson类,不相等。返回false

    答案: False

    • re5:
    BOOL re5 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]];
    
    • isKindOfClass对象方法,调用objc_opt_isKindOfClass,初始值为本类,拥有遍历操作
    1. 获取[NSObject alloc]的本类: NSObject类
    2. 判断NSObject类[NSObject class]相等, 返回True

    答案: True

    • re6:
    BOOL re6 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]];
    
    • isMemberOfClass对象方法,初始值为本类,无遍历操作,直接比较
    1. 获取[NSObject alloc]的本类: NSObject类
    2. 判断NSObject类[NSObject class]相等, 返回True

    答案: True

    • re7:
    BOOL re7 = [(id)[HTPerson alloc] isKindOfClass:[HTPerson class]];
    
    • isKindOfClass对象方法,调用objc_opt_isKindOfClass,初始值为本类,有遍历操作
    1. 获取[HTPerson alloc]的本类: HTPerson类
    2. 判断HTPerson类[HTPerson class]相等, 返回True

    答案: True

    • re8:
    BOOL re8 = [(id)[HTPerson alloc] isMemberOfClass:[HTPerson class]];
    
    • isMemberOfClass对象方法,初始值为本类,无遍历操作,直接比较
    1. 获取[HTPerson alloc]的本类: HTPerson类
    2. 判断HTPerson类[HTPerson class]相等, 返回True

    答案: True

    附上打印结果

    答案

    是不是掌握了诀窍之后,瞬间无对手了?


    别飘? 咱们加设一题

    BOOL re9 = [(id)[HTPerson class] isKindOfClass:[NSObject class]];
    BOOL re10 = [(id)[HTPerson alloc] isKindOfClass:[NSObject class]];
    BOOL re11 = [(id)[HTPerson class] isMemberOfClass:[NSObject class]];
    BOOL re12 = [(id)[HTPerson alloc] isMemberOfClass:[NSObject class]];
    HTLog(@" re9 :%hhd        re10 :%hhd        re11 :%hhd        re12 :%hhd",re9,re10,re11,re12);
    

    记住我每个答案上第一行解题思路
    什么函数对象方法还是类方法? 初始值是元类还是本类? 是遍历superclass还是直接比较

    开始答题:

    • re9:
    BOOL re9 = [(id)[HTPerson class] isKindOfClass:[NSObject class]];
    
    • isKindOfClass类方法,调用objc_opt_isKindOfClass,初始值为元类,拥有遍历操作
    1. 获取[HTPerson class]isa指向的类: HTPerson元类
    2. 判断HTPerson元类NSObject类,不相等。
    3. 继续层层寻找HTPerson元类superclass
    4. 依次找到: NSObject元类NSObject类
    5. NSObject类相等,返回True

    答案: True

    • re10:
    BOOL re10 = [(id)[HTPerson alloc] isKindOfClass:[NSObject class]];
    
    • isKindOfClass对象方法,调用objc_opt_isKindOfClass,初始值为本类,有遍历操作
    1. 获取[HTPerson alloc]的本类: HTPerson类
    2. 判断HTPerson类[NSObject class]不相等。
    3. 继续寻找HTPerson类superclassNSObject类
    4. 此时NSObject类[NSObject class]``相等,返回True

    答案: True

    • re11:
    BOOL re11 = [(id)[HTPerson class] isMemberOfClass:[NSObject class]];
    
    • isMemberOfClass类方法,初始值为元类,无遍历操作,直接比较
    1. 获取[HTPerson class]isa指向的类: HTPerson元类
    2. 判断HTPerson元类NSObject类,不相等。返回False

    答案: False

    • re12:
    BOOL re12 = [(id)[HTPerson alloc] isMemberOfClass:[NSObject class]];
    
    • isMemberOfClass实例方法,初始值为本类,无遍历操作,直接比较
    1. 获取[HTPerson alloc]的本类: HTPerson类
    2. 判断HTPerson类NSObject类,不相等。返回False

    答案: False

    附上打印结果

    答案

    恭喜你,挑战成功!

    别吐槽,面试怎么虐得爽就怎么来 😂

    我自己写的时候也很凌乱,直到整理出的解题思路,才打通任督二脉

    什么函数对象方法还是类方法? 初始值是元类还是本类? 是遍历superclass还是直接比较

    相关文章

      网友评论

        本文标题:OC底层原理十: 类的深入理解

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