Objective-C isa指针探秘

稍微精深一点的IOS开发都听说过isa指针。它在OC的类中起到了指示自身类型的作用,是runtime实现的基础。那么isa指针到底是如何实现的呢,让我们从源码的层面进行分析。

NSObject -> Class -> objc_class -> objc_object

新建一个最简单的空类:

@interface Person : NSObject
@end

@implementation Person
@end

command点击NSObejct,我们可以看到cls的实现:

@interface NSObject  {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
    Class isa  OBJC_ISA_AVAILABILITY;
#pragma clang diagnostic pop
}

那么这个Class又是什么数据呢?

typedef struct objc_class *Class;

我们可以看到Class实际上是objc_class结构体的指针。而objc_class,就是OC类的元类

objc_class在objc源码中有三处定义:

// in runtime.h
struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
    .....
} OBJC2_UNAVAILABLE;


#if __OBJC2__
#include "objc-runtime-new.h"
#else
#include "objc-runtime-old.h"
#endif

// in objc-runtime-old.h
struct objc_class : objc_object {}

// in objc-runtime-new.h
struct objc_class : objc_object {}

我们当前使用的版本多是 objc2,所以起作用的是 最后一个 objc-runtime-new.h中定义的objc_classobjc_class则继承了objc_object

struct objc_class : objc_object 

objc_object同样在两个位置有定义:

in objc.h

#if !OBJC_TYPES_DEFINED
....
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};
#endif


in objc-private.h

#ifdef _OBJC_OBJC_H_
#error include objc-private.h before other headers
#endif

#define OBJC_TYPES_DEFINED 1

struct objc_object {
private:
    isa_t isa;
.....
}

根据objc-private.h中的宏定义我们可以看到,起作用的实际上是objc-private.h中的定义。

对一个继承了NSObject的类而言,isa最后能定位到objc_object中。而用来存储类信息的,是objc_objectisa_t isa;

isa_t

** isa_t是一个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;  // 这才是关键
    };
#endif
};

isa_t 是一个联合体,这里所占空间为 8字节,共64位 ,内存布局从低位到高位情况如下图:


    struct {
        ISA_BITFIELD;  // 这才是关键
    };

#   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

注意 在 struct 的变量A后面加 :(数字n), 是指定该变量A在内存中占用 n 位。

  • nonpointer(存储在第0字节) : 是否为优化isa标志。0代表是优化前的isa,一个纯指向类或元类的指针;1表示优化后的isa,不止是一个指针,isa中包含类信息、对象的引用计数等。现在基本上都是优化后的isa。

  • has_assoc (存储在第1个字节): 关联对象标志位。对象含有或者曾经含有关联引用,0表示没有,1表示有,没有关联引用的可以更快地释放内存。

  • has_cxx_dtor(存储在第2个字节): 析构函数标志位,如果有析构函数,则需进行析构逻辑,如果没有,则可以更快速地释放对象。

  • shiftcls :(存储在第3-35字节)存储类的指针,其实就是优化之前 isa 指向的内容。在arm64架构中有33位用来存储类指针。x86_64架构有44位。

  • magic(存储在第36-41字节):判断对象是否初始化完成, 是调试器判断当前对象是真的对象还是没有初始化的空间。

  • weakly_referenced(存储在第42字节):对象被指向或者曾经指向一个 ARC 的弱变量,没有弱引用的对象可以更快释放(dealloc的底层代码有体现)。

  • deallocating(存储在第43字节):标志对象是否正在释放内存。

  • has_sidetable_rc(存储在第44字节):判断该对象的引用计数是否过大,如果过大则需要其他散列表来进行存储。

  • extra_rc(存储在第45-63字节。):存放该对象的引用计数值减1后的结果。对象的引用计数超过 1,会存在这个里面,如果引用计数为 10,extra_rc 的值就为 9。

isa的初始化

对OC类而言,isa的初始化是在 alloc中完成的。具体来说是在

static ALWAYS_INLINE id
_class_createInstanceFromZone(Class cls,...) {

...
id obj; // id 是objc_object的指针

    // 3: ?
    if (!zone && fast) {
        obj->initInstanceIsa(cls, hasCxxDtor);
    } else {
        obj->initIsa(cls);
    }
...
}

initInstanceIsainitIsa最后都会统一到一个函数里:

(去除一些宏定义,断言以及条件判断等,我们直接将代码减少到它执行的代码)

inline void 
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor)  {
          isa_t newisa(0);

        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;

        isa = newisa;
}
  1. 首先对整个bits进行赋值,传入 ISA_MAGIC_VALUE ,在arm64架构下,该值为
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL

将该值转换为2进制

image.png

对应 isa_t 中内存布局的位置,可以看出对 bits 赋值 就是对 nonpinter 和 magic 赋值的过程。

2.其次对has_cxx_dtor赋值。

  1. 最后对shifcls赋值

shiftcls详解

shiftcls 里面存放着类的指针。众所周知指针是64位的,那么为什么可以放置在33位或或者44位的shiftcls中呢?在需要指针时又如何还原呢?

首先我们看下shiftcls的赋值:

inline void 
objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) 
{ 
....
        isa_t newisa(0);
....
        newisa.shiftcls = (uintptr_t)cls >> 3;
....
        isa = newisa;
    }
}

首先我们将cls的尾部三位去掉,这是因为对类而言,它至少包含一个isa指针,那么它的长度一定是8的整数倍。这里我们可以使用runtime计算下Person的长度来认证下:

#import 
size_t insSize = class_getInstanceSize([Person class]);
NSLog(@"PP Size:%zd",insSize);

然后,我们会看到源码中shiftcls的宏定义后面有一个数据 MACH_VM_MAX_ADDRESS:

 uintptr_t shiftcls          : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \

这是最大虚拟寻址空间,也就是内存空间能够分配的最大地址。以ARM32为例,它最大的虚拟寻址空间为 0x10,0000,0000。而33 + 3 (右移的三位) = 36 = 4 * 9。正好是虚拟寻址空间的位数 - 1。正好可以覆盖虚拟寻址空间。也就是说64位的数据中,前面64 - 36 = 18位是无用的。

因此,只要存储64位指针中间的33位就可以表示了。只要在使用的时候与上mask:0x0000000ffffffff8,就可以得到对应的类指针。

你可能感兴趣的:(Objective-C isa指针探秘)