iOS备忘录

runtime(一) isa 指针

2019-03-15  本文已影响0人  小新0514

本文章基于 objc4-750 进行测试.
objc4 的代码可以在 https://opensource.apple.com/tarballs/objc4/ 中得到.

类和对象

id, Class 和 NSObject 是 iOS 类和对象中比较重要的, 在 objc.h 和 NSObject.h 中找到了他们的定义:

// NSObject.h
@interface NSObject <NSObject> {
    Class isa  OBJC_ISA_AVAILABILITY;
}
// objc.h
typedef struct objc_object *id;
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};
typedef struct objc_class *Class;

objc_class 这个结构体可以在 runtime.h 中找到, 但 runtime.h 中有众多的 OBJC2_UNAVAILABLE 标记, 其中 objc_class 就是其中一个:

struct objc_class {
    ...
} OBJC2_UNAVAILABLE;

查看 runtime 的源码可以发现, 工程中有 objc_private.h 以及 objc_runtime_new.h 文件, 继续探索可以发现:

// NSObject.h 中:
#include <objc/objc.h> 
// NSObject.mm 中:
#include "objc-private.h" 
#include "NSObject.h"
// objc.h 中
#if !OBJC_TYPES_DEFINED // 宏定义为 0 才会编译下面的代码
typedef struct objc_class *Class;
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};
typedef struct objc_object *id;
#endif
// objc-private.h 中
#ifdef _OBJC_OBJC_H_ // 在引入 objc-private.h 之前引入 objc.h 的话会出现错误
#error include objc-private.h before other headers
#endif

#define OBJC_TYPES_DEFINED 1 // 宏定义的值为 1, 避免 objc.h 编译相关代码
#undef OBJC_OLD_DISPATCH_PROTOTYPES
#define OBJC_OLD_DISPATCH_PROTOTYPES 0

可以看出, NSObject 是先引入的 objc-private.h, 后引入的 objc.h, 所以 objc.h 无法编译 Class 和 id 相关的部分, objc2 中的 Class 和 id 是在 objc_private.h 和 objc_runtime_new.h 中定义的. 由于代码过长, 我只贴出一部分:

// objc_private.h
typedef struct objc_class *Class;
typedef struct objc_object *id;
// objc_runtime_new.h
struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    ...
}

经过探索我们看到, 不管是类还是对象, 最终都落到了结构体 objc_object 上.

isa 指针

在 runtime 的源码中可以找到两个 isa 指针:

@interface NSObject <NSObject> {
    Class isa  OBJC_ISA_AVAILABILITY;
}
struct objc_object {
private:
    isa_t isa;
public:
     ...
} 

NSObject 调用 alloc, 会返回一个 id 类型的指针, id 类型强制转换为 NSObject 之后, 访问的 Class 类型的 isa 实际上就是结构体 objc_object 中 isa_t 类型的 isa. 可以依照如下方法测试一下(需要支持 C++):

struct Woman {
private:
    NSInteger age;
public:
    void setAge(NSInteger newAge) {
        age = newAge;
    }
};

struct Man {
    NSInteger age;
};

int main(int argc, char * argv[]) {
    Woman woman = Woman();
    woman.setAge(10);
    void * unknow = &woman;
    Man * man = (Man *)unknow;
    NSLog(@"%ld", (long)man->age);
}

所以整个 isa 指针部分, 最终都归结到一个 isa_t 联合体上.

isa 指针的优化

我们知道 isa 指针实际上是指向对应的类对象的, 但 iOS 现在已经进入 64 位的时代了, 64bit 可寻址的范围十分巨大, 而最新的 iPhone XS Max 设备的运行内存也不过 4 个 G, 实际上 32bit 就可以完成 4 个 G 的寻址任务, 所以使用 64bit 来寻址就有些浪费了, 而且程序运行中指针的数量也是十分的多, 会浪费很多内存. 所以从 32 位机过渡到 64 位机的同时, 苹果也考虑到了指针的优化问题.

isa_t

isa_t 是一个联合体(共用体), 联合体和结构体类似, 区别在于它所有的成员共用一段内存.

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_t 联合体的大小就是 64 bit, 联合体中的 cls、bits 和 一个结构体共同使用这 64 bit 的地址空间, 比较重要的就是结构体中的宏定义 ISA_BITFIELD, 该宏定义在 isa.h 中找到了定义处:

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

这里我只摘录了真机环境(ARM64)下的 ISA_BITFIELD 的定义. 在对象 alloc 结束后, 会调用初始化 isa 联合体的函数, 在这个函数中, 会将对象的地址赋值给 isa 联合体的成员.

接下来我们测试一下 isa 指针

这里注意要用真机测试, 真机是 ARM64 环境, 模拟器是 x86_64 环境.

- (void)test {
    NSObject * obj = [[NSObject alloc] init];
    NSLog(@""); //在这里打一个断点
}

断点停在 NSLog 处之后, 我们用 LLDB 来分别调试一下(p/x 是以十六进制形式输出).
(lldb) p/x obj
(lldb) p/x obj->isa
(lldb) p/x [obj class]
(lldb) p/x &obj
(lldb) p/x &obj->isa

对应的输出分别是:

(NSObject *) $0 = 0x0000000283044760
(Class) $1 = 0x000001a22b16feb1
(Class) $2 = 0x000000022b16feb0
(NSObject **) $3 = 0x000000016fd7d4b8
(Class *) $4 = 0x0000000283044760
  1. obj 和 &obj->isa 输出的都是 obj 对象在内存中的地址, C 语言中, 一个结构体的地址就是这个结构体第一个成员的地址, 而 obj 的第一个成员就是 isa, 所以 isa 的地址就是 obj 的地址.
  2. obj->isa 和 [obj class]
    一个对象的 isa 指针指向它的类对象. 所以 obj->isa 和 [obj class] 输出的地址应该是一样的, 这里的 obj->isa 输出了优化后的 isa 指针, 将 isa & ISA_MASK 得到的结果, 就是 [obj class] 的输出结果. 另外可以看到 obj->isa 的高 19 位都是 0, 也就是说这个对象只有一个强引用, 引用计数是1, 并且没有关联对象、没有 weak 引用, 也没有超出引用计数范围等.
  3. &obj
    obj 是一个指针, 它指向的地址是 obj 这个对象的地址, &obj 实际上是这个指针在栈上的地址.

下一篇打算写一下 isa 的补充--SideTable (已更新: https://www.jianshu.com/p/ea4c176ffb2b)

上一篇 下一篇

猜你喜欢

热点阅读