runtime源码探究(二)strong的实现

本文深入探讨Objective-C runtime中的`objc_storeStrong`、`objc_retain`和`objc_release`函数,详细解析引用计数的增减过程,以及Tagged Pointer在内存管理中的应用。通过对源码的分析,揭示了对象引用计数存储的细节和优化策略。

objc_storeStrong

oc中用strong修饰一个对象,实际上是调用了

void objc_storeStrong(id *location, id obj)

函数,location是引用对象的指针的地址,obj是对象本身。下面是该函数的完整实现:

void objc_storeStrong(id *location, id obj)
{
    id prev = *location;
    if (obj == prev) {
        return;
    }
    objc_retain(obj);
    *location = obj;
    objc_release(prev);
}

实现很简单,首先和之前的引用相比判断是不是同一个引用,是的话就return;否则的话就对obj对象进行retain,并且释放*location之前的引用(也就是说*location指针不再指向之前的对象,要把之前对象引用计数减1)。

objc_retain

objc_retain函数主要是对对象引用计数加1,下面来看objc_retain函数的实现。objc_retain函数调用了id objc_object::sidetable_retain()函数,该函数的实现如下:

id objc_object::sidetable_retain()
{
#if SUPPORT_NONPOINTER_ISA
    assert(!isa.nonpointer);
#endif
    SideTable& table = SideTables()[this];

    table.lock();
    size_t& refcntStorage = table.refcnts[this];
    if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
        refcntStorage += SIDE_TABLE_RC_ONE;
    }
    table.unlock();

    return (id)this;
}

这个函数的实现也很简单,首先从SideTable中取出当前对象的引用计数信息refcntStorage,并且做了非负判断。再来看

 refcntStorage += SIDE_TABLE_RC_ONE;

这一行,SIDE_TABLE_RC_ONE的定义如下:

#define SIDE_TABLE_RC_ONE            (1UL<<2)  // MSB-ward of deallocating bit

为什么refcntStorage加了个4呢?个人推测苹果可能是出于节省空间的考虑,把对象的一些信息都存储到了refcntStorage上,看下面这些宏定义:

// The order of these bits is important.
#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
#define SIDE_TABLE_RC_PINNED         (1UL<<(WORD_BITS-1))

#define SIDE_TABLE_RC_SHIFT 2
#define SIDE_TABLE_FLAG_MASK (SIDE_TABLE_RC_ONE-1)

可以推断出refcntStorage的第一位存储了对象的弱引用信息,第二位用来标记对象是否正在被dealloc,第三位开始才真正存储了对象的引用计数。

objc_release

objc_release函数对对象的引用计数进行减一,里面调用了uintptr_t
objc_object::sidetable_release(bool performDealloc)
函数。再来看该函数的实现:

uintptr_t
objc_object::sidetable_release(bool performDealloc)
{
#if SUPPORT_NONPOINTER_ISA
    assert(!isa.nonpointer);
#endif
    SideTable& table = SideTables()[this];

    bool do_dealloc = false;

    table.lock();
    RefcountMap::iterator it = table.refcnts.find(this);
    if (it == table.refcnts.end()) {
        do_dealloc = true;
        table.refcnts[this] = SIDE_TABLE_DEALLOCATING;
    } else if (it->second < SIDE_TABLE_DEALLOCATING) {
        // SIDE_TABLE_WEAKLY_REFERENCED may be set. Don't change it.
        do_dealloc = true;
        it->second |= SIDE_TABLE_DEALLOCATING;
    } else if (! (it->second & SIDE_TABLE_RC_PINNED)) {
        it->second -= SIDE_TABLE_RC_ONE;
    }
    table.unlock();
    if (do_dealloc  &&  performDealloc) {
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
    }
    return do_dealloc;
}

首先判断该SideTable是否包含该对象(迭代器it==refcnts.end()就证明map里面没有这个对象),如果没有,那么把这个对象标记为正在被dealloc;否则的话判断该对象是否没有引用,如果是的话,那么也把这个对象标记为正在被dealloc;如果两者都不是,那么就取出引用计数信息减1(通过减SIDE_TABLE_RC_ONE来实现第三位开始减1)。接下来如果发现这个对象需要被dealloc,那么直接执行对象的dealloc函数。
这里其实有一个疑问,因为根据我们的了解,当对象的引用计数为0的时候,会自动被dealloc,那么it->second -= SIDE_TABLE_RC_ONE; 的结果就有可能是0,但是并没有这样的逻辑判断。其实apple在存储对象的引用计数信息的时候,保存的是其真正的引用计数减1,所以sidetable_release 函数能够正确执行。

Tagged Pointer和引用计数

Tagged Pointer是苹果为了优化内存管理而设计的一种技术,简单来说就是当对象的信息比较简单,内容较少,而64位平台每个指针的长度为64位,每个bit位不一定都会被使用,造成内存浪费,这时apple会使用指针的bit位来存储对象的一些信息。
那么如何确认apple是如何存储引用计数的呢,我们可以从对象的结构中寻找线索。

struct objc_object {
private:
    isa_t isa;

public:
xxxx
xxxx
xxxx

对象中有一个isa_t类型的对象,isa是一个联合体,其中包含了这样的一组定义:

# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
    struct {
        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)
    };

我们可以看到,当使用Tagged Pointer时候,从指针的第45位开始,一共19位,存储的都是对象的引用计数信息,标志为extra_rc。当存储空间不够时,苹果会使用对象的sidetable来存储对象的引用计数信息。这个过程实现如下:

// Move some retain counts to the side table from the isa field.
// Returns true if the object is now pinned.
bool objc_object::sidetable_addExtraRC_nolock(size_t delta_rc)
{
    assert(isa.nonpointer);
    SideTable& table = SideTables()[this];

    size_t& refcntStorage = table.refcnts[this];
    size_t oldRefcnt = refcntStorage;
    // isa-side bits should not be set here
    assert((oldRefcnt & SIDE_TABLE_DEALLOCATING) == 0);
    assert((oldRefcnt & SIDE_TABLE_WEAKLY_REFERENCED) == 0);

    if (oldRefcnt & SIDE_TABLE_RC_PINNED) return true;

    uintptr_t carry;
    size_t newRefcnt = 
        addc(oldRefcnt, delta_rc << SIDE_TABLE_RC_SHIFT, 0, &carry);
    if (carry) {
        refcntStorage =
            SIDE_TABLE_RC_PINNED | (oldRefcnt & SIDE_TABLE_FLAG_MASK);
        return true;
    }
    else {
        refcntStorage = newRefcnt;
        return false;
    }
}

这段代码清楚的描述了引用计数信息是如何从指针中转移到sidetable里面的


#define SIDE_TABLE_RC_PINNED    (1UL<<(WORD_BITS-1))
#define SIDE_TABLE_FLAG_MASK (SIDE_TABLE_RC_ONE-1)
refcntStorage =
            SIDE_TABLE_RC_PINNED | (oldRefcnt & SIDE_TABLE_FLAG_MASK);

通过这段代码,apple把存储引用计数信息的refcntStorage变量的第32位标记为计数溢出位(也就是计数数值只能为正整数),并且将正真的引用计数减一。

另外当我们获取引用计数的时候,调用了如下函数:

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,这也验证了我们上面的说法。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值