引用计数器

引用计数的存储策略

  1. 有些对象如果支持使用Tagged Pointer,苹果会直接将其指针值作为引用计数返回;
  2. 如果当前设备是64位环境并且使用Objective-C 2.0,那么“一些”对象会使用其isa指针的一部分空间来存储它的引用计数;
  3. 否则Runtime会使用一张散列表来管理引用计数。

Tagged Pointer

Tagged Pointer

isa指针

isa

为什么既要使用一个extra_rc又要使用SideTables

可能是因为历史问题,以前cpu是32位的,isa中能存储的引用计数就只有$2^7=128$。因此在arm64下,引用计数通常是存储在isa中的。

SideTable

SideTable

alloc实现

经过一系列调用,最终调用了C函数calloc,此时并没有设置引用计数为1

此时并没有设置引用计数为1

retain实现

SideTable &table = SideTables()[this];
//在tables里面,根据当前对象指针获取对应的sidetable

size_t &refcntStorage = table.refcnts[this];
//获得引用计数

//添加引用计数
refcntStorage += SIDE_TABLE_RC_ONE(4,位计算)

release 实现

SideTable &table = SideTables()[this];
RefcountMap::iterator it = table.refcnts.find[this];
it->second -= SIDE_TABLE_RC_ONE

retianCount

SideTable &table = SideTables()[this];
size_t refcnt_result = 1;
RefcountMap::iterator it = table.refcnts.find[this];
refcnt_result += it->second >> SIDE_TABLE_RC_SHIFT;(将向右偏移操作)

引用计数的获取

通过retainCount可以获取到引用计数器,其定义:

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

inline uintptr_t objc_object::rootRetainCount() {
    if (isTaggedPointer()) return (uintptr_t)this;

    sidetable_lock();
    // 加锁,用汇编指令ldxr来保证原子性
    isa_t bits = LoadExclusive(&isa.bits);
    // 释放锁,使用汇编指令clrex
    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();
}

//sidetable_retainCount()函数实现
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的话,直接返回isa本身;
  2. Tagged Pointer,且开启了指针优化,此时引用计数先从extra_rc中去取(这里将取出来的值进行了+1操作,所以在存的时候需要进行-1操作),接着判断是否有SideTable,如果有再加上存在SideTable中的计数;
  3. Tagged Pointer,没有开启了指针优化,使用sidetable_retainCount()函数返回。

总结

  1. 引用计数存在什么地方?

    • Tagged Pointer不需要引用计数,苹果会直接将对象的指针值作为引用计数返回;
    • 开启了指针优化(nonpointer == 1)的对象其引用计数优先存在isaextra_rc中,大于524288便存在SideTableRefcountMap或者说是DenseMap中;
    • 没有开启指针优化的对象直接存在SideTableRefcountMap或者说是DenseMap中。
  2. retain/release的实质

    • Tagged Pointer不参与retain/release
    • 找到引用计数存储区域,然后+1/-1,并根据是否开启指针优化,处理进位/借位的情况;
    • 当引用计数减为0时,调用dealloc函数。
  3. isa是什么

    // ISA() assumes this is NOT a tagged pointer object
    Class ISA();
    
    // getIsa() allows this to be a tagged pointer object
    Class getIsa();
    
    • 首先要知道,isa指针已经不一定是类指针了,所以需要用ISA()获取类指针;
    • Tagged Pointer的对象没有isa指针,有的是isa_t的结构体;
    • 其他对象的isa指针还是类指针。
  4. 对象的值是什么

    • 如果是Tagged Pointer,对象的值就是指针;
    • 如果非Tagged Pointer, 对象的值是指针指向的内存区域中的值。

补充: 一道多线程安全的题目

以下代码运行结果

@property (nonatomic, strong) NSString *target;
//....

dispatch_queue_t queue = dispatch_queue_create("parallel", DISPATCH_QUEUE_CONCURRENT);
for (int i = 0; i < 1000000 ; i++) {
    dispatch_async(queue, ^{
        self.target = [NSString stringWithFormat:@"ksddkjalkjd%d",i];
    });
}

答案:大概率地发生Crash。

Crash的原因:过度释放。

这道题看着虽然是多线程范围的,但是解题的最重要思路确是在引用计数上,更准确的来说是看对强引用的理解程度。关键知识点如下:

  1. 全局队列和自定义并行队列在异步执行的时候会根据任务系统决定开辟线程个数;
  2. target使用strong进行了修饰,Block是会截获对象的修饰符的;
  3. 即使使用_target效果也是一样,因为默认使用strong修饰符隐式修饰;
  4. strong的源代码如下:
objc_storeStrong(id *location, id obj) {
    id prev = *location;
    if (obj == prev) {
        return;
    }
    objc_retain(obj);
    *location = obj;
    objc_release(prev);
}

假设这个并发队列创建了两个线程A和B,由于是异步的,可以同时执行。因此会出现这么一个场景,在线程A中,代码执行到了objc_retain(obj),但是在线程B中可能执行到了objc_release(prev),此时prev已经被释放了。那么当A在执行到objc_release(prev)就会过度释放,从而导致程序crash。

解决方法:

  1. 加个互斥锁
  2. 使用串行队列,使用串行队列的话,其实内部是靠DISPATCH_OBJ_BARRIER_BIT设置阻塞标志位
  3. 使用weak
  4. Tagged Pointer,如果说上面的self.target指向的是一个Tagged Pointer技术的NSString,那程序就没有问题。

Reference

1.iOS引用计数管理之揭秘计数存储

OC内存管理-引用计数器 | NeroXie的个人博客

results matching ""

    No results matching ""