iOS 底层探索篇 —— isa的初始化&指向分析

1,935 阅读10分钟

前言

isa的引出

1. 从内存段分析

iOS 底层探索篇 —— 内存字节对齐分析这篇文章中,我们通过lldb调试的时候,第一个内存段我们并不是直接打印po出来的,而是po 0x00000001029570d0 & 0x0000000ffffffff8这样的操作,来打印出来的对象。

0x00000001029570d0这片内存段其实就是isa0x0000000ffffffff8这个值就是ISA_MASK掩码的值。

2. 从对象的本质分析

iOS 底层探索篇 —— 对象的本质这篇文章中,我们知道了对象的本质就是结构体,通过继承关系找到父类在底层就是objc_objcet的成员isa

isa的初始化

1. 初始化流程

iOS 底层探索篇 —— alloc、init、new的探索这篇文章中,我们知道会调用一个initIsa()的函数。

inline void 
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) 
{ 
    assert(!isTaggedPointer()); 
    
    if (!nonpointer) {
        isa.cls = cls;
    } else {
        assert(!DisableNonpointerIsa);
        assert(!cls->instancesRequireRawIsa());

        isa_t newisa(0);

#if SUPPORT_INDEXED_ISA
        assert(cls->classArrayIndex() > 0);
        newisa.bits = ISA_INDEX_MAGIC_VALUE;
        // isa.magic is part of ISA_MAGIC_VALUE
        // isa.nonpointer is part of ISA_MAGIC_VALUE
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else
        newisa.bits = ISA_MAGIC_VALUE;
        // isa.magic is part of ISA_MAGIC_VALUE
        // isa.nonpointer is part of ISA_MAGIC_VALUE
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.shiftcls = (uintptr_t)cls >> 3;
#endif
        ...省略无用代码...
        isa = newisa;
    }
}
  • nonpointer,就直接赋值cls
  • nonpointer就会做一些初始化的赋值。

2. isa数据类型分析

union isa_t {
    //两个默认的构造函数
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

  //isa指向的类
    Class cls;
    uintptr_t bits;
#if defined(ISA_BITFIELD)
    struct {
        //位域
        ISA_BITFIELD;  // defined in isa.h
    };
#endif
};
  • union联合体,一种数据类型,所占用8个字节。
  • 联合体的特性:内存共用,或者说带有互斥特性,意思就是赋值了cls,就不对其他成员赋值了。

分析ISA_BITFIELD

# 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

ISA_BITFIELD里面的字段field是相同的,但是在不同的架构平台下,每个field对应的位域会有不同。下面介绍每个field的意思

  • nonpointer表示是否对isa指针开启优化(我们现在的都是开启了优化),值为0:纯isa指针; 值为1:不止是类对象地址,isa中还包含了类信息,对象的引用计数等;
  • has_assoc是否有关联对象,值0无,值1有。
  • has_cxx_dtor是否有c++或者objc析构函数,如果有析构函数,先走析构逻辑,没有就更快的释放对象。
  • shiftcls存储类指针的值,开启指针优化的时候,在arm64架构中有33位存储类指针。
  • magic用来调试器判断当前对象是真的对象还是没有初始化的空间。
  • weakly_referenced标志对象是否指向或者曾经指向一个ARC的弱变量,没有弱引用的对象可以更快的释放。
  • deallocating标志对象是否正在释放。
  • has_sidetable_rc当对象的引用计数大于10的时候,会借用该变量存储进位。
  • extra_rc表示该对象的引用计数的值,实际上是引用计数的值减去1。 例如:该对象的引用计数为10,则该变量的值为9,若超过10,则需要用到has_sidetable_rc

isa的指向分析

1. 对象与类的关联

XDPerson *person = [XDPerson alloc];

1.1 通过控制台调试

简单介绍一下lldb命令

  • x/4gx objc打印objc的4段内存信息。扩展:x/6gx就是打印6段内存信息。
  • p/tp/t 打印二进制信息;p/o打印八进制信息;p/x打印十六进制信息;

