美文网首页iOS底层原理深度体会iOSOC对象的本质
OC对象的本质(下)—— 详解isa&supercl

OC对象的本质(下)—— 详解isa&supercl

作者: RUNNING_NIUER | 来源:发表于2019-03-20 22:31 被阅读101次

    OC对象的本质(上):OC对象的底层实现原理
    OC对象的本质(中):OC对象的种类
    OC对象的本质(下):详解isa&superclass指针

    isa指针

    先总结一下我们在对象的分类一文里面分析过的问题,OC对象氛围三类

    instance对象,内部包含

    • 成员变量
    • 特殊的成员变量isa指针

    class对象,用来描述instance对象,内部包含

    • isa指针
    • superclass指针
    • 属性信息
    • 对象方法信息(-方法)
    • 协议信息
    • instance对象的成员变量的描述信息

    meta-class对象,用来存放类方法,内部包含

    • isa指针
    • superclass指针
    • 类方法信息(+方法)
    oc对象的分类以及内部结构

    对一个类来说,它的instanceclassmete-class对象之间,一定是有某种联系的。假设这种联系不存在,我们看看会碰到什么问题。比如我调用一个instance对象的方法

    [InstanceObj InstanceObjMethod];
    

    它的底层是

    objc_msgSend(instanceObj, @sel_registerName("instanceObjMethod"));
    

    也就是给instanceObj对象发消息。

    instance对象不跟外界关联的情况下,它内部只有一些成员变量信息,是不可能完成方法调用的,因为对象方法是存放在class对象里面的,对class对象调用+方法的时候也是一样,必须有办法跟meta-class对象关联起来,才能完成对+方法的调用。所以isa指针,就只它们之间的关联。可以通过下图来理解isa的作用。

    isa指针的作用

    大致可以归纳为

    • instance对象isa指针指向class对象。当调用对象方法(-方法)时,通过instanceisa找到class,然后在class的方法列表里面找到对应的实现进行调用。
    • class对象的isa指针指向meta-class对象。当调用类方法(+)方法时,通过classisa找到meta-class,最后在meta-class的方法列表找到对应的实现进行调用。

    那么通过isa的桥接作用,我梦应该能更近一步地理解OC消息发送以及方法调用的过程了。

    superclass指针

    显而易见,从字面意思,我们就能知道,superclass就是父类的意思。
    假定我们有以下几个类

    @interface Person : NSObject
    @end
    
    @interface Student : Person
    @end
    

    我们知道superclass指针存在于class对象meta-class对象里面。我们根据接下来的图示来阐述一下:

    class的superclass指针
    一个类的class对象里面的superclass指针指向该类的父类的class对象
    Studentinstance对象要调用Person的对象方法时,会先通过isa找到Studentclass对象,然后通过这个class对象superclass找到Person(Student的父类)class对象,最后找到相应的对象方法(-方法)的实现进行调用
    meta-class的superclass指针
    一个类的meta-class里面的superclass指针指向该类的父类的meta-class对象
    Studentclass对象要调用Person的类方法时,会先通过isa找到Studentmeta-class对象,然后通过这个meta-class对象superclass找到Person(Student的父类)meta-class对象,最后找到相应的类方法(+方法)的实现进行调用

    isa、superclass总结

    isa、superclass指针作用图例

    上图来自苹果官方,完整描述了isa、superclass指针的作用,为了更加便于理解,我们在后面的图例中用Student代替subclass,Person代替superclass,NSObject代替rootclass。

    • instanceisa指向class
    • classisa指向meta-class
    • meta-classisa指向基类的meta-class
    • classsuperclass指向父类的class如果没有父类,superclass指针为nil
    • meta-classsuperclass指向父类的meta-class,基类的meta-classsuperclass指向基类的class

    instance调用对象方法的轨迹
    我们以[student abc];为例,studentStudent类的实例对象,调用轨迹如下图


    对于student来说,并不知道abc方法在哪里,唯一知道的就是可以去它的class对象里面找,
    • 于是先通过isa指针进入Student类的class对象,如果在其中找到了abc就直接进行调用,调用过程结束,
    • 没找到的话,就通过class对象superclass指针进入Student类的父类,也就是Person类的class对象,重复上一步的查找逻辑
    • 以此类推,一层一层往上寻找,如果最终到了基类,也就是NSObject类的class对象里面,还没找到的话,由于它的superclassnil,最终就会碰到一个经典的报错[ERROR: unrecognized selector sent to instance],调用轨迹结束

    class调用类方法的轨迹
    我们以[Student abc];为例调用轨迹图如下


    对与Student类来说,abc在哪也是不知道的,我们知道类方法被规定放在meta-class对象里面,所以
    • 首先,通过Studentclass对象isa指针找到其meta-class对象,然后在方法列表里面寻找是否有abc,有的话就调用,调用逻辑结束。
    • 没有的话,就通过meta-class对象superclass指针找到Student的父类Personmeta-class对象,然后查找abc方法,找到就调用,结束调用轨迹
    • 没有的话,就通过Personmeta-class对象superclass指针,重复上一步的流程
    • 一次类推,通过meta-class对象superclass指针,一层层往上查找
    • 如果到了基类(NSObject)的meta-class还没能够找到abc,此时比较特殊,接下来的superclass指针会找到NSObject的class对象,你可能会奇怪,我们调用一个类方法,怎么跑到class对象里面来了,先保留你的疑问,只需记住,苹果确实是这么设计的,此时会继续在NSObject的class对象里面,寻找abc,如果真的找到了abc,就会调用
    • 如果还没有找到,由于此时的superclassnil,最终系统将给出报错

    面试题 isa指针指向哪里?

    根据我们上面的梳理和总结,我们可以得出结论

    isa(of instance) --> isa(of class) --> isa(of meta-class)
    下面我们通过代码来验证一下

    #import <Foundation/Foundation.h>
    #import <objc/runtime.h>
    
    @interface CLPerson : NSObject <NSCopying>
    
    @end
    
    @implementation CLPerson
    
    @end
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            CLPerson *person = [[CLPerson alloc] init];
            Class personClass = [CLPerson class];
            Class personMetaClass = object_getClass(personClass);
            NSLog(@"%p %p %p", person, personClass, personMetaClass);
        }
        return 0;
    }
    

    我们在代码中加入断点,通过控制台查看一下personisa信息。但是貌似系统只给出了有限信息


    还有个办法,可以右击红框中的isa,下拉菜单第一个有个打印功能Print Description of "xxx",可以得到更为详细的输出

    看起来结果仍然被系统包裹了一层
    如果你习惯直接在代码上快捷操作,也可以这么做试试

    但我还是喜欢用LLDB来查看,便于比较,和复制数据。

    通过p/x命令来打印指针,/后面是打印参数,x参数表示用16进制数输出。因为我们知道person这个instance的结构体的包含一个isa成员变量,person本身就是指针,所以可以通过person->isa访问isa的值。
    代码里面,personClassPerson类class对象,输出结果显示,
    person的isa = 0x001d8001000014d1
    personClass = 0x00000001000014d0
    它俩。。。并不相等!!!
    这是什么情况?不是说好了instance对象isa指向class对象嘛?

    其实在64位机器出现之前,instance对象isa确实是直接指向class对象的,
    也就是
    person->isa == personClass
    从64bit开始,isa需要进行一次为运算,才能计算出真实的class对象地址,系统给我们提供了一个ISA_MASK,这个可以在objc4源码里面找到。我先直接贴出来

    # if __arm64__
    #   define ISA_MASK        0x0000000ffffffff8ULL
    #   define ISA_MAGIC_MASK  0x000003f000000001ULL
    #   define ISA_MAGIC_VALUE 0x000001a000000001ULL
    #   define ISA_BITFIELD                                                      \
          uintptr_t nonpointer        : 1;                                       \
          uintptr_t has_assoc         : 1;                                       \
          uintptr_t has_cxx_dtor      : 1;                                       \
          uintptr_t shiftcls          : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
          uintptr_t magic             : 6;                                       \
          uintptr_t weakly_referenced : 1;                                       \
          uintptr_t deallocating      : 1;                                       \
          uintptr_t has_sidetable_rc  : 1;                                       \
          uintptr_t extra_rc          : 19
    #   define RC_ONE   (1ULL<<45)
    #   define RC_HALF  (1ULL<<18)
    
    # elif __x86_64__
    #   define ISA_MASK        0x00007ffffffffff8ULL
    #   define ISA_MAGIC_MASK  0x001f800000000001ULL
    #   define ISA_MAGIC_VALUE 0x001d800000000001ULL
    #   define ISA_BITFIELD                                                        \
          uintptr_t nonpointer        : 1;                                         \
          uintptr_t has_assoc         : 1;                                         \
          uintptr_t has_cxx_dtor      : 1;                                         \
          uintptr_t shiftcls          : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \
          uintptr_t magic             : 6;                                         \
          uintptr_t weakly_referenced : 1;                                         \
          uintptr_t deallocating      : 1;                                         \
          uintptr_t has_sidetable_rc  : 1;                                         \
          uintptr_t extra_rc          : 8
    #   define RC_ONE   (1ULL<<56)
    #   define RC_HALF  (1ULL<<7)
    
    # else
    #   error unknown architecture for packed isa
    # endif
    

    大家请看清这里是分了arm64和x86_64的,分别对应的是移动设备开发和mac开发。我的代码是一个mac命令行工程,所以我们用x86的这个值来试一下


    可以看到,结果就显而易见了。通过和ISA_MASK进行一次&运算,我们得到了personClass的地址。同样,我们来试一下personClass的isa指针。

    结果我试图通过personClass->isa先打印出其isa指针的时候,得到了错误提示,告诉我们说personClass的类型Class不是一个结构体,看不太明白,那就先查看一下Class的定义,typedef struct objc_class *Class;,然后在往下看一下objc_class的细节
    struct objc_class {
        Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
    
    #if !__OBJC2__
        Class _Nullable super_class                              OBJC2_UNAVAILABLE;
        const char * _Nonnull name                               OBJC2_UNAVAILABLE;
        long version                                             OBJC2_UNAVAILABLE;
        long info                                                OBJC2_UNAVAILABLE;
        long instance_size                                       OBJC2_UNAVAILABLE;
        struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
        struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
        struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
        struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
    #endif
    
    } OBJC2_UNAVAILABLE;
    

    虽然这个结构体里面有isa指针,但是尾部的OBJC2_UNAVAILABLE;提示我们,这已经是过时的API了。
    不过我们在第一篇文章中,已经得出结论,知道class对象里面第一个成员变量确实是一个isa指针,我们可以通过一个小技巧来处理这个问题

    struct cl_objc_class {
        Class isa;
    };
    
    int main(int argc, const char * argv[]) {
        @autoreleasepool {
            
            CLPerson *person = [[CLPerson alloc] init];
            Class personClass = [CLPerson class];
            struct cl_objc_class *personClass2 = (__bridge struct cl_objc_class *)(personClass);
            Class personMetaClass = object_getClass(personClass);
            NSLog(@"%p %p %p", person, personClass, personMetaClass);
        }
        return 0;
    }
    

    我们自定义一个struct,包含一个isa指针,然后再借助这个结构体类型来读取personClass里面的内容,如上代码,我们用personClass2在来尝试一次


    ok,结果显示,class对象isa指针经过ISA_MASK转换之后,得到了正确的mete-class对象的地址。到此,上面的面试题相信大家已经可以完整回答了。在用一个图来总结一下就是

    你会许还会问,那么superclass指针呢,是不是也需要一个什么mask转换?答案是不需要的,可以用上面相同的方法进行验证,这里不作赘述。总之isa指针稍微特殊一点点,特别记住一下关于ISA_MASK的细节就行。

    深度窥探class/meta-class的内部结构----struct objc_class

    在OC对象的本质(一)中,我们得知了一个事实,在class对象中,存放了一个类的方法列表、属性信息、协议信息、成员变量信息;在meta-class对象中,存放了类的类信息。但是还没有对其仔细验证过。下面我们就来研究一下这个问题。
    因为classmeta-class的类型都是struct objc_class*,所以我们问题的答案,就都在这个objc_class里面。上面的段落我们已经看了它的结构了,很可惜是一个已经废弃的API,所以我们必须去最新的源码里面,去看一下它的实现。在objc4源码里面,我们找到如下objc_class的实现

    struct objc_class : objc_object {
        // Class ISA;
        Class superclass;
        cache_t cache;             // formerly cache pointer and vtable
        class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    
        class_rw_t *data() { 
            return bits.data();
        }
    //下面是一大堆的方法
    ...
    ...
    ...
    }
    

    这里的objc_class是一个C++的结构体,如果对C++不太熟的话,先不用过分纠结,可以借用OC的类来理解就行了,它们的相似度很高,可以有成员变脸,也可以有方法,区别主要是一些成员变量的默认作用域不一样。可以看到objc_class继承自objc_object,我们可以在源码objc-private.h里看一下objc_object的实现

    struct objc_object {
    private:
        isa_t isa;
    //剩下的都是方法
    ...
    ...
    ...
    }
    

    看得出来,其实就是一个isa指针。于是和objc_class的内容融合一下,我们可以理解成下面的这个结构

    struct objc_class {
        isa_t isa;
        Class superclass;
        cache_t cache;             // formerly cache pointer and vtable
        class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    
        class_rw_t *data() { 
            return bits.data();
        }
    //下面是一大堆的方法
    ...
    ...
    ...
    }
    

    很明显,objc_class的内部,头两个成员分别是isa和superclass,跑不了。但是下面的好像不是我们期待的内容,没看到方法列表、属性协议信息啥的呀。但是我门可以看到这里的第一个方法,返回一个class_rw_t *,看字面意思,class代表类,rw通常代表读写(readwrite),t通常指的是 表信息(table),也就是类的可读写信息。那么我们有理由怀疑这里面肯定有宝贝。进去看一看


    果然,原来方法、属性、协议信息都放在了这里。同时,我们还发现了一个class_ro_t *ro,字面就是只读表,类对象里面有什么信息是只读的呢?没错,成员变量信息,于是我们在进去验证一下

    看上去推断是对的,确实找到成员变量信息。注意一下,这里的ivars是成员变量的描述信息,如名称,类型等,只需要一份的,所以存在class对象里面,成员变量的具体值是存在具体的instance对象里面的,不要理解混了。

    对于meta-class来说,结构上和class是一样的,只不过有些内容可能用不到,例如属性,协议列表。meta-class的类方法信息其实就放在我们刚才看到的那个方法列表里面,没错。class对象的对象方法信息也正是放在这个方法列表里的


    还有一点就是,怎么说呢,还是看图明白
    途中我们看出来,objc_class有个成员变量bits,正是通过 bits & FAST_DATA_MASK,将objc_class和它的可读写表关联起来了。下面我引用大神的一张ppt总结一下struct objc_class的结构

    面试题解答

    • 对象的isa指针指向哪里?
    1. instance对象的isa指针指向class对象
    2. class对象的isa指针指向meta-class对象
    3. meta-class对象的isa指针指向基类(也就是NSObject)的meta-class对象
    • OC的类信息存放在哪里?
    1. 对象方法,属性信息,成员变量信息,协议信息,存放在class对象中
    2. 类方法,存放在meta-class对象中
    3. 成员变量的具体值,存放在instance对象中



    OC对象的本质(上):OC对象的底层实现
    OC对象的本质(中):OC对象的分类
    OC对象的本质(下):详解isa&superclass指针

    特别备注

    本系列文章总结自MJ老师在腾讯课堂开设的OC底层原理课程,相关图片素材均取自课程中的课件。

    相关文章

      网友评论

        本文标题:OC对象的本质(下)—— 详解isa&supercl

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