Objc源码之引用计数实现

Objc源码之对象创建alloc和init
Objc源码之initialize实现
Objc源码之Load方法实现
Objc源码之NSObject和isa
Objc源码之引用计数实现
objc源码之Method消息发送

前言

   我们都知道OC的内存管理是引用计数,可是对象的引用计数是存储在哪里的呢?今天我们就从源码入手,来揭开这个谜底,下面我们就从对象创建开始说起。

一、从alloc和retainCount引用计数

首先我们看下retainCount方法,这个是获取引用计数的方法:

- (NSUInteger)retainCount {
    return ((id)self)->rootRetainCount();
}

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

看到这里,我们会发现引用计数包含三部分:
1. isTaggedPointer,是否是Tagged Pointer类型
2. nonpointer 是否是优化的isa指针。
3. sidetable_retainCount 其它类型

1.isTaggedPointer的情况

Tagged Pointer是苹果在64位系统之后,用来优化内存的。
1.Tagged Pointer专门用来存储小的对象,例如NSString、NSNumber、NSDate等;
2.Tagged Pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要malloc和free;
3.在内存读取上有着3倍的效率,创建时比以前快106倍。
具体可以看看这个iOS Tagged Pointer (源码阅读必备知识) - 掘金

总结就是isTaggedPointer情况下引用计数返回的是对象本身。

2.nonpointer优化的情况

nonpointer表示是否开启指针优化,

  • 0表示isa_t没有开启指针优化,不使用isa_t中定义的结构体。
  • 1表示isa_t开启指针优化,不能直接访问objc_object的isa成员变量 ,isa中包含了类信息、对象的引用计数等信息。

要了解nonpointer,首先看alloc是怎么创建一个对象,其中nonpointerextra_rc都是走嗯么赋值的,下面是简化的alloc函数的调用过程:

+ (id)alloc {
    return _objc_rootAlloc(self);
}

id  _objc_rootAlloc(Class cls)
{
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}

static ALWAYS_INLINE id callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
      ...
      id obj = class_createInstance(cls, 0);
      return obj;
      ...
}

id class_createInstance(Class cls, size_t extraBytes)
{
    return _class_createInstanceFromZone(cls, extraBytes, nil);
}

static __attribute__((always_inline)) id _class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone, 
                              bool cxxConstruct = true, 
                              size_t *outAllocatedSize = nil)
{
        ...
        obj = (id)calloc(1, size);
        if (!obj) return nil;
        obj->initInstanceIsa(cls, hasCxxDtor);
        return obj;
}

inline void objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
    initIsa(cls, true, hasCxxDtor);
}

inline void objc_object::initIsa(Class cls, bool nonpointer, bool hasCxxDtor) 
{ 
        ...
        newisa.bits = ISA_MAGIC_VALUE;
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.shiftcls = (uintptr_t)cls >> 3;
        isa = newisa;
}

#   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

我们总结一下调用过程alloc->_objc_rootAlloc->callAlloc->class_createInstance->_class_createInstanceFromZone->initIsa
关于引用计数的部分,在最后初始化isa指针中initIsa,下面我们看下initIsa中的具体过程:

newisa.bits = ISA_MAGIC_VALUE;

在给bits赋值的时候,通过ISA_MAGIC_VALUE这个宏来赋值的,arm64下这个宏的值是0x000001a000000001ULL,通过ISA_BITFIELD,我们来看下具体含义:

#   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

extra_rc代表的就是对象的引用计数,它包含19位,nonpointer只包含一位,下面我们看下arm64下ISA_MAGIC_VALUE:

ISA_MAGIC_VALUE

紫色部分是 extra_rc的值,我们看到 extra_rc的值是0,我们知道,刚创建的对象,应用计数应该是1,但是这里怎么是0,那是因为 extra_rc保存的是引用计数-1的值,在获取的时候,会进行+1.在arm64下nonpointer默认是1,默认开启isa指针优化的。

nonpointer情况下,我们可以看到rootRetainCount代码包含两部分:

    if (bits.nonpointer) {
        uintptr_t rc = 1 + bits.extra_rc;
        if (bits.has_sidetable_rc) {
            rc += sidetable_getExtraRC_nolock();
        }
        sidetable_unlock();
        return rc;
    }

第一部分就是上面说的extra_rc,获取到extra_rc以后,在又会判断has_sidetable_rc的值。
has_sidetable_rc是用来做什么的?
has_sidetable_rc是用来判断extra_rc是否存储不下引用计数数量的,如果引用计数大于extra_rc的最大存储数量,那么就会超出的引用计数存储到SideTables,下面看下源码:

size_t objc_object::sidetable_getExtraRC_nolock()
{
    assert(isa.nonpointer);
    SideTable& table = SideTables()[this];
    RefcountMap::iterator it = table.refcnts.find(this);
    if (it == table.refcnts.end()) return 0;
    else return it->second >> SIDE_TABLE_RC_SHIFT;
}

了解weak原理的话,应该会熟悉SideTables,这也是weak对象存储的地方,上面的代码是取的SideTablesrefcnts中以对象为key的值,这里面refcnts是一个哈希表。

struct SideTable {
    spinlock_t slock;
    RefcountMap refcnts;
    weak_table_t weak_table;
}

typedef objc::DenseMap,size_t,true> RefcountMap;

总结:如果extra_rc能够存储下引用计数,就只使用extra_rc,如果存储不下,就会使用SideTables进行辅助存储。

3.sidetable_retainCount的情况

sidetable_retainCount是在没有开启指针优化的情况,对象的引用计数直接存储在SideTable中。

uintptr_t objc_object::sidetable_retainCount()
{
    SideTable& table = SideTables()[this];

    size_t refcnt_result = 1;
    
    table.lock();
    RefcountMap::iterator it = table.refcnts.find(this);
    if (it != table.refcnts.end()) {
        // this is valid for SIDE_TABLE_RC_PINNED too
        refcnt_result += it->second >> SIDE_TABLE_RC_SHIFT;
    }
    table.unlock();
    return refcnt_result;
}

二、总结:

1. Tagged Pointer情况,直接返回对象指针
2. 优化的isa指针,也就是nonpointer为1时,通过extra_rc和SideTables一起来管理引用计数。
3.未开启指针优化,也就是nonpointer为0时,直接使用SideTables来管理引用计数 。

参考:
objc4-750源码
OC内存管理--引用计数器
【译】采用Tagged Pointer的字符串
从 NSObject 的初始化了解 isa.md
isa详解

你可能感兴趣的:(Objc源码之引用计数实现)