编译器会把 OC(Objective-C) 代码重写(-rewrite-objc)为 C 和 C++ 代码,即把 OC 的类、block、属性、方法、协议、实例变量、类别、实例变量列表、方法类别、属性列表等表达为 C 结构体,通过运行时库的消息机制实现 OC 的动态特性,完成从 C 和 C++ 到 OC 语言特性的跃变。这是一个以低级概念(结构体)为基础,通过对函数调用流程的逻辑构建形成了以消息发送为基础的编码 API;通过 isa 指针构建数据与代码之间的内聚与互服务关系,通过 superclass 指针构建结构体之间的继承关系,形成二维的网状结构,具有丰富表现能力。
结构体、isa、superclass 和函数动态查找,这些 C 和 C++ 的概念与构造块,在运行时库和代码重写机制的加持下,最终以类和消息发送两个高级概念提供给程序设计人员,完全屏蔽掉了底层实现机制的复杂性。
本文不涉及函数动态查找的内容,只研究类和类中各元素与结构体的映射关系、对象的类指针 isa 和 父类指针 superclass 构造的对象体系和类体系结构、isa 和 superclass 联合体 union isa_t 的内部机制。
对象的本质
万物皆对象,实例对象、类对象和元类对象,都是用结构体表达的,对象具有相同的头部结构即一个 isa 字段。在 OC 中一个以 isa 开始的堆上的连续内存区域就是一个对象,8字节长的 isa 字段的内部各个 bit 的定义就是对象的本质,它是实例对象与实例类对象、类对象与元类对象、元类对象和根元类对象之间的桥梁。
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
....
}
struct objc_object {
private:
isa_t isa;
public:
// ISA() assumes this is NOT a tagged pointer object
Class ISA();
// getIsa() allows this to be a tagged pointer object
Class getIsa();
...
}
typedef struct objc_class *Class;
typedef struct objc_object *id;
实例对象之间互相独立,是以类对象为模版,请求系统开辟出来的一块连续可读写的内存区域,一个分配在堆上的结构体,其首个字段 isa 指向类对象,即存储类对象的内存起始地址。同理,类对象的 isa 指向元类对象,元类对象的 isa 指向根元类,根元类的 isa 指向自己。
类 Class 和 对象 id 本质上是一样的,都是在堆上以都是 **isa **开始的一个占用连续内存区域的结构体
继承机制比较简单,不同的类体系(如NSObject 和 NSProxy)中类和元类最终的父类都是根类(NSObject 或NSProxy)
模型重写
@interface NSObject1 : NSObject
@end
@implementation NSObject1
@end
clang -rewrite-objc main.m -o main.cpp 重写后:
#ifndef _REWRITER_typedef_NSObject
#define _REWRITER_typedef_NSObject
typedef struct objc_object NSObject;
typedef struct {} _objc_exc_NSObject;
#endif
struct NSObject_IMPL {
Class isa;
};
#ifndef _REWRITER_typedef_NSObject1
#define _REWRITER_typedef_NSObject1
typedef struct objc_object NSObject1;
typedef struct {} _objc_exc_NSObject1;
#endif
struct NSObject1_IMPL {
struct NSObject_IMPL NSObject_IVARS;
...
};
所有 OC 类重写后对应的类型都是 struct objc_object 。类就是一个 objc_object 类型结构体体,是一个对象,通过 typedef 为 objc_object 定义的别名。但是 objc_object 结构体的字段是固定不可变的,无法表述类中定义的实例变量、属性、协议等等元素。这一个定义丢失了类对数据和行为封装后所有获得的丰富语意,一定需要其它的结构体来承载数据与行为的封装所代理的丰富性。
OC 层面的类经过重写后被二分抽象化。类型被 OC 运行时预定义的 objc_object 结构体的别名类型;数据与行为通过动态产生的多个结构体承载,即 _IMPL 结构体。系统用 objc_object 的 isa 赋予各个别名类型以丰富的语意,为 _IMPL 结构体封装的数据提供行为规范,构建成了以类型封装的行为和 *_IMPL 结构体封装的数据的二维体系结构。
isa 结构
万物皆对象,对象都有自己模版即类或元类。对象和其模版之间通过 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 结构分析
实例对象封装数据,首字段存 isa 存储类对象在内存中的地址,后续字段存储实例变量
@interface NSObject1 : NSObject
@property (copy) NSString *name;
@end
@implementation NSObject1
@end
NSObject1 * obj = [[NSObject1 alloc] init];
obj.name = @"name_123456";
(lldb) x/4gx obj
0x101718580: 0x001d800100001355 0x0000000100001048
0x101718590: 0x0000000101718660 0x00000001017188a0
**类对象内存地址 **
(lldb) p/x 0x001d800100001355 & 0x00007ffffffffff8ULL
(unsigned long long) $44 = 0x0000000100001350
(lldb) po 0x0000000100001350
NSObject1
x/4gx 0x0000000100001350 //显示元类对象内存布局
重复上述过程,可从对象追溯到类对象,到元类对象,... ,直到根元类对象
元类对象也在堆上,紧邻类对象在其之上
类对象 name 属性
(lldb) po 0x0000000100001048
name_123456
isa bit 结构 分析
isa_t 是Class cls、uintptr_t bits、struct { ISA_BITFIELD;} 三个属性的联合体,同样的数据对外展现出两三种完全不同的概念系,其中 Class cls 以数据的整体性为面向对象编程范式提供类信息,uintptr_t bits 提供无结构无概念的纯 bit 信息,struct { ISA_BITFIELD;} 以结构化的 bit 信息为运行时库提供紧凑的数据存储与查询服务,是分析和学习的重点。
指针优化(nonpointer: 1)
有两种查找类的方式:
-
SUPPORT_PACKED_ISA 通过 isa_t 中存储的内存位置信息直接寻址类对象
-
SUPPORT_INDEXED_ISA 在 isa_t 存储类对象在表中的下标,通过下标找到类对象。
ALWAYS_INLINE Class & classForIndex(uintptr_t index) { assert(index > 0); assert(index < (uintptr_t)objc_indexed_classes_count); return objc_indexed_classes[index]; }
x86_64 和 arm64 都采用 PACKED,下面是 x86_64 架构中的 ISA_BITFIELD 都格式(采用的测试环境);其它架构都采用 INDEXED 方式查找类对象。
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
# define ISA_MAGIC_MASK 0x001f800000000001ULL
# define ISA_MAGIC_VALUE 0x001d800000000001ULL
# 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
# define RC_ONE (1ULL<<56)
# define RC_HALF (1ULL<<7)
PACKED 和 INDEXED 都采用指针优化,即 SUPPORT_NONPOINTER_ISA === 1,isa_t 都经结构化把每一个 bit 赋予了不同的含义。
这里 SUPPORT_NONPOINTER_ISA 存储在 ISA_BITFIELD 中的 nonpointer,存储在类对象的 isa 字段中
// Define SUPPORT_NONPOINTER_ISA=1 on any platform that may store something
// in the isa field that is not a raw pointer.
#if !SUPPORT_INDEXED_ISA && !SUPPORT_PACKED_ISA
# define SUPPORT_NONPOINTER_ISA 0
#else
# define SUPPORT_NONPOINTER_ISA 1
#endif
标号指针(: 1)
标号指针的标志位存储在最低位 bit。标号指针是一个伪指针,id 变量中不存储内存地址值,存储对象的值、类索引信息和其它标号信息。
标号指针是一个伪指针,是一个INDEX对象(32位系统中存储在 objc_indexed_classes 中的和64位中存储在 objc_tag_ext_classes 和 objc_tag_classes 中的)。标号指针是一个伪指针,是真真切切的对象,是在常规指针的空间内存储对象的机制。
用指针寻址对象十分灵活(实例对象的指针寻址实例对象,isa 指针寻址类对象或元类对象),同时中间层也消耗非常多的系统资源。在 32 位系统中,指针中间层引入的消耗对于大多数数据类型是合理的可以接受的,如对于 int 类型的 NSNumber来说,指针占4字节、isa 4字节、数据 4 字节,一共12字节,有4字节的浪费。对于 64 系统,中间层引入的消耗有时是不必要的,指针 8 字节、isa 8 字节 、数据 8 字节,一共 24 字节,64 位系统比 32 位系统多浪费12字节。
苹果在 iPhone5s 搭载的 64 位系统中提出 Tagged Pointer 概念用以优化小对象(如NSNumber
和NSDate
)对内存的使用方式:把类对象的索引信息、信息和其它标号信息直接存储原来的指针变量中,其中最低位赋值为1,用以标记当前指针是一个标号指针。彻底消除指针间接层的浪费、小值域类型对多余二进制位的浪费、对象指针编码浪费。
NSNumber *num2 = @(16);
(lldb) p num2
(__NSCFNumber *) $4 = 0x97f9bc34e0749a27 (int)16
(lldb) x num2
error: memory read failed for 0x97f9bc34e0749a00
static inline bool
_objc_isTaggedPointer(const void * _Nullable ptr)
{
return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}
objc_object::getIsa()
{
if (!isTaggedPointer()) return ISA(); //非
uintptr_t ptr = (uintptr_t)this;
if (isExtTaggedPointer()) {//256 个扩展标号位
uintptr_t slot =
(ptr >> _OBJC_TAG_EXT_SLOT_SHIFT) & _OBJC_TAG_EXT_SLOT_MASK;
return objc_tag_ext_classes[slot];
} else {//7 个扩展标号位
uintptr_t slot =
(ptr >> _OBJC_TAG_SLOT_SHIFT) & _OBJC_TAG_SLOT_MASK;
return objc_tag_classes[slot];
}
}
关联存储(has_assoc: 1)
类对象在注册到运行时系统后,对象的内存模型就再无法变更。关联存储是运行时提供的动态修改对象关系图的强大机制,补充对象内存模型的在可扩展性方面的不足。has_assoc 是一个标志位,用来标志当前对象是否有关联的对象。
关联对象通过当前对象的隐藏指针,把关联的对象存储到一个全局的哈希表中,使用 5 种关联策略动态地构建对象图
AssociationsHashMap &associations(manager.associations());
disguised_ptr_t disguised_object = DISGUISE(object);
objc_AssociationPolicy modifier
OBJC_ASSOCIATION_ASSIGN assign
OBJC_ASSOCIATION_RETAIN_NONATOMIC nonatomic, strong
OBJC_ASSOCIATION_COPY_NONATOMIC nonatomic, copy
OBJC_ASSOCIATION_RETAIN atomic, strong
OBJC_ASSOCIATION_COPY atomic, copy
void *objc_destructInstance(id obj)
{
if (obj) {
// Read all of the flags at once for performance.
bool cxx = obj->hasCxxDtor();
bool assoc = obj->hasAssociatedObjects();
// This order is important.
if (cxx) object_cxxDestruct(obj);
if (assoc) _object_remove_assocations(obj);
obj->clearDeallocating();
}
return obj;
}
在对象析构过程中,如果检测到 has_assoc == 1 ,会解除通过关联对象而构建起来的对象图。
关联对象不支持 weak
析构函数(has_cxx_dtor: 1)
在 ARC 下对象的 has_cxx_dtor === 1 ,编译器自动插入 .cxx_destructor 析构函数。当前对象通过 dealloc 析构时会通过 objc_destructInstance 调用 object_cxxDestructFromClass 按从子类到父类的顺序调用 .cxx_destructor, 释放关联的实例对象。
static void object_cxxDestructFromClass(id obj, Class cls)
{
void (*dtor)(id);
// Call cls's dtor first, then superclasses's dtors.
for ( ; cls; cls = cls->superclass) {
if (!cls->hasCxxDtor()) return;
dtor = (void(*)(id))
lookupMethodInClassAndLoadCache(cls, SEL_cxx_destruct);
if (dtor != (void(*)(id))_objc_msgForward_impcache) {
if (PrintCxxCtors) {
_objc_inform("CXX: calling C++ destructors for class %s",
cls->nameForLogging());
}
(*dtor)(obj);
}
}
}
编译器为ARC代码自动插入 .cxx_construct 和 .cxx_destructor ,在非 ARC 下只用于 C++对象的构建和析构
类对象指针(shiftcls)
可以通过 (0x001d800100001355 & ISA_MASK)>>>3 得到类对象指针或类对象索引。
在 arm64 架构上,shiftcls 占用 33 位;在 x86_64 架构上,shiftcls 占用 44 位;其它架构上,shiftcls 占用 15 位 。如果是扩展标号指针占用 8 bit ,常规标号指针占用 4 bit,节省下来的 bit 用于存储更多的数据。
类对象指针(magic )
在 arm64 架构上,magic 占用 6 位,magic == 65 || 0;
在 x86_64 架构上,magic 占用 6 位,magic == 65|| 0;
其它架构上,magic 占用 4 位,magic == 15 || 0。
固定值,用于标志当前对象是否初始化。
这里的 magic 与 自动释放池的 struct magic_t 没有关系
弱引用标志(weakly_referenced: 1)
当给 weak 属性赋值时,系统生成的 setter 会自定调用 setWeaklyReferenced_nolock 设置被弱引用的对象 isa 的 weakly_referenced bit ,标志属性对象被其它对象引用了,用以在析构的的时候自动清除相关对象上的弱属性为 nil。
inline void
objc_object::setWeaklyReferenced_nolock()
{
retry:
isa_t oldisa = LoadExclusive(&isa.bits);
isa_t newisa = oldisa;
if (slowpath(!newisa.nonpointer)) {
ClearExclusive(&isa.bits);
sidetable_setWeaklyReferenced_nolock();
//nonpointer == 0 时,即没有指针优化时,弱引用标号存储在边表上
return;
}
if (newisa.weakly_referenced) {
ClearExclusive(&isa.bits);
return;
}
newisa.weakly_referenced = true;//设置弱引用标志位在
if (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)) goto retry;
}
void
objc_object::sidetable_setWeaklyReferenced_nolock()
{
#if SUPPORT_NONPOINTER_ISA
assert(!isa.nonpointer);
#endif
SideTable& table = SideTables()[this];
table.refcnts[this] |= SIDE_TABLE_WEAKLY_REFERENCED;
}
在引用计数超过 extra_rc 的存储能力时,引用计数会转移部分值到边表中的哈希表 refcnts 存储。最低的两位即保留用来标志当前对象是否被弱引用、是否正在被析构。引用计数是以 4 为一个单位存储在边表的哈希表 refcnts 中。
#define SIDE_TABLE_WEAKLY_REFERENCED (1UL<<0)
#define SIDE_TABLE_DEALLOCATING (1UL<<1) // MSB-ward of weak bit
#define SIDE_TABLE_RC_ONE (1UL<<2) // MSB-ward of deallocating bit
struct SideTable {
spinlock_t slock;
RefcountMap refcnts;
weak_table_t weak_table;
...
}
析构标志(deallocating: 1)
请参考弱引用标志(weakly_referenced: 1)
引用计数溢出存储(has_sidetable_rc: 1)
在指针优化下,引用计数溢出直接存储在 isa 的 has_sidetable_rc bit 中即可,否则引用计数直接存储在边表的引用计数哈希表中,不需要标志引用计数是否溢出。
ALWAYS_INLINE id
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
....
do {
transcribeToSideTable = false;
oldisa = LoadExclusive(&isa.bits);
newisa = oldisa;
if (slowpath(!newisa.nonpointer)) {//非优化指针,不需要标记引用计数是否溢出
ClearExclusive(&isa.bits);
if (!tryRetain && sideTableLocked) sidetable_unlock();
if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
else return sidetable_retain();
}
...
newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); // extra_rc++
if (slowpath(carry)) {//引用计数 extra_rc溢出,标记溢出标志
...
transcribeToSideTable = true;
newisa.extra_rc = RC_HALF;
newisa.has_sidetable_rc = true;
}
} while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));
if (slowpath(transcribeToSideTable)) {
// Copy the other half of the retain counts to the side table.
sidetable_addExtraRC_nolock(RC_HALF);//转移部分计数到边表的哈希表中
}
...
return (id)this;
}
局部引用计数器(extra_rc)
请参考引用计数溢出存储(has_sidetable_rc: 1)
类内存结构分析
类对象存储类的元信息,即实例变量信息、属性信息、方法信息、协议信息等。
在类对象的内存结构中,isa 存储了到原类的指针,其首地址 0x100001350 就存储在对象内存结构
struct objc_class : objc_object {
// Class ISA; //占用8字节
Class superclass; //占用8字节
cache_t cache; // formerly cache pointer and vtable 占用16字节
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
...
}
(lldb) x/4gx NSObject1.class / x/6gx obj.class
0x100001350: 0x001d800100001329 0x0000000100afd140
0x100001360: 0x0000000100f074d0 0x0000000400000007
0x100001370: 0x0000000101716834 0x0000000100afd0f0
0x100001350 == $44 == ****0x0000000100001350 ,上面计算的类对象内存地址是正确的。
0x001d800100001329 是 isa 值,存储元类对象地址和类对象相关的各种信息,如extra_rc、类地址等。详细信息可参考 isa bit 结构分析。
(lldb) po 0x0000000100afd140
NSObject
0x0000000100afd140 存储 superclass 指针
(lldb) p (cache_t *)0x100001360
(cache_t *) $33 = 0x0000000100001360
(lldb) p *$33
(cache_t) $34 = {
_buckets = 0x0000000100f074d0
_mask = 7
_occupied = 4
}
0x100001360: 0x0000000100f074d0 0x0000000400000007 存储 cache_t ,方法缓存。
下面 0x100001370: 0x0000000101716834 0x0000000100afd0f0 是 class_rw_t 内存结构
(lldb) p (class_data_bits_t *)0x100001370
(class_data_bits_t *) $26 = 0x0000000100001370
(lldb) p $26->data()
(class_rw_t *) $27 = 0x0000000101716830
(lldb) p *$27
(class_rw_t) $28 = {
flags = 2148139008
version = 0
ro = 0x0000000100001100
methods = {
list_array_tt<method_t, method_list_t> = {
= {
list = 0x0000000100001148
arrayAndFlag = 4294971720
}
}
}
properties = {
list_array_tt<property_t, property_list_t> = {
= {
list = 0x00000001000011c0
arrayAndFlag = 4294971840
}
}
}
protocols = {
list_array_tt<unsigned long, protocol_list_t> = {
= {
list = 0x0000000000000000
arrayAndFlag = 0
}
}
}
firstSubclass = nil
nextSiblingClass = NSDate
demangledName = 0x0000000100000ea6 "NSObject1"
}
class_ro_t 内存结构
(lldb) p (class_ro_t *)0x0000000101716838
(class_ro_t *) $39 = 0x0000000101716838
(lldb) p *$39
(class_ro_t) $42 = {
flags = 4352
instanceStart = 1
instanceSize = 4424
reserved = 1
ivarLayout = 0x00000001000011c0 "\x10"
name = 0x0000000000000000 <no value available>
baseMethodList = 0x0000000000000000
baseProtocols = 0x00007fffa34dfea8
ivars = 0x0000000100000ea6
weakIvarLayout = 0xbac3bac304bfc34c <no value available>
baseProperties = 0x4000000010171600
}
结论
本文从 OC 向 C 和 C++ 的映射入手,回答了什么是 OC 对象,较详细的分析了 isa 指针的各个 bit 的含义及其在运行时的行为,使得 OC 底层的原理中的部分清晰的展现出来。同时,我们打印出类对象的内存结构,使我们对类对象有了一个初步感性的认识,关于类对象的详情分析请参考下一片文章《Objective-C 原理-二类对象元素管理》,其中详细阐述实例变量、属性、实例方法、类方法、协议、扩展、类别、类属性等类元素是如何管理的。