本文章基于 objc4-750 进行测试.
objc4 的代码可以在 https://opensource.apple.com/tarballs/objc4/ 中得到.
类和对象
id, Class 和 NSObject 是 iOS 类和对象中比较重要的, 在 objc.h 和 NSObject.h 中找到了他们的定义:
// NSObject.h
@interface 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
// 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 {
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 联合体的成员.
- nonpointer
共用体 isa_t 中的一个结构体成员, 共用 isa_t 的第一个 bit. 这个 bit 标记 isa 指针是否支持优化, 目前 ARM64 环境下都是支持优化的. 只有这个标记位为 1, 才会有后面的 isa 优化. - has_assoc
共用 isa_t 的第二个 bit, 标记这个对象是否有绑定关联对象, 对应 objc_setAssociatedObject() 和 objc_getAssociatedObject(), 如果没有绑定, 则会跳过释放关联对象的步骤, 能够在释放对象时节省时间. - has_cxx_dtor
共用 isa_t 的第三个 bit, 标记本对象是否有析构函数, 如果没有析构函数, 则会跳过析构逻辑, 能加快对象的释放. - shiftcls
共用 isa_t 的第 4 到第 36 个 bit, 用于存储类对象的地址. 优化后的 isa 指针使用了 33 个 bit 来存储它的类对象地址, 33 bit 可以寻址 0~8G 的地址空间, 对目前最大允许内存为 4G 的 iPhone 手机来说是绰绰有余的. - magic
和 AutoreleasePoolPage 里的 magic 类似, 在申请一段内存空间并初始化后设置为一个固定的值, 作为已完成内存申请并初始化的标记. - weakly_referenced
标记本对象是否有 weak 指针指向, 如果没有, 则释放本对象时, 就会跳过 weak 指针的处理逻辑, 加快释放速度. - deallocating
标记本对象是否正在回收, 可以避免使用到正处于回收中的对象, 造成错误. - has_sidetable_rc
isa 指针中用来存储引用计数的位数有限, 虽然可以存储 2^19 引用计数, 但最终还是要考虑到超过这个数字时的方案, 苹果给的方案是使用 SideTable, 一个 hash 表. 这个标记位标记引用计数已经超出 isa 指针预留的数目. - extra_rc
isa 指针的最高 19bit, 用来存储对象的引用计数, 通常情况下这里存储的是实际的引用计数减去 1 的结果, 当 release 的时候, 如果存储的是0, 就会启动释放流程. 平时我们输出 retainCount 的时候, 都会把这个数字加 1 后返回. - ISA_MASK
isa & ISA_MASK 可以保留 isa 的第 4 到 36 个 bit, 得出的结果再向右移 3 位, 就是指向对应的类对象的指针的值了. - ISA_MAGIC_MASK 和 ISA_MAGIC_VALUE
如果一个 isa & ISA_MAGIC_MASK == ISA_MAGIC_VALUE, 那么这个 isa 指针是一个完整可用 isa 指针. - RC_ONE
一个宏定义, 1 << 45 刚好是 extra_rc 的最低位, 当对一个对象进行 retain 操作时, 直接给这个对象的 isa 指针加上一个 RC_ONE, 就相当于给 extra_rc 加 1. - RC_HALF
一旦引用计数过大, isa 无法存储时, runtime 会利用 isa 和 SideTable 同时来存储引用计数, extra_rc = RC_HALF, 来存储 2^18 个引用计数, 其它的使用 SideTable. 如果 has_sidetable_rc 为 1, 管理引用计数时会优先操作 SideTable, 直到引用计数小于 2^18, 就会将 has_sidetable_rc 设置为 0, 按照正常的 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
- obj 和 &obj->isa 输出的都是 obj 对象在内存中的地址, C 语言中, 一个结构体的地址就是这个结构体第一个成员的地址, 而 obj 的第一个成员就是 isa, 所以 isa 的地址就是 obj 的地址.
- obj->isa 和 [obj class]
一个对象的 isa 指针指向它的类对象. 所以 obj->isa 和 [obj class] 输出的地址应该是一样的, 这里的 obj->isa 输出了优化后的 isa 指针, 将 isa & ISA_MASK 得到的结果, 就是 [obj class] 的输出结果. 另外可以看到 obj->isa 的高 19 位都是 0, 也就是说这个对象只有一个强引用, 引用计数是1, 并且没有关联对象、没有 weak 引用, 也没有超出引用计数范围等. - &obj
obj 是一个指针, 它指向的地址是 obj 这个对象的地址, &obj 实际上是这个指针在栈上的地址.
下一篇打算写一下 isa 的补充--SideTable (已更新: https://www.jianshu.com/p/ea4c176ffb2b)