iOS底层原理探究05-类的底层原理isa链&继承链&类的内存结构

isa指向分析

通过《iOS底层原理探究04-OC对象的本质&联合体位域&isa分析》我们对isa已经有了一定的了解,现在我们来研究下isa具体的指向情况。

对象的isa指向

对象的isa指向类这个我们都知道下面就来验证一下
这里需要知道nopointerisa的存在,普通的isa是直接指向类的,但是得添加OBJC_DISABLE_NONPOINTER_ISA = YES参数能会使用普通的isa否则默认会使用nopointerisa

image.png

当我们不使用nopointerisaisa是直接指向类的
image.png

  • 对象中第一个变量就是isa0x0000000100008700
  • nopointerisaisa指向类 po 0x0000000100008700打印LGPerson
    image.png

    首先x/4gx查看p对象的内存,首地址0x011d800100008365就是pisa指针,nopointerisa需要&上ISA_MASK才是isa指向的类的地址(这些在上一篇里都有介绍),po输出了LGPerson,后面又通过p/x验证了一下LGPerson.classisa & ISA_MASK的地址,结果是一样的,这样就充分验证了对象pisa指向它的类LGPerson

类的isa指向

现在我们验证了对象的isa是指向类的,那么类的isa又指向什么呢,我们来继续探究

image.png

  • x/4gx查看类的内存情况得到类的isa地址(即内存的首地址) 0x0000000100008338
  • po输出类的isa & ISA_MASK输出了LGPerson,这个LGPerson还是当前类的吗?
  • p/x 0x0000000100008338 & 0x00007ffffffffff8通过p/x isa & ISA_MASK输出地址,发现是0x0000000100008338和之前的$12 = 0x0000000100008360 LGPerson并不是通一个地址,因此这两个LGPerson并不是相同的。

通过上面的一通操作我们得到了这样一个结果0x00000001000083600x0000000100008338 都输出 LGPerson,这里我们大胆猜想一下 ,是不是类和对象一样在内存里也会存在很多个,接下来我们验证一下这个猜想

image.png

我们使用各种方法得到的类打印出来的地址都是同一个,说明我们的猜想不正确。
用烂苹果(MachOView)看一下吧
image.png

把Products文件夹下编译生成的可执行文件(002-isa分析)放到烂苹果里分析下
image.png

到符号表(Symbol Table)里查看

  • 0000000100008360_OBJC_ClASS(类)类型的LGPerson
  • 0000000100008338_OBJC_METACLASS(元类)类型的LGPerson
    这下就清晰了 一个是类,一个是元类,类的isa指向元类,这个元类我们代码里并没有创建,是系统编译的时候自动生成的,也就是说 对象 isa -> 类 isa -> 元类

此时我会类的isa指向元类,那元类的isa指向哪里呢?接下来继续探究一下

image.png

输出元类的isa & ISA_MASK发现指向的是根元类NSObject,进一步输出跟元类的isa & ISA_MASK是指向自己的,这个我们输出NSObject类的地址发现和根元类NSObject的地址并不相同。至此我们可以得到这样一个结论
isa走位图

一般的类isa的走位图是这样的,那么根类NSObject的isa的走位图又是什么样的呢
image.png

可以看到NSObject还是有点区别的比普通的类少了一层
NSObject的isa走位图

结合到一起就得到了完整的isa走位图
完整的isa走位图

接下来看下继承链
继承链

运行结果说明

  • LGPerson元类的父类是NSObject元类
  • LGTeacher(LGPerson的子类)元类的父类是LGPerson元类
  • 根类NSObject没有父类
  • 根元类NSObject的父类是NSObject类
    至此继承链也已经很清晰了


    继承链

    结合isa走位图和继承链图得到苹果官网上的这张图


    isa流程图.png

    类的isa指向和继承链已经很清晰了。关于isa的走位和继承链就探索到这里,接下来开始探究类的内存结构。

类的内存结构

