object内存结构
关联对象
举一个简单的例子来说明关联对象在内存中以什么形式存储的,以下面的代码为例:
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *obj = [NSObject new];
objc_setAssociatedObject(obj, @selector(hello), @"Hello", OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
return 0;
}
调用栈:
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
└── void objc_setAssociatedObject_non_gc(id object, const void *key, id value, objc_AssociationPolicy policy)
└── void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy)
这里的关联对象 ObjcAssociation(OBJC_ASSOCIATION_RETAIN_NONATOMIC, @”Hello”) 在内存中是这么存储的:
这个哈希表中的键为disguised_ptr_t,在得到这个指针的时候,源码中执行了DISGUISE方法,这个方法的功能是获得指向self地址的指针,即为指向对象地址的指针。通过地址这个唯一标识,可以找到对应的value,即一个子哈希表 关联对象又是如何实现并且管理的呢:
- 关联对象其实就是 ObjcAssociation 对象
- 关联对象由 AssociationsManager(全局静态变量) 管理并在 AssociationsHashMap 存储
- 对象的指针以及其对应 ObjectAssociationMap 以键值对的形式存储在 AssociationsHashMap 中
- ObjectAssociationMap 则是用于存储关联对象的数据结构 ( 这个枚举类型一般叫做policy)
- 每一个对象都有一个标记位 has_assoc 指示对象是否含有关联对象
流程:
- 使用 old_association(0, nil) 创建一个临时的 ObjcAssociation 对象(用于持有原有的关联对象,方便在方法调用的最后释放值)
- 调用 acquireValue 对 new_value 进行 retain 或者 copy
- 初始化一个 AssociationsManager,并获取唯一的保存关联对象的哈希表 AssociationsHashMap
- 先使用 DISGUISE(object) 作为 key 寻找对应的 ObjectAssociationMap
- 如果没有找到,初始化一个 ObjectAssociationMap,再实例化 ObjcAssociation 对象添加到 Map 中,并调用 setHasAssociatedObjects 方法,表明当前对象含有关联对象
- 如果找到了对应的 ObjectAssociationMap,就要看 key 是否存在了,由此来决定是更新原有的关联对象,还是增加一个
- 如果原来的关联对象有值的话,会调用 ReleaseValue() 释放关联对象的值
[get类型,查看文档](https://draveness.me/ao/)
setHasAssociatedObjects()它会将 isa 结构体中的标记位 has_assoc 标记为 true,也就是表示当前对象有关联对象
inline void objc_object::setHasAssociatedObjects() {
if (isTaggedPointer()) return;
retry:
isa_t oldisa = LoadExclusive(&isa.bits);
isa_t newisa = oldisa;
if (!newisa.indexed) return;
if (newisa.has_assoc) return;
newisa.has_assoc = true;
if (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)) goto retry;
}
在Runtime的销毁对象函数objc_destructInstance里面会判断这个对象有没有关联对象,如果有,会调用_object_remove_assocations做关联对象的清理工作。
isa指针和引用计数存储
struct objc_object {
isa_t isa;
};
struct objc_class : objc_object {
isa_t isa;
Class superclass;
cache_t cache;
class_data_bits_t bits;
};
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
struct {
uintptr_t indexed : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 44;
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 8;
};
};
extra_rc 就是用于保存自动引用计数的标志位
分析下位域中存储的每个数据
- nonpointer :表示是否对 isa 指针开启指针优化。0:纯isa指针,1:不止是类对象地址,isa 中包含了类信息、对象的引用计数等。
- has_assoc:关联对象标志位,0没有,1存在
- has_cxx_dtor:该对象是否有 C++ 或者 Objc 的析构器,如果有析构函数,则需要做析构逻辑,如果没有,则可以更快的释放对象
- shiftcls:存储类指针的值。开启指针优化的情况下,在 arm64 架构中有 33 位用来存储类指针。
- magic:用于调试器判断当前对象是真的对象还是没有初始化的空间。
- weakly_referenced:标志对象是否被指向或者曾经指向一个 ARC 的弱变量,没有弱引用的对象可以更快释放。
- deallocating:标志对象是否正在释放内存
- has_sidetable_rc:当对象引用技术大于 10 时,则需要借用该变量存储进位
- extra_rc:当表示该对象的引用计数值,实际上是引用计数值减 1, 例如,如果对象的引用计数为 10,那么 extra_rc 为 9。如果引用计数大于 10, 则需要使用到下面的 has_sidetable_rc。
- 如果有些对象支持使用 TaggedPointer:
- 苹果会直接将对象的指针值作为引用计数返回。
- 如果另外一些对象不支持使用 TaggedPointer:
- 如果当前设备是 64 位环境并且使用 Objective-C 2.0,那么会使用对象的 isa 指针 的 一部分空间 (bits.extra_rc)来存储它的引用计数;
- 否则 Runtime 会使用一张 散列表 (SideTables())来管理引用计数。
inline uintptr_t
objc_object::rootRetainCount()
{
if (isTaggedPointer()) return (uintptr_t)this;
sidetable_lock();
isa_t bits = LoadExclusive(&isa.bits);
ClearExclusive(&isa.bits);
if (bits.nonpointer) {
uintptr_t rc = 1 + bits.extra_rc;
if (bits.has_sidetable_rc) {
rc += sidetable_getExtraRC_nolock();
}
sidetable_unlock();
return rc;
}
sidetable_unlock();
return sidetable_retainCount();
}
- 情况0 – TaggedPointer, 直接返回isa值本身
- 情况1 – 非TaggedPointer,且开启了指针优化,且存储在extra_rc中
- 情况2 – 非TaggedPointer,且没有开启指针优化,且存储在散列表中
方法缓存
struct objc_method_list {
struct objc_method_list *obsolete;
int method_count;
#ifdef __LP64__
int space;
#endif
/* variable length structure */
struct objc_method method_list[1];
};
struct objc_method {
SEL method_name;
char *method_types; /* a string representing argument/return types */
IMP method_imp;
};
struct objc_cache {
uintptr_t mask; /* total = mask + 1 */
uintptr_t occupied;
cache_entry *buckets[1];
};
- mask:可以认为是当前能达到的最大index(从0开始的),所以缓存的size(total)是mask+1
- occupied:被占用的槽位,因为缓存是以散列表的形式存在的,所以会有空槽,而occupied表示当前被占用的数目
- buckets:用数组表示的hash表,cache_entry类型,每一个cache_entry代表一个方法缓存
类的缓存是用的散列表,类的方法列表用的是list 为什么类的方法列表不直接做成散列表呢,做成list,还要单独缓存,多费事?
这个问题么,我觉得有以下三个原因:
- 散列表是没有顺序的,Objective-C的方法列表是一个list,是有顺序的;Objective-C在查找方法的时候会顺着list依次寻找,并且category的方法在原始方法list的前面,需要先被找到,如果直接用hash存方法,方法的顺序就没法保证。
- list的方法还保存了除了selector和imp之外其他很多属性
- 散列表是有空槽的,会浪费空间
弱引用 (补充弱引用)
弱引用weak是一个hash表,object的地址为key,weak形式指向这个对象的地址的集合(数组)为value。 文章地址:http://www.cocoachina.com/articles/18962
当weak引用指向的对象被释放时,又是如何去处理weak指针的呢?当释放对象时,其基本流程如下: 1、调用objc_release 2、因为对象的引用计数为0,所以执行dealloc 3、在dealloc中,调用了_objc_rootDealloc函数 4、在_objc_rootDealloc中,调用了object_dispose函数 5、调用objc_destructInstance 6、最后调用objc_clear_deallocating objc_clear_deallocating该函数的动作如下: 1、从weak表中获取废弃对象的地址为键值的记录 2、将包含在记录中的所有附有 weak修饰符变量的地址,赋值为nil 3、将weak表中该记录删除 4、从引用计数表中删除废弃对象的地址为键值的记录 看了objc-weak.mm的源码就明白了:其实Weak表是一个hash(哈希)表,然后里面的key是指向对象的地址,Value是Weak指针的地址的数组。
isa补充
- isa 是指向元类的指针,不了解元类的可以看 Classes and Metaclasses
- super_class 指向当前类的父类
- cache 用于缓存指针和 vtable,加速方法的调用
- bits 就是存储类的方法、属性、遵循的协议等信息的地方
在 objc_class 结构体中的注释写到 class_data_bits_t 相当于 class_rw_t 指针加上 rr/alloc 的标志。
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
它为我们提供了便捷方法用于返回其中的 class_rw_t * 指针:
class_rw_t* data() { return (class_rw_t *)(bits & FAST_DATA_MASK); }
因为 class_rw_t * 指针只存于第 [3, 47] 位,所以可以使用最后三位来存储关于当前类的其他信息:
#define FAST_IS_SWIFT (1UL<<0)
#define FAST_HAS_DEFAULT_RR (1UL<<1)
#define FAST_REQUIRES_RAW_ISA (1UL<<2)
#define FAST_DATA_MASK 0x00007ffffffffff8UL
- isSwift()
- FAST_IS_SWIFT 用于判断 Swift 类
- hasDefaultRR()
- FAST_HAS_DEFAULT_RR 当前类或者父类含有默认的 retain/release/autorelease/retainCount/_tryRetain/_isDeallocating/retainWeakReference/allowsWeakReference 方法
- requiresRawIsa()
- FAST_REQUIRES_RAW_ISA 当前类的实例需要 raw isa 执行 class_data_bits_t 结构体中的 data() 方法或者调用 objc_class 中的 data() 方法会返回同一个 class_rw_t * 指针,因为 objc_class 中的方法只是对 class_data_bits_t 中对应方法的封装。
class_rw_t 和 class_ro_t
ObjC 类中的属性、方法还有遵循的协议等信息都保存在 class_rw_t 中:
struct class_rw_t {
uint32_t flags;
uint32_t version;
const class_ro_t *ro;
method_array_t methods;
property_array_t properties;
protocol_array_t protocols;
Class firstSubclass;
Class nextSiblingClass;
};
/// 其中还有一个指向常量的指针 ro,其中存储了当前类在编译期就已经确定的属性、方法以及遵循的协议。
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
uint32_t reserved;
const uint8_t * ivarLayout;
const char * name;
method_list_t * baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
};
在编译期间类的结构中的 class_data_bits_t *data 指向的是一个 class_ro_t * 指针:
- 从 class_data_bits_t 调用 data 方法,将结果从 class_ro_t 强制转换为 class_rw_t 指针
- 初始化一个 class_rw_t 结构体
- 设置结构体 ro 的值以及 flag
- 最后设置正确的 data。
const class_ro_t *ro = (const class_ro_t *)cls->data();
class_rw_t *rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
rw->ro = ro;
rw->flags = RW_REALIZED|RW_REALIZING;
cls->setData(rw);
方法的结构
struct method_t {
SEL name;
const char *types;
IMP imp;
};
上面的 -[XXObject hello] 方法的结构体是这样的:
name = "hello"
types = 0x0000000100000fa4 "v16@0:8"
imp = 0x0000000100000e90 (method`-[XXObject hello] at XXObject.m:13
objc_msgSend
函数中的IMP查找流程比较清晰了:
- 首先从缓存中查找,如果当前类没有找到,当前类的方法列表里找
- 如果步骤1没有找到对应的IMP,在父类的缓存和方法列表中查找,根据isa指针,直到查找到NSObject为止
- 如果步骤2也没有找到,转到_class_lookupMethodAndLoadCache3,调用lookUpImpOrForward继续查找并进入消息转发流程
- _class_resolveMethod调用_class_resolveInstanceMethod查看当前类是否实现了resolveInstanceMethod方法(对于类方法,对应的是resolveClassMethod),如果实现了,resolved置true,重新执行一次lookUpImpOrNil流程,将新的IMP加入缓存并执行 (方法)。
- 先调用 forwardingTargetForSelector 方法获取新的 target 作为 receiver 重新执行 selector,如果返回的内容不合法(为 nil 或者跟旧 receiver 一样),那就进入第二步。
- 调用 methodSignatureForSelector 获取方法签名后,判断返回类型信息是否正确,再调用 forwardingTargetForSelector 执行 NSInvocation 对象,并将结果返回。如果对象没实现 methodSignatureForSelector 方法,进入第三步。
- 调用 doesNotRecognizeSelector 方法。
或者
- 缓存命中
- 查找当前类的缓存及方法
- 查找父类的缓存及方法
- 方法决议
- 消息转发
Tagged Pointer
Tagged Pointer 是一种特殊标记的对象,通过在其最后一个 bit 位设置为特殊标记位,并将数据直接保存在指针自身中。 Tagged Pointer 的思路,可以将低位设置为 1 加以区分。
并且在最低位之后的 3 位,赋予其类型意义。3 位,可以表示 7 种数据类型
OBJC_TAG_NSAtom = 0,
OBJC_TAG_1 = 1,
OBJC_TAG_NSString = 2,
OBJC_TAG_NSNumber = 3,
OBJC_TAG_NSIndexPath = 4,
OBJC_TAG_NSManagedObjectID = 5,
OBJC_TAG_NSDate = 6,
OBJC_TAG_7 = 7
在剩余的字段中,记录所包含的数据。在 Intel 的 x86 架构中,我们 Tagged Pointer 对象的表示如下
OBJC_TAG_7 类型的 Tagged Pointer 是个例外,它可以将后 8 位作为扩展字段,基于此我们可以多支持 256 种类型的 Tagged Pointer,如 UIColors 或 NSIndexSets 之类的对象。
在 ARM64 中表现会不太一样:
最高位代表 Tagged Pointer 标识位,次 3 位标识 Tagged Pointer 的类型,接下去的位来表示包含的数据(可能包含扩展类型字段)。 那么在 ARM64 中,为什么要用最高位代表的 Tagged Pointer 标记,而不是像 Intel 一样使用低位标记? 它实际是对 objc_msgSend 的微小优化。我们希望 objc_msgSend 检索的时间尽可能快,这个时间开销是出现在 objc_msgSend 查找指针的一种 Corner Case 上,即对比 Tagged Pointer 指针和 nil。当标记在最高位时,可以通过复杂度 的比较直接完成,无形之中节省了一次遍历的时间。