isa_t的分析
上源码, 先不赘述objc_object和objc_class的关系了,下图应该清楚的表达了.
下面我们来分析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
};
};
上述源码关于位域的部分就不在此解释了,此时我们注意到,上面有个
ISA_MASK的定义, 那么这个ISA_MASK到底是什么了? 用2进制标识如下,发现刚好前面17位和后面3位空出来了, 刚好是不是上述isa_t中shiftcls, 由此可知shiftcls中存的是isa的指针.
反之事实是否真的如此, 我们看源码, 定位到alloc流程initIsa, 发现shiftcls存的就是cls即isa
后面我们举例验证下:
发现我们第一个字节中的值 0x001d8001000023f9 按位与上 ISA_MASK 0x00007ffffffffff8 得到的就是MLPerson的地址
后面我们继续对MLPerson做同样的操作会发生什么呢?
会发现
MLPerson的 isa打印出来竟然也是MLPerson,那么这是为什么呢?引入下面一个概念元类
元类
首先我们先看下MLPerson会在内存中有几份,通过下面一个例子来说明
上述例子,发现无论怎么样操作,
MLPerson的地址都是同一个,那么在上一小节留下的问题,MLPerson的isa打印出来是MLPerson应该不是 class MLPerson. 在此也不废话,直接上答案, MLPerson的isa指向的就是MLPerson的元类. 但是我们从来都没有写过元类的代码,那么元类是怎么来的呢?
我们借助一个工具 MachOView,打开编译出来的可执行文件, 看看symbol table 是否能够得到一些线索
发现在符号列表里有个一个
_OBJC_METACLASS_$_MLPerson,这是系统帮我们自动生成的. 继续探索,那元类的isa指向谁呢?
我们发现 MLPerson的元类的isa指向的是NSObject的isa,我们称NSObject的元类称为根元类, 而根元类的isa指向根元类本身. 那么元类之间是否有继承关系呢?通过下面一个例子
得到下面这一副经典的isa走位图
类的结构
类的本质就是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中的内容呢?比如方法列表等等类中包含的数据。
通过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.
最终我们可以通过LLDB的方式获取这个类里面的内容,当前指示列了取methods的一种情况. 通过LLDB的这种方式能够更加理解类的结构.
探索
我们在探索的过程中发现methods里面并没有存储类方法,那么类方法是存在哪儿的,答案是元类里面,为什么存在元类里?对于所有的方法,在底层都是函数,如果方法和类方法都放在类里面,那怎么区分呢?所有就有了元类,元类存类方法,以此来区分类方法和实例方法, 这也是元类存在的理由.
方法存在类里面(避免浪费内存),类方法存在元类里面. 元类是在编译阶段,编译器生成的.
class_getClassMethod实现是调用class_getInstantMethod for class->metaClass
如果class本身就是一个元类,那么class->metaClass == class
这是为什么我们在调用
class_getClassMethod的时候如果传入的是元类指针,最后也会返回正确的类方法.
附录
上面有个小问题是firstSubclass = nil, 类的加载是懒加载的方式,所以当调用了LGTeacher.class之后firstSubclass就不为空了