在《iOS底层原理探究04-OC对象的本质&联合体位域&isa分析》里面我们已经知道了OC的类Class在底层是struct objc_class *类型,接下来就来看看objc_class的结构

struct objc_class : objc_object {
    ...省略无关代码
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    
    ...省略无关代码
}

因为类objc_class有好几百行代码,我们省略掉无关紧要的代码和方法代码(结构体的方法不在接口对象里存而是在方法区保存),直接研究他的成员变量,可以得到objc_class包含4个成员变量

  • ISA(从objc_object继承过来的)
  • superclass父类
  • cache 缓存
  • bits 类的数据
    第一个isa我们在前面的内容其实已经验证过了
    看下第二个成员变量是不是superclass
    image.png

    下面我们使用LGPerson类来做示例,看下属性、方法、成员变量在类里是怎么存的。
    image.png

    po第二个成员变量确实打印了父类NSObject
    cache我们先不看(下一篇重点介绍)先看bits,但是要想看bits就先得知道cache的大小,然后通过类的首地址+偏移量(ISA + superclass + cache)来找到bits的地址进而查看bits的内存结构。ISAsuperclass都是Class类型8字节 但是cache是个结构体它的大小要根据成员变量确定,所以要先简单看下它的结构确定大小。
struct cache_t {
private:
    explicit_atomic _bucketsAndMaybeMask;
    union {
        struct {
            explicit_atomic    _maybeMask;
#if __LP64__
            uint16_t                   _flags;
#endif
            uint16_t                   _occupied;
        };
        explicit_atomic _originalPreoptCache;
    };
...省略静态变量
...省略无关方法代码
}

因为静态变量是存在静态存储区的,方法存在方法区都不影响cache_t结构体的大小所以省略掉这些代码,下面就很清晰了就剩一个explicit_atomic _bucketsAndMaybeMask和联合体。

  • typedef unsigned long uintptr_t;可知_bucketsAndMaybeMask是8字节
  • 联合体中_maybeMask(mask_t) + _flags(uint16_t) + _occupied(uint16_t),mask_tuint32_t类型4字节 uint16_t是2字节 ,就是 4 + 2 + 2 = 8字节,联合体中另外一个成员_originalPreoptCache是指针类型也是8字节,所以联合体整体占用8字节(关于联合体看这里)
  • cache_t就是8 + 8 = 16字节

那么拿到class的首地址 + 32就是bits的位置。

image.png

按我们之前的思路确实能获取到class_data_bits_t *类型的bits,但是怎么看这个数据结构呢。

struct class_data_bits_t {
...省略无关代码
public:

    class_rw_t* data() const {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
...省略无关代码
}

通过源码可以找到class_data_bits_t中存在data ()取数据的方法我们来试一下。

image.png

通过data()方法确实取到了class_rw_t,打印得到了class_rw_t的数据结构,再来看下class_rw_t的源码。

struct class_rw_t {
    ...省略无关代码
    const method_array_t methods() const {
        auto v = get_ro_or_rwe();
        if (v.is()) {
            return v.get(&ro_or_rw_ext)->methods;
        } else {
            return method_array_t{v.get(&ro_or_rw_ext)->baseMethods()};
        }
    }

    const property_array_t properties() const {
        auto v = get_ro_or_rwe();
        if (v.is()) {
            return v.get(&ro_or_rw_ext)->properties;
        } else {
            return property_array_t{v.get(&ro_or_rw_ext)->baseProperties};
        }
    }

    const protocol_array_t protocols() const {
        auto v = get_ro_or_rwe();
        if (v.is()) {
            return v.get(&ro_or_rw_ext)->protocols;
        } else {
            return protocol_array_t{v.get(&ro_or_rw_ext)->baseProtocols};
        }
    }
};
  • methods() 取方法列表方法
  • properties() 取属性列表方法
  • protocols() 取协议列表方法
    打印属性列表

