4--类的原理分析之isa

391 阅读4分钟

isa_t的分析

上源码, 先不赘述objc_objectobjc_class的关系了,下图应该清楚的表达了. image.png

下面我们来分析isa_t, isa_t是一个联合体,即所有变量共用同一片内存空间

struct objc_object {
private:
    isa_t isa;
    }
#   define ISA_MASK        0x00007ffffffffff8ULL
union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;
    //位域
    struct {
      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
    };
};

image.png 上述源码关于位域的部分就不在此解释了,此时我们注意到,上面有个ISA_MASK的定义, 那么这个ISA_MASK到底是什么了? 用2进制标识如下,发现刚好前面17位和后面3位空出来了, 刚好是不是上述isa_tshiftcls, 由此可知shiftcls中存的是isa的指针.

image.png

反之事实是否真的如此, 我们看源码, 定位到alloc流程initIsa, 发现shiftcls存的就是clsisa

image.png

后面我们举例验证下:

image.png

发现我们第一个字节中的值 0x001d8001000023f9 按位与上 ISA_MASK 0x00007ffffffffff8 得到的就是MLPerson的地址

后面我们继续对MLPerson做同样的操作会发生什么呢?

image.png 会发现 MLPersonisa打印出来竟然也是MLPerson,那么这是为什么呢?引入下面一个概念元类

元类

首先我们先看下MLPerson会在内存中有几份,通过下面一个例子来说明 image.png 上述例子,发现无论怎么样操作,MLPerson的地址都是同一个,那么在上一小节留下的问题,MLPersonisa打印出来是MLPerson应该不是 class MLPerson. 在此也不废话,直接上答案, MLPersonisa指向的就是MLPerson的元类. 但是我们从来都没有写过元类的代码,那么元类是怎么来的呢?

我们借助一个工具 MachOView,打开编译出来的可执行文件image.png, 看看symbol table 是否能够得到一些线索

image.png 发现在符号列表里有个一个_OBJC_METACLASS_$_MLPerson,这是系统帮我们自动生成的. 继续探索,那元类的isa指向谁呢?

image.png

我们发现 MLPerson的元类的isa指向的是NSObjectisa,我们称NSObject的元类称为根元类, 而根元类的isa指向根元类本身. 那么元类之间是否有继承关系呢?通过下面一个例子

image.png

得到下面这一副经典的isa走位图

image.png

类的结构

类的本质就是objc_class

struct objc_object {
private:
    isa_t isa;//8 字节
    }
    
struct objc_class : objc_object {
    Class superclass;//8 字节
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits; 
    }

那么我们怎么获取objc_class中的内容呢?比如方法列表等等类中包含的数据。

image.png 通过x/4gx 打印出指针地址后面四个字节存储的内容,发现第二个8字节就是superclass,第一个8个字节是isa

struct cache_t {
private:
    explicit_atomic<uintptr_t> _bucketsAndMaybeMask;//8 字节
    union {
        struct {
            explicit_atomic<mask_t>    _maybeMask;//4 字节
#if __LP64__
            uint16_t                   _flags;//2 字节
#endif
            uint16_t                   _occupied;//2 字节
        };
        explicit_atomic<preopt_cache_t *> _originalPreoptCache;//8字节
    };//总体16字节

所以如果我们要取到class_data_bits_t bits; 的地址,首地址加32. 通过内存平移,我们最终得到class_rw_t中存的值,可以看到有methods,properties, protocols. image.png

image.png 最终我们可以通过LLDB的方式获取这个类里面的内容,当前指示列了取methods的一种情况. 通过LLDB的这种方式能够更加理解类的结构.

探索

我们在探索的过程中发现methods里面并没有存储类方法,那么类方法是存在哪儿的,答案是元类里面,为什么存在元类里?对于所有的方法,在底层都是函数,如果方法和类方法都放在类里面,那怎么区分呢?所有就有了元类,元类存类方法,以此来区分类方法和实例方法, 这也是元类存在的理由.

方法存在类里面(避免浪费内存),类方法存在元类里面. 元类是在编译阶段,编译器生成的.

class_getClassMethod实现是调用class_getInstantMethod for class->metaClass 如果class本身就是一个元类,那么class->metaClass == class image.png 这是为什么我们在调用class_getClassMethod的时候如果传入的是元类指针,最后也会返回正确的类方法.

附录

源码请参照

上面有个小问题是firstSubclass = nil, 类的加载是懒加载的方式,所以当调用了LGTeacher.class之后firstSubclass就不为空了

image.png