ISA_BITFIELD里面的shiftCls我们了解到是存储类的指针的。

(lldb) x/4gx person //打印对象person的内存信息
0x1018482f0: 0x001d800100001129 0x0000000000000000
0x101848300: 0x00000001018483d0 0x0000000101848610

(lldb) p/t XDPerson.class //打印XDPerson的二进制值
(Class) $1 = 0b0000000000000000000000000000000100000000000000000001000100101000 XDPerson

(lldb) p/t 0x001d800100001129 //person的isa的二进制值
(long) $2 = 0b0000000000011101100000000000000100000000000000000001000100101001

(lldb) p/t $2>>3<<3 //shiftcl 前面有三位 我们需要右移3位 然后左移还原位置
(long) $3 = 0b0000000000011101100000000000000100000000000000000001000100101000

(lldb) p/t $3<<17>>17 //因为是模拟器_x86_64 shiftcl后面还有17位 故先左移17位找到 然后右移17位还原位置
(long) $4 = 0b0000000000000000000000000000000100000000000000000001000100101000
  • 通过上面的调试我们会发现$1$4的值是相同的,验证了对象personisa是和类XDPerson关联上了。

1.2 通过objc_getClass()调试 这里提供给一个objc的源码

Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}
inline Class 
objc_object::getIsa() 
{
    if (!isTaggedPointer()) return ISA();

    uintptr_t ptr = (uintptr_t)this;
    if (isExtTaggedPointer()) {
        uintptr_t slot = 
            (ptr >> _OBJC_TAG_EXT_SLOT_SHIFT) & _OBJC_TAG_EXT_SLOT_MASK;
        return objc_tag_ext_classes[slot];
    } else {
        uintptr_t slot = 
            (ptr >> _OBJC_TAG_SLOT_SHIFT) & _OBJC_TAG_SLOT_MASK;
        return objc_tag_classes[slot];
    }
}

我们知道objc都是!isTaggedPointer的,可以直接定位到ISA()函数。

inline Class 
objc_object::ISA() 
{
    assert(!isTaggedPointer()); 
#if SUPPORT_INDEXED_ISA
    if (isa.nonpointer) {
        uintptr_t slot = isa.indexcls;
        return classForIndex((unsigned)slot);
    }
    return (Class)isa.bits;
#else
    return (Class)(isa.bits & ISA_MASK);
#endif
}

SUPPORT_INDEXED_ISA这个宏定义在iOS的平台是0.

  • 可以直接知道isa.bits & ISA_MASK这么一个&运算来获取的。
  • 同时我们也可以了解objc->getIsa()返回的就是Class

下面我们通过lldb调试直接定位到cls

(lldb) x/4gx person     //打印对象的内存信息
0x101d047e0: 0x001d800100001129 0x0000000000000000
0x101d047f0: 0x00007fff9b5ff170 0x00000000c3000002

(lldb) p/x XDPerson.class  //打印类的地址
(Class) $1 = 0x0000000100001128 XDPerson

(lldb) p/x 0x001d800100001129 & 0x0000000ffffffff8 //通过对象的isa & ISA_MASK
(long) $2 = 0x0000000100001128

直接可以观察到$1$2的值是相同的,也验证了对象的isa指向了类。

2. isa的在类和元类之间的游走

2.1 类在底层的本质

这里先提供给一个源码,用来描述类的本质在底层是objc_class,同时也继承自objc_object,说明objc_class第一个成员也是isa

typedef struct objc_class *Class;
struct objc_class : objc_object{};

2.1 isa的继续走位 我们已经了解到了对象的isa指向了类,从而来绑定对象与类。那么类的isa又会怎么走呢。

我们继续通过lldb来调试。

XDPerson *person = [XDPerson alloc];
  • isa从对象指向类
