今天看啥  ›  专栏  ›  聪莞

NONPOINTER_ISA和散列表

聪莞  · 掘金  ·  · 2019-07-09 02:25
阅读 14

NONPOINTER_ISA和散列表

#NONPOINTER_ISA 这个设计思想跟TaggetPointer类似,ISA其实并不单单是一个指针。其中一些位仍旧编码指向对象的类。但是实际上并不会使用所有的地址空间,Objective-C 运行时会使用这些额外的位去存储每个对象数据就像它的引用计数和是否它已经被弱引用。 从源码来看:

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
};
复制代码

isa是一个定义的联合体。 ####什么是联合体?

当多个数据需要共享内存或者多个数据每次只取其一时,可以利用联合体(union),利用union可以用相同的存储空间存储不同型别的数据类型,从而节省内存空间。

我们来写一个联合体的例子

#define DirectionLeftMask (1 << 0)  // 0000 0001        //第一位表示向左
#define DirectionRightMask (1 << 0)  // 0000 0010       //第二位表示向右

@interface Car() {
    //联合体来表示方向
    union {
        char bits;
    }_direction;
}

@end

@implementation Car

- (instancetype)init
{
    self = [super init];
    if (self) {
        _direction.bits = 0b00000000;        //初始化二进制
    }
    return self;
}

- (void)moveLeft {
    _direction.bits |= DirectionLeftMask;  // 0000 0000 | 0000 0001 = 0000 0001
}

- (void)moveRight {
    _direction.bits |= DirectionRightMask; // 0000 0000 | 0000 0010 = 0000 0010
}
复制代码

这样我们就用了同一片地址来表示了两个取值。 这样的写法有点麻烦,所以又引入了位域的概念。引入位域后如下代码:

@interface Car() {
    //联合体来表示4个方向
    union {
        char bits;
        struct {
            //位域
            char left : 1;    //第一位代表向前,占一位
            char right : 1;  
            char front : 1;
            char back : 1;
        };
    }_direction;
}

@end

@implementation Car

- (instancetype)init
{
    self = [super init];
    if (self) {
        _direction.bits = 0b00000000;        //初始化二进制
    }
    return self;
}

- (void)moveLeft {
    _direction.left = 1;
}

- (void)moveRight {
    _direction.right = 1;
}

- (void)moveFront {
    _direction.front = 1;
}

- (void)moveBack {
    _direction.back = 1;
}
复制代码

接下来,我们接着查看isa的定义,它里面定义了一个位域:ISA_BITFIELD,点击查看这个宏:

# if __arm64__
#   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
# elif __x86_64__
#   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

复制代码

可以看到,__x86_64__和__arm64__下的位域定义是不一样的,不过都是占满了所有的64位(1+1+1+33+6+1+1+1+19 = 64,__x86_64__同理),下面来说明一下每一个位域参数的含义: nonpointer:表示是否对isa开启指针优化 。0代表是纯isa指针,1代表除了地址外,还包含了类的一些信息、对象的引用计数等。 has_assoc:关联对象标志位 has_cxx_dtor:该对象是否有C++或Objc的析构器,如果有析构函数,则需要做一些析构的逻辑处理,如果没有,则可以更快的释放对象 shiftcls:存在类指针的值,开启指针优化的情况下,arm64位中有33位来存储类的指针 magic:判断当前对象是真的对象还是一段没有初始化的空间 weakly_referenced:是否被指向或者曾经指向一个ARC的弱变量,没有弱引用的对象释放的更快 deallocating:是否正在释放 has_sidetable_rc:当对象引用计数大于10时,则需要进位 extra_rc:表示该对象的引用计数值,实际上是引用计数减一。例如:如果引用计数为10,那么extra_rc为9。如果引用计数大于10,则需要使用has_sidetable_rc

#散列表 在runtime内存空间中,SideTables是一个hash数组,里面存储了SideTable。SideTables的hash键值就是一个对象obj的address。 因此可以说,一个obj,对应了一个SideTable。但是一个SideTable,会对应多个obj。因为SideTable的数量有限,所以会有很多obj共用同一个SideTable。

我们来看一下SideTable的结构

struct SideTable {
    spinlock_t slock;      // 自旋锁
    RefcountMap refcnts;    //引用计数的Map表 key-value
    weak_table_t weak_table;  //弱引用表
复制代码

来抛出一个问题,为什么不直接用一张SideTable,而是用SideTables去管理多个SideTable? SideTable里有一个自旋锁,如果把所有的类都放在同一个SideTable,有任何一个类有改动都会对整个table做操作,并且在操作一个类的同时,操作别的类会被锁住等待,这样会导致操作效率和查询效率都很低。而有多个SideTable的话,操作的都是单个Table,并不会影响其他的table,这就是分离锁。

继续SideTables,来看一下散列表的数据结构(数组+链表),举个例子,我们需要把小于100的放到第1个Table,大于900的放到第6个Table:

image.png

如何从sideTables里找到特定的sideTable呢,这就用到了散列函数。查看源码,runtime是通过这么一个函数来获取到相应的sideTable:

table = &SideTables()[obj];

点进去:
static StripedMap<SideTable>& SideTables() {
    return *reinterpret_cast<StripedMap<SideTable>*>(SideTableBuf);
}
复制代码

如果看不懂没关系,来看StripedMap的定义:

template<typename T>
class StripedMap {
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
    enum { StripeCount = 8 }; // iPhone时这个值为8
#else
    enum { StripeCount = 64 }; //否则为64
#endif

    struct PaddedT {
        T value alignas(CacheLineSize);
    };

    PaddedT array[StripeCount];

    static unsigned int indexForPointer(const void *p) {
        //这里是做类型转换
        uintptr_t addr = reinterpret_cast<uintptr_t>(p);

        //这就是哈希算法了
        return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
    }
public:
    T& operator[] (const void *p) { 
        //返回sideTable
        return array[indexForPointer(p)].value; 
    }
复制代码

可以看到,在对StripeCount取余后,所得到的值根据机器不同,会在0-7或者0-63之间,这就是通过哈希函数来获取到了sideTable的下标,然后再根据value取到所需的sideTable。

我们来验证一下: 编译objc源码,打上断点看一下具体调用:

image.png
image.png

执行table = &SideTables()[obj];之后,执行到了array[indexForPointer(p)].value;,然后进行哈希算法获取到下标,再返回所需的sideTable

image.png




原文地址:访问原文地址
快照地址: 访问文章快照