OC底层探索 - isa

110 阅读5分钟

通过 OC底层探索 - _class_createInstanceFromZone 一章,我们知道 isaisa_t 类型的。

union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    uintptr_t bits;

private:
    // Accessing the class requires custom ptrauth operations, so
    // force clients to go through setClass/getClass by making this
    // private.
    Class cls;

public:
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };

    bool isDeallocating() {
        return extra_rc == 0 && has_sidetable_rc == 0;
    }
    void setDeallocating() {
        extra_rc = 0;
        has_sidetable_rc = 0;
    }
#endif

    void setClass(Class cls, objc_object *obj);
    Class getClass(bool authenticated);
    Class getDecodedClass(bool authenticated);
};

可见,isa 实际上是个联合体。对底层有所了解的话,都会知道,isa 是用来存放 Class类型的结构体指针的。那么为什么 isa 会是一个联合体呢?

这是因为isa分为 POINTER_ISA(指针类型)和 NONPOINTER_ISA(非指针类型)。
相对早期版本,苹果现在采用了 nonPointerIsa 来进行内存优化。因为 isa 是一个Class类型的结构体指针, 占8个字节,但是8个字节意味着它就有 8*8=64 位。用来存储一个指针根本不需要这么多内存空间,会造成浪费。而 nonPointerIsa 会把一些和对象息息相关的信息也放置在这块内存中。
所以为了兼容早期版本的设计,采用了联合体的设计。

那么 nonPointerIsa 中到底存了些什么呢?

通过 ISA_BITFIELD 这个宏可以看到,在不同架构上,存储的内容是不同的。

// arm64 (模拟器)
uintptr_t nonpointer        : 1;                                       
uintptr_t has_assoc         : 1;                                       
uintptr_t weakly_referenced : 1;                                       
uintptr_t shiftcls_and_sig  : 52;                                      
uintptr_t has_sidetable_rc  : 1;                                       
uintptr_t extra_rc          : 8


// arm64 (真机)
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 unused            : 1;                                       
uintptr_t has_sidetable_rc  : 1;                                       
uintptr_t extra_rc          : 19


// x86_64
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 unused            : 1;                                         
uintptr_t has_sidetable_rc  : 1;                                         
uintptr_t extra_rc          : 8

uintptr_t nonpointer : 1; 这种写法,是使用了位域。

位域的宽度不能超过前面数据类型的最大⻓度,比如int占4个字节也就是32位,那后面的数字就不能超过32。一个位域存储在同一个字节中,如一个字节所剩空间不够存放另一位域时,则会从下一单元起存放该位域。位域能够节省一定的内存空间。

以上面 x86_64 为例,isa 一共有 64 位,第1位存放 nonpointer 的值,第2位存放 has_assoc的值,第 4 到 47 位存放的是 shiftcls 的值, 第 57 到 64 位存放 extra_rc 的值。

nonpointer_isa存储内容含义

nonpointer : 是否对 isa 指针开启指针优化 0 - 未开启,当前是纯 isa指针; 1 - 开启优化,isa中不止存储了指针,还存储其他信息

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

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

shiftcls : 存储类指针的值。开启指针优化的情况下,在 x86_64 架构中,有 44 位用来存储类指针。

magic : 用于调试器判断当前对象是真的对象还是没有初始化的空间

weakly_referenced : 标志对象是否被指向或曾经指向一个ARC的弱变量,没有弱引用的对象可以更快释放

unused : 标志对象是否正在释放

has_sidetable_rc : 是否需要使用 sidetable 来存储引用计数

extra_rc : 对象的引用计数值

通过 isa 获取类地址

if (NSHostByteOrder() == NS_BigEndian) {
    NSLog(@"大端模式");
}else if (NSHostByteOrder() == NS_LittleEndian) {
    NSLog(@"小端模式");
}else {
    NSLog(@"NS_UnknownByteOrder");
}
TestClass *test = [TestClass new];

因为测试使用的是 iPhone7 真机,所以是arm64架构的,又通过打印证明是小端模式

image.png

根据nonpointer_isa的结构可以得知,除去左侧28位,和右侧3位,后中间的33位正是存储的类地址。

我们通过 isa 的位运算的到的结果 与 TestClass.class 对比,发现的确中间33位存放的就是类地址。

image.png

isa_t 里面提供了获取类的方法

Class getClass(bool authenticated);


inline Class
isa_t::getClass(MAYBE_UNUSED_AUTHENTICATED_PARAM bool authenticated) {
#if SUPPORT_INDEXED_ISA
    return cls;
#else

    uintptr_t clsbits = bits;

#   if __has_feature(ptrauth_calls)
#       if ISA_SIGNING_AUTH_MODE == ISA_SIGNING_AUTH
    // Most callers aren't security critical, so skip the
    // authentication unless they ask for it. Message sending and
    // cache filling are protected by the auth code in msgSend.
    if (authenticated) {
        // Mask off all bits besides the class pointer and signature.
        clsbits &= ISA_MASK;
        if (clsbits == 0)
            return Nil;
        clsbits = (uintptr_t)ptrauth_auth_data((void *)clsbits, ISA_SIGNING_KEY, ptrauth_blend_discriminator(this, ISA_SIGNING_DISCRIMINATOR));
    } else {
        // If not authenticating, strip using the precomputed class mask.
        clsbits &= objc_debug_isa_class_mask;
    }
#       else
    // If not authenticating, strip using the precomputed class mask.
    clsbits &= objc_debug_isa_class_mask;
#       endif

#   else
    clsbits &= ISA_MASK;
#   endif

    return (Class)clsbits;
#endif
}

可见 clsbits &= ISA_MASK; 这样就能计算出类地址来。

以arm64为例
define ISA_MASK        0x0000000ffffffff8ULL
8写作二进制是 1000
f写作二进制是 1111
ISA_MASK 有8个f,所以其就有 4*8=32个1,后面补上8的二进制 1000 ,前面补0
所以 isa & ISA_MASK 就可以得出我们上面操作的到的类地址

大端模式和小端模式

  • 大端模式,是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中,这样的存储模式有点儿类似于把数据当作字符串顺序处理:地址由小向大增加,数据从高位往低位放;这和我们的阅读习惯一致。
  • 小端模式,是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中,这种存储模式将地址的高低和数据位权有效地结合起来,高地址部分权值高,低地址部分权值低。

举个例子,把 0x12345678 存入内存,假设从地址 0x4000 开始存放

小端模式下

image.png

大端模式下

image.png