(lldb) x/4gx person
0x10185eac0: 0x001d8001000011a9 0x0000000000000000
0x10185ead0: 0x000000010185eba0 0x000000010185ede0
(lldb) p/x 0x001d8001000011a9 & 0x0000000ffffffff8
(long) $1 = 0x00000001000011a8
(lldb) po $1
XDPerson

我们打印person的内存信息 通过对象的isa & ISA_MASK 获取到了类的信息。即isa从类的实例对象person指向了类XDPerson

  • isa从类指向元类
(lldb) x/4gx $1
0x1000011a8: 0x001d800100001181 0x00000001000011f8
0x1000011b8: 0x00000001003a1e50 0x0000000000000000
(lldb) p/x 0x001d800100001181 & 0x0000000ffffffff8
(long) $2 = 0x0000000100001180
(lldb) po $2
XDPerson

我们打印类XDPerson的内存信息,通过类的isa&ISA_MASK获取到了另外一个类的信息。即isa从类XDPerson又指向了类XDPerson(其实这个类是我们XDPersonmetaClass元类,它与第一步的XDPerson的内存地址并不同)。我们编译器会把一个类作为它的元类的实例化对象来处理,就像一个对象从类实例化出来的模式。

  • isa从元类指向根元类
(lldb) x/4gx $2
0x100001180: 0x001d800100aff0f1 0x00000001000011d0
0x100001190: 0x0000000101e142b0 0x0000000100000007
(lldb) p/x 0x001d800100aff0f1 & 0x0000000ffffffff8
(long) $3 = 0x0000000100aff0f0
(lldb) po $3
NSObject

我们打印元类XDPerson的内存信息 通过元类的isa & ISA_MASK 获取到了另外一个元类类NSObject的信息。即isa从元类XDPerson又指向了元类NSObject。这里我们可以来打印NSObjcr.class来观察,元类NSObject的内存地址NSObjcr.class的内存地址并不同。

  • isa从根元类指向根根元类
(lldb) x/4gx $3
0x100aff0f0: 0x001d800100aff0f1 0x0000000100aff140
0x100aff100: 0x0000000101e146e0 0x0000000300000007
(lldb) p/x 0x001d800100aff0f1 & 0x0000000ffffffff8
(long) $4 = 0x0000000100aff0f0
(lldb) po $4
NSObject

我们打印元类NSObject的内存信息,通过元类的isa & ISA_MASK获取到了根元类NSObject的信息。即isa 从元类NSObject指向了根元类NSObject。其实做到这里我们就可以看到了 第三步的NSObject与第四步的NSObject是同一片内存地址。

通过上面的lldb调试我们基本上了解到了isa的指向走位分析,下面就把苹果官方提供的isa的走位图拿出来做一下解释

  • 虚线代表了isa的走位。实例对象->类->元类->根元类->根根元类(根元类本身)。
  • 实线代表了继承关系。这里值得注意的就是根元类的父类是NSObject

继承关系

类的结构体的补充

struct objc_class {
    //Class isa
    Class superclass;
    ...省略部分...
}

说明类的第二段内存是指向父类。

类的继承关系

  1. 定义继承关系

XDTeacher -> XDPerson -> NSObject

  1. 验证继承关系
  • XDTeacher的父类验证
(lldb) x/4gx XDTeacher.class
0x1000011f8: 0x001d8001000011d1 0x00000001000011a8
0x100001208: 0x00000001003a1e50 0x0000000000000000
(lldb) po 0x00000001000011a8
XDPerson
  • XDPerson的父类验证
(lldb) x/4gx 0x00000001000011a8
0x1000011a8: 0x001d800100001181 0x0000000100aff140
0x1000011b8: 0x00000001003a1e50 0x0000000000000000
(lldb) po 0x0000000100aff140
NSObject
  • NSObject的父类验证
