iOS runtime相关02

object内存结构

895cbccf7d91751559a3fa81d74f8bfc

关联对象

举一个简单的例子来说明关联对象在内存中以什么形式存储的,以下面的代码为例:

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”) 在内存中是这么存储的:

e6769e39ba707f45fb1c436086b7948b

这个哈希表中的键为disguised_ptr_t,在得到这个指针的时候,源码中执行了DISGUISE方法,这个方法的功能是获得指向self地址的指针,即为指向对象地址的指针。通过地址这个唯一标识,可以找到对应的value,即一个子哈希表 关联对象又是如何实现并且管理的呢:

  • 关联对象其实就是 ObjcAssociation 对象
  • 关联对象由 AssociationsManager(全局静态变量) 管理并在 AssociationsHashMap 存储
  • 对象的指针以及其对应 ObjectAssociationMap 以键值对的形式存储在 AssociationsHashMap 中
  • ObjectAssociationMap 则是用于存储关联对象的数据结构 ( 这个枚举类型一般叫做policy)
  • 每一个对象都有一个标记位 has_assoc 指示对象是否含有关联对象

流程:

  1. 使用 old_association(0, nil) 创建一个临时的 ObjcAssociation 对象(用于持有原有的关联对象,方便在方法调用的最后释放值)
  2. 调用 acquireValue 对 new_value 进行 retain 或者 copy
  3. 初始化一个 AssociationsManager,并获取唯一的保存关联对象的哈希表 AssociationsHashMap
  4. 先使用 DISGUISE(object) 作为 key 寻找对应的 ObjectAssociationMap
  5. 如果没有找到,初始化一个 ObjectAssociationMap,再实例化 ObjcAssociation 对象添加到 Map 中,并调用 setHasAssociatedObjects 方法,表明当前对象含有关联对象
  6. 如果找到了对应的 ObjectAssociationMap,就要看 key 是否存在了,由此来决定是更新原有的关联对象,还是增加一个
  7. 如果原来的关联对象有值的话,会调用 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指针和引用计数存储

a4d51d8af85614fd7dc3fd2b2dba55e5

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;
    };
};

7838978b03e038bbdea49d7e1bba6460 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。

d1c9912d98f5311ab4bce4a1da98abcc

  1. 如果有些对象支持使用 TaggedPointer:
    • 苹果会直接将对象的指针值作为引用计数返回。
  2. 如果另外一些对象不支持使用 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]; 
};
  1. mask:可以认为是当前能达到的最大index(从0开始的),所以缓存的size(total)是mask+1
  2. occupied:被占用的槽位,因为缓存是以散列表的形式存在的,所以会有空槽,而occupied表示当前被占用的数目
  3. buckets:用数组表示的hash表,cache_entry类型,每一个cache_entry代表一个方法缓存

40b6a258f72ba817d759fe305161c6c1

类的缓存是用的散列表,类的方法列表用的是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

c4352295a0c252e321200257f3791a0a d3f1de1932c7d9b91ef7665f39f10791

当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补充

4295ca18f23d0cb1a2ae6d3d85c5401d

  • 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] 位,所以可以使用最后三位来存储关于当前类的其他信息: 47419422badeb68d2e6507107890644a

#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 * 指针:

  1. 从 class_data_bits_t 调用 data 方法,将结果从 class_ro_t 强制转换为 class_rw_t 指针
  2. 初始化一个 class_rw_t 结构体
  3. 设置结构体 ro 的值以及 flag
  4. 最后设置正确的 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. 首先从缓存中查找,如果当前类没有找到,当前类的方法列表里找
  2. 如果步骤1没有找到对应的IMP,在父类的缓存和方法列表中查找,根据isa指针,直到查找到NSObject为止
  3. 如果步骤2也没有找到,转到_class_lookupMethodAndLoadCache3,调用lookUpImpOrForward继续查找并进入消息转发流程
  4. _class_resolveMethod调用_class_resolveInstanceMethod查看当前类是否实现了resolveInstanceMethod方法(对于类方法,对应的是resolveClassMethod),如果实现了,resolved置true,重新执行一次lookUpImpOrNil流程,将新的IMP加入缓存并执行 (方法)。
  5. 先调用 forwardingTargetForSelector 方法获取新的 target 作为 receiver 重新执行 selector,如果返回的内容不合法(为 nil 或者跟旧 receiver 一样),那就进入第二步。
  6. 调用 methodSignatureForSelector 获取方法签名后,判断返回类型信息是否正确,再调用 forwardingTargetForSelector 执行 NSInvocation 对象,并将结果返回。如果对象没实现 methodSignatureForSelector 方法,进入第三步。
  7. 调用 doesNotRecognizeSelector 方法。

或者

  1. 缓存命中
  2. 查找当前类的缓存及方法
  3. 查找父类的缓存及方法
  4. 方法决议
  5. 消息转发

90bdf5d684696d583f0852443fdd244e

Tagged Pointer

Tagged Pointer 是一种特殊标记的对象,通过在其最后一个 bit 位设置为特殊标记位,并将数据直接保存在指针自身中。 Tagged Pointer 的思路,可以将低位设置为 1 加以区分。

c366530946ac89d3eb1bf68826cde381

并且在最低位之后的 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 对象的表示如下 591abf1aee9290cd5039dd1ccf89fd82

OBJC_TAG_7 类型的 Tagged Pointer 是个例外,它可以将后 8 位作为扩展字段,基于此我们可以多支持 256 种类型的 Tagged Pointer,如 UIColors 或 NSIndexSets 之类的对象。 119e20b440864a9e14630fd4dd1a3978

在 ARM64 中表现会不太一样: be0508da0506b697c3d692352eb5de9c

最高位代表 Tagged Pointer 标识位,次 3 位标识 Tagged Pointer 的类型,接下去的位来表示包含的数据(可能包含扩展类型字段)。 那么在 ARM64 中,为什么要用最高位代表的 Tagged Pointer 标记,而不是像 Intel 一样使用低位标记? 它实际是对 objc_msgSend 的微小优化。我们希望 objc_msgSend 检索的时间尽可能快,这个时间开销是出现在 objc_msgSend 查找指针的一种 Corner Case 上,即对比 Tagged Pointer 指针和 nil。当标记在最高位时,可以通过复杂度 的比较直接完成,无形之中节省了一次遍历的时间。