    使用properties()方法获取到了property_array_t通过控制台打印的结构看到property_array_t里有个list结构,list里包含了ptr,深入进去得到了property_list_t类型的一个列表,使用get()方法查看list的内容
    image.png

    至此我们在类内存结构里找到了类的两个属性namehobby
    获取类里的方法
(lldb) x/4gx LGPerson.class
0x100008380: 0x00000001000083a8 0x000000010036c140
0x100008390: 0x0000000100364360 0x0000802800000000
(lldb) p/x 0x100008380+0x20
(long) $1 = 0x00000001000083a0
(lldb) p (class_data_bits_t *)0x00000001000083a0
(class_data_bits_t *) $2 = 0x00000001000083a0
(lldb) p $2->data()
(class_rw_t *) $3 = 0x0000000101349aa0
(lldb) p *$3
(class_rw_t) $4 = {
  flags = 2148007936
  witness = 0
  ro_or_rw_ext = {
    std::__1::atomic = {
      Value = 4295000344
    }
  }
  firstSubclass = nil
  nextSiblingClass = NSUUID
}
(lldb) p $4.methods()
(const method_array_t) $5 = {
  list_array_tt = {
     = {
      list = {
        ptr = 0x0000000100008160
      }
      arrayAndFlag = 4295000416
    }
  }
}
(lldb) p $5.list
(const method_list_t_authed_ptr) $6 = {
  ptr = 0x0000000100008160
}
(lldb) p $6.ptr
(method_list_t *const) $7 = 0x0000000100008160
(lldb) p *$7
(method_list_t) $8 = {
  entsize_list_tt = (entsizeAndFlags = 27, count = 6)
}
(lldb) p $8.get(0).big()
(method_t::big) $9 = {
  name = "sayNB"
  types = 0x0000000100003f77 "v16@0:8"
  imp = 0x0000000100003d40 (KCObjcBuild`-[LGPerson sayNB])
}
(lldb) p $8.get(1).big()
(method_t::big) $10 = {
  name = "hobby"
  types = 0x0000000100003f6f "@16@0:8"
  imp = 0x0000000100003db0 (KCObjcBuild`-[LGPerson hobby])
}
(lldb) p $8.get(2).big()
(method_t::big) $11 = {
  name = "setHobby:"
  types = 0x0000000100003f8b "v24@0:8@16"
  imp = 0x0000000100003de0 (KCObjcBuild`-[LGPerson setHobby:])
}
(lldb) p $8.get(3).big()
(method_t::big) $12 = {
  name = "init"
  types = 0x0000000100003f6f "@16@0:8"
  imp = 0x0000000100003ce0 (KCObjcBuild`-[LGPerson init])
}
(lldb) p $8.get(4).big()
(method_t::big) $13 = {
  name = "name"
  types = 0x0000000100003f6f "@16@0:8"
  imp = 0x0000000100003d50 (KCObjcBuild`-[LGPerson name])
}
(lldb) p $8.get(5).big()
(method_t::big) $14 = {
  name = "setName:"
  types = 0x0000000100003f8b "v24@0:8@16"
  imp = 0x0000000100003d80 (KCObjcBuild`-[LGPerson setName:])
}
(lldb) 
  • 流程跟获取属性的大致一样
  • 获取列表的时候换成methods()方法
  • 遍历时候get()之后需要调用big()方法才能获取到元素

为什么需要调用big(),来看源码

image.png

method_t的结构里方法的信息存储在struct big结构体里,所以需要调用big()方法

总结

通过对类的结构的探究,了解到类的方法列表属性列表协议列表都储存在bits数据结构中,后面我们会继续对类的存储探究。

遗留问题

本篇留下了一个问题:类的实例方法是存在methods()数据接口中的但是类方法并没有存在这里,那类方法存在哪里呢?我再下一篇《iOS底层原理探究06-类的底层原理下》中为您解答

你可能感兴趣的:(iOS底层原理探究05-类的底层原理isa链&继承链&类的内存结构)