内存管理之引用计数

作者: 予京 | 来源:发表于2017-03-01 09:59 被阅读0次

    什么是引用计数?

    引用计数是最普遍的垃圾回收策略之一。每一个对象都会有一个额外的计数值来表示当前被引用的次数。有新的引用,这个值就会+1;结束引用,这个值会自动-1,直到计数值为0时,对象所指的内存块就会废弃掉被系统回收,从而达到释放内存的目的。

    在ARC还没引入之前,引用计数大都是Coder该思考的事情,所以我们可以先关掉项目的ARC来深入了解苹果的内存管理机制。(花括号 "{ }" 表示一个生命周期)

    {
        // 初始化一个对象并被obj持有 reference count = 1
        NSObject *obj = [[NSObject alloc] init];
        
        // newObj 持有后,reference count = 2
        NSObject *newObj = [obj retain];
        
        // 连续释放对象
        [newObj release];
        [obj release];
        
        /// 两步release释放操作的是同一个对象
        /// reference count = 0, 对象被废弃回收
    }
    

    如何操作引用计数?

    上一步操作中,其实已经涉及到如何操作引用计数了。显然,在Foundation框架中,NSObject 类负担了内存管理的职责,大部分对象都是继承自 NSObject。

    +[NSObject alloc]        // 生成对象
    -[NSObject retain]       // 持有对象,增加一个新的引用
    -[NSObject release]      // 释放对象,减少一个引用
    -[NSObject dealloc]      // 废弃对象
    

    从上面的方法中可以看出,生成对象和持有对象是两个完全独立的过程,很多一开始接触代码就用ARC的童鞋会把这两个过程搞混淆,并且以为是一个整体。苹果主要有两大类方式来管理生成和持有对象。第一种是所有童鞋都知道的以 alloc / new / copy / mutableCopy 等开头的初始化方法名,这种是自己生成即持有。除了第一种以外的初始化方法,比如 +[NSArray array], 就是第二种,这种是别人生成,自己持有,有点类似于寄生,也是被大部分童鞋所忽略的。通过举栗子来分析两者的差别。

    第一种生成即持有的栗子:

    {
        // 以 alloc 等字眼为开头的初始化方法生成即持有
        NSArray *obj = [[NSArray alloc] init];
        
        // 释放 obj
        [obj release];
        
    }
    

    第二种生成不持有的栗子:

    {
        // newObj 不持有新生成的对象
        NSArray *newObj = [NSArray array];
       
        // 通过调用 retain 来持有非自己生成的对象
        [newObj retain];
        
        // 释放对象。如果之前未调用retain,会导致程序崩溃
        [newObj release];
    }
    

    第二种生成方式需要显示地调用 -retain 才能真正持有对象,实际上 NSArray 的静态初始化方法,是调用了[[NSArray alloc] init],但是为什么我们还要手动再调一次 -retain 呢?,苹果在这里用了自动释放池 @autoreleasepool ,简单地说就是注册到释放池的对象会随着释放池生命周期结束而自动释放(如果博主勤快的话,自动释放池会在后续的章节里详细阐述)。

    + (instancetype)array {
        // 这是一种通认的实现方式
        NSArray *obj = [[[NSArray alloc] init] autorelease];
        return obj;
        
        /// 新生成的对象实际上是由自动释放池释放
        /// 这边引入释放池可以保证自己生成对象由自己释放
    }
    

    引用计数原理

    苹果管理引用计数的方法是通过哈希表来实现的,具体实现我们来看下苹果的源码 ,下面提取的源码已进行过简化整合,并删除一些判断条件以及表锁。前面已经提到,引用计数的操作是NSObject负责的,我们可以在runtime/NSObject.mm类文件中找到相关源码。

    我们先看下 -[NSObject retain] 是如果实现增加引用计数的:

    - (id)retain {
        // 获取对象哈希表
        SideTable& table = SideTables()[this];
        
        // 获取对象当前的引用计数
        size_t& refcntStorage = table.refcnts[this];
        
        if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
            // 如果当前引用计数未越界,增加引用计数
            refcntStorage += SIDE_TABLE_RC_ONE;
            return (id)this;
        }
        
        // 这一步是苹果做的优化
        // 如果前面操作引用计数失败,会进入sidetable_retain_slow函数
        // sidetable_retain_slow 做的是前面类似的工作
        return sidetable_retain_slow(table);
    }
    

    相对于 -[NSObject retain],-[NSObject release] 会多一步判断引用计数和释放内存块的过程:

    - (oneway void)release {
        // 获取对象哈希表
        SideTable& table = SideTables()[this];
        
        bool do_dealloc = false;
    
        if (table.trylock()) {
        
            // 获取对象引用计数
            RefcountMap::iterator it = table.refcnts.find(this);
            
            if (it == table.refcnts.end()) {
                // 引用计数为0后设置delloc标记
                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();
                
            // release 传进来的 performDealloc = true
            if (do_dealloc  &&  performDealloc) {
                // 废弃对象
                ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
            }
            
            return do_dealloc;
        }
    
        return sidetable_release_slow(table, performDealloc);
    }
    

    另外,分析下苹果的源码,就会发现苹果对引用计数做的一些优化:

    1. 使用哈希表管理引用计数,可以通过键值对追溯到对象的内存块,在内存泄露的时候很容易定位到问题的源头;
    2. 哈希表是分段的,也就是说,查找指定对象地址时,会先去查找对象的一个范围区间,再确定具体地址。类似于图书馆里的书都是分类整理的;
    3. 哈希表加入锁,线程安全;
    4. 操作引用计数失败,会有备案操作。
    (经验水平有限,如有错误欢迎指正)

    相关文章

      网友评论

        本文标题:内存管理之引用计数

        本文链接:https://www.haomeiwen.com/subject/cvlqgttx.html