(lldb) x/4gx 0x0000000100aff140
0x100aff140: 0x001d800100aff0f1 0x0000000000000000
0x100aff150: 0x000000010105b980 0x0000000100000003
(lldb) po 0x0000000000000000
<nil>

验证了类的继承关系是XDTeacher -> XDPerson -> NSObject -> nil

元类的继承关系

  1. 类的继承链上的内存地址
(lldb) x/4xg XDTeacher.class
0x1000011f8: 0x001d8001000011d1 0x00000001000011a8
0x100001208: 0x00000001003a1e50 0x0000000000000000
(lldb) po 0x00000001000011a8
XDPerson

(lldb) x/4xg 0x00000001000011a8
0x1000011a8: 0x001d800100001181 0x0000000100aff140
0x1000011b8: 0x00000001003a1e50 0x0000000000000000
(lldb) po 0x0000000100aff140
NSObject

我们做好准备条件把类的父类的内存地址全部先获取。

  1. 元类的内存地址
  • XDTeacher的元类内存地址和元类的父类的内存地址
(lldb) p/x 0x001d8001000011d1 & 0x0000000ffffffff8
(long) $3 = 0x00000001000011d0
(lldb) x/4xg 0x00000001000011d0
0x1000011d0: 0x001d800100aff0f1 0x0000000100001180
0x1000011e0: 0x00000001018002a0 0x0000000300000003

我们获取到了XDTeacher元类的父类的内存地址0x0000000100001180

  • XDPerson的元类内存地址
(lldb) x/4xg 0x00000001000011a8
0x1000011a8: 0x001d800100001181 0x0000000100aff140
0x1000011b8: 0x00000001003a1e50 0x0000000000000000
(lldb) p/x 0x001d800100001181 & 0x0000000ffffffff8
(long) $4 = 0x0000000100001180

XDPerson的元类的内存地址是0x0000000100001180,同XDTeacher元类的父类的内存地址时相同的。

  • 这一步就可以验证了XDTeacher元类继承自XDPerson的元类
  • 全部验证
//查看XDPerson的元类的内存信息
(lldb) x/4xg 0x0000000100001180
0x100001180: 0x001d800100aff0f1 0x0000000100aff0f0 -->XDPerson的元类的父类内存地址
0x100001190: 0x0000000101d09e50 0x0000000300000003

//查看NSObjec的内存信息
(lldb) x/4xg 0x0000000100aff140
0x100aff140: 0x001d800100aff0f1 0x0000000000000000
0x100aff150: 0x000000010183a780 0x0000000100000003
//查看NSObjec的元类内存地址
(lldb) p/x 0x001d800100aff0f1 & 0x0000000ffffffff8
(long) $5 = 0x0000000100aff0f0 -->验证XDPerson的元类->NSObjec的元类

//查看根元类的内存信息
(lldb) x/4xg 0x0000000100aff0f0
0x100aff0f0: 0x001d800100aff0f1 0x0000000100aff140
0x100aff100: 0x0000000101d094c0 0x0000000300000007
//查看根元类的父类
(lldb) x/4xg 0x0000000100aff140
0x100aff140: 0x001d800100aff0f1 0x0000000000000000
0x100aff150: 0x000000010183a780 0x0000000100000003
//这里我么可以提前获取一下打印NSObject.class的内存信息 
//方便区分根元类NSObject 和 类NSObject 我这里没有打印
//po NSObject.class
//得到根元类的父类NSObject
(lldb) p/x 0x001d800100aff0f1 & 0x0000000ffffffff8
(long) $7 = 0x0000000100aff0f0
(lldb) po $7
NSObject
(lldb) po 0x0000000000000000
<nil>

通过我们一步步的调试我们的元类的继承关系就出来了。

  • XDTeacher元类->XDPerson元类->NSObject元类->NSObject->nil。

通过这一步的流程,也验证了isa走位图,笔者能力有限,探索不足之处可以在评论区指正。

学习之路,砥砺前行

不足之处可以在评论区指出