iOS底层原理探索-03- isa

266 阅读5分钟

《目录-iOS & OpenGL & OpenGL ES & Metal》
之前探索alloc底层的时候,遗留了一个问题,initInstanceIsa方法是怎么关联的。是不是有不少小伙伴一直困扰isa到底是什么?我们今天就来探索一下~

一、isa是什么?

前面2篇文章都提到过,打印一个对象的内存地址,排在第一段的值是一个isa。那我们今天就来探究一下!

我们都知道oc中有一个特别重要类:NSObject,我们追踪进去看,会发现它里面只有一个isa,它是一个指针

@interface NSObject <NSObject> {
    Class isa  OBJC_ISA_AVAILABILITY;   
}

what?这么nb的类怎么就只有一个isa呢?是不是隐藏了一些其他的信息呢?我们再看源码中的objc_object

struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

的确只有一个isa。由此,我们可以看出每一个类都有一个初始且必有的属性-isa指针。这个isa的类型是class,也就意味着,它是一个类指针。

继续前两篇的探索,来到initInstanceIsa方法

inline void 
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
    assert(!cls->instancesRequireRawIsa());
    assert(hasCxxDtor == cls->hasCxxDtor());

    initIsa(cls, true, hasCxxDtor);
}

来到initIsa方法

inline void 
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) 
{ 
    assert(!isTaggedPointer()); 
    
    if (!nonpointer) {
    	//在这个地方,绑定isa的cls指向传进来的cls
        //我们可以直接从这里看到isa的源码
        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

        // This write must be performed in a single store in some cases
        // (for example when realizing a class because other threads
        // may simultaneously try to use the class).
        // fixme use atomics here to guarantee single-store and to
        // guarantee memory order w.r.t. the class index table
        // ...but not too atomic because we don't want to hurt instantiation
        isa = newisa;
    }
}

** isa是有一个联合体结构(union),它里面还有位域**
当多个数据需要共享内存或者多个数据每次只取其一时,可以利用联合体(union),利用union可以用相同的存储空间存储不同型别的数据类型,从而节省内存空间。

//联合体
union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;
#if defined(ISA_BITFIELD)
	//位域
    struct {
         ISA_BITFIELD;  // defined in isa.h
    };
#endif
};

联合体里面的属性是互斥的,比如说,设置了cls,就不用设置bits了。

我们从前面知道了isa是一个8字节,也就是64位,那isa里面到底存放了些什么?

二、isa里存放的有什么

# 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)

以真机下,arm64为例:从0~64位,依次存储的信息:

nonpointer:占1位,表示是否对 isa 指针开启指针优化【0:纯isa指针,1:不⽌是类对象地址,isa 中包含了类信息、对象的引⽤计数等】

has_assoc:占1位,关联对象标志位【0没有,1存在】

has_cxx_dtor:占1位,该对象是否有 C++ 或者 Objc 的析构器【0如果没有,则可以更快的释放对象;1如果有析构函数,则需要做析构逻辑】

shiftcls:占33位,存储类指针的值。开启指针优化的情况下,在 arm64 架构中有 33 位⽤来存储类指针。

magic:占6位,⽤于调试器判断当前对象是真的对象还是没有初始化的空间

weakly_referenced:占1位,表示对象是否被指向或者曾经指向⼀个 ARC 的弱变量, 没有弱引⽤的对象可以更快释放。

deallocating:占1位,表示对象是否正在释放内存

has_sidetable_rc:占1位,当对象引⽤技术⼤于 10 时,则需要借⽤该变量存储进位

extra_rc:占19位,当表示该对象的引⽤计数值,实际上是引⽤计数值减 1。【例如,如果对象的引⽤计数为 10,那么 extra_rc 为 9。如果引⽤计数⼤于 10,则需要使⽤到刚提到的 has_sidetable_rc】

** 我们可以看出,isa中其实包含了很多信息,其实就是便于我们优化内存**

这里找到一张图,很形象:

三、isa关联对象和属性

我们现在已经知道了一个对象的第一个属性是isa。继续上面的思路去调试,需要根据位域的信息进行一系列位运算有点太繁琐了。我们换另一个思路:
根据object_getClass(object)方法是可以拿到这个对象的类信息的,通过对象找到类,必然是通过isa,我们可以从这个方法入手,看isa是怎么把对象和类关联起来的。

//测试代码
LGPerson *object = [LGPerson alloc];
object_getClass(object);

来到object_getClass方法

Class object_getClass(id obj)
{
    if (obj) return obj->getIsa();
    else return Nil;
}

来到getIsa()方法

inline Class 
objc_object::getIsa() 
{
	//直接来到这一步,一般都不是TaggedPointer,是的话走下面
    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];
    }
}

来到ISA()方法

inline Class 
objc_object::ISA() 
{
    //...省略一些代码,只看关键代码
    
    //#define ISA_MASK        0x00007ffffffffff8ULL  __x86_64__
    //我们是在mac上跑的,所以用的x86的,arm64的是0x0000000ffffffff8ULL
    //给isa联合体的位域 加上一层蒙版,也就是 与 ISA_MASK
    //蒙版什么意思呢,就是遮住一部分,只让我们看有用的
    return (Class)(isa.bits & ISA_MASK);
    
}

我们来LLDB调试看一下:

可以说,isa就是通过与上一个遮罩ISA_MASK,将对象与类关联起来。
也可以说,isa关联了对象和类,对象的isa指向这个对象的类。

四、isa的走位

在分析isa走位之前,有个小知识点测试: 对象可以创建多个,那类可以创建多个吗?

对象可以创建多个,但类只能创建一个那我们就可以放心地去一步步向上找isa的指向了。

(ps:图片中,没有放p/x isa的地址 & isa_mask这一步,是因为我在正常工程中跑的,没在源码跑,发现p/x这一步打印的值和isa的值一样,就怕看起来太恶心,免去了多余的一步)

我们可以通过x/4xg object拿到isa,然后p/x isa的地址 & isa_mask,拿到指向地址。一步步向上找出isa的走位。

我们再看一下苹果官方文档的图。

我们来解读一下,里面有2条线:isa指向一条线。继承关系一条线。

isa指向:实例对象 --> 类 --> 元类 --> 根元类 --> 根元类。

细分:

  • 子类实例对象 --> 子类 --> 子元类 --> 根元类 --> 根元类
  • 父类实例对象 --> 父类 --> 父元类 --> 根元类 --> 根元类
  • 根类实例对象 --> 根类 --> 根元类 --> 根元类
* 对象是由类创建的
* 类其实也是一个对象(万物皆对象),类对象是由元类创建的
* 根类/根元类其实就是NSObject
* 如果继续查,后面就无限递归指向 根元类	

继承关系:

  • 子类 ---> 父类 ---> 根类 ---> nil
  • 子元类 ---> 父元类 ---> 根元类 ---> 根类 ---> nil
* 根元类继承自根类
* 根类继承自nil 

这里或许会有个疑问:类和元类是什么时候创建的呢?

其实,它们是在编译器编译的时候创建的。可以通过两种方法去查证:

  1. LLDB调试,断点在alloc方法执行之前,打印类的class,根据isa和isa_mask看是不是能拿到父类
  2. command+b,生成一个编译文件,放在machoView中看class相关信息,这时候程序还没有跑起来,但是已经有了相应的父类,即可证明

五、总结

  • 每个对象创建的时候都会有一个默认属性isa指针,指向创建这个对象的类
  • isa是一个联合体,里面包含了位域信息,主要目的是为了内存优化
  • isa的属性之间互斥
  • isa的走位:实例对象 --> 类 --> 元类 --> 根元类 --> 根元类