专注、坚持

Obj-C 中的 isa 指针

2019.08.18 by kingcos
Date Notes Refer.
2019-08-18 首次提交 objc4-750.1

0

Preface

isa 指针是所有 Obj-C 对象中都拥有的一个成员。因为除了继承链之外,Obj-C 还特有一条从实例对象到类对象、元类对象的链。而后者正是依靠 isa 而串联起来的,那么本文就将结合源码谈谈 Obj-C 中的 isa

对于不大熟悉 Obj-C 中对象的同学,可以先行阅读 Obj-C 中的对象一文。

Where

// NSObject.h
@interface NSObject <NSObject> {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"
    Class isa  OBJC_ISA_AVAILABILITY; // ⬅️
#pragma clang diagnostic pop
}

// objc-runtime-new.h
struct objc_class : objc_object {
    // Class ISA;
}

// objc-private.h
struct objc_object {
private:
    isa_t isa; // ⬅️
}

如上,对于实例对象,isa 定义在基类 NSObject 的内部被其它类所继承;对于类对象和元类对象,isaobjc_object 结构体中被继承。

1

实例对象中的 isa 指向类对象,类对象中的 isa 指向元类对象,元类对象中的 isa 指向根元类对象(包括根元类对象也指向自己)。

What

那么 isa 到底是什么呢?其实在 ARM 64 之前,isa 直接保存了类对象或者元类对象的地址,而之后使用了位域结构存储了更多信息。

我们可以在源码中追根溯源找到 isa_t 类型的共用体(关于共用体和位域,可参考 C/C++ 中的位域与共用体一文):

// objc-private.h
struct objc_object {
private:
    isa_t isa;

// ...
}

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.h
// ARM 64
# 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)

// _uintptr_t.h
#ifndef _UINTPTR_T
#define _UINTPTR_T
typedef unsigned long           uintptr_t;
#endif /* _UINTPTR_T */

为了最大程度地优化内存占用,isa 使用了位域与共用体结构,将 unsigned long 的 8 个字节分为 64 位来使用。在 ARM 64 结构之下,isa 中的内容如下:

ISA_BITFIELD Bits Notes
nonpointer 1 0:普通指针,直接存储类或元类对象的地址
1:经过优化的指针,使用位域结构存储了更多信息
has_assoc 1 0:标记未设置过关联对象
1:标记设置过关联对象
has_cxx_dtor 1 0:标记没有 C++ 析构函数
1:标记有 C++ 析构函数
shiftcls 33 存储类或元类对象的地址
magic 6 调试时分辨对象是否未完成初始化
weakly_referenced 1 0:标记未弱引用过
1:标记弱引用过
deallocating 1 0:对象未正在释放
1:对象正在释放
has_sidetable_rc 1 0:引用计数存储在 extra_rc
1:引用计数过大,存储在 side table 类的属性中
extra_rc 19 存储引用计数 - 1

得到了 isa 中各个成员的位数,这样我们就可以通过掩码来运算得出我们想要的部分。比如 ISA_MASK0x0000000ffffffff8 换算为二进制即 0b 0000 0000 0000 0000 0000 0000 0000 1111 1111 1111 1111 1111 1111 1111 1111 1000,因此如果使用 & 逻辑与将把 shiftcls 即类或元类对象的地址得出:

#import <Foundation/Foundation.h>
#import <objc/runtime.h>

@interface Foo : NSObject
@end

@implementation Foo
@end

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Foo *foo = [[Foo alloc] init];

        NSLog(@"%p", object_getClass(foo));
    }
}

// OUTPUT:
// 0x100002478
// LLDB:
// (lldb) p/x foo->isa
// (Class) $0 = 0x001d800100002479 Foo
// (lldb) p/x 0x001d800100002479 & 0x0000000ffffffff8ULL
// (unsigned long long) $1 = 0x0000000100002478

Reference