深入浅出ARC(上)

最近和同事在讨论一个循环引用问题的时候,不经意间讨论到了内存管理的本质到底是什么,于是翻出《Objective-C高级编程》一看,但是发现上面有些问题,可能是比较旧了,苹果有了新的实现,所以拿出Objc源码研究了下,下面是自己研究的内容,如果理解有误,请定要联系我纠正。

内存管理

大家都知道,Objc是通过引用计数来管理内存的(Mac除外啊,毕竟处理器牛逼),主要就遵循以下几个原则:

  1. 自己生成的对象,自己持有。
  2. 非自己生成的对象,自己也能持有。
  3. 不再需要持有的对象,自己可以释放。
  4. 非自己持有的对象不能释放。

其实这些就是对应了Objc中得alloc,retain等方法,这些是引用计数的思考方式,并不因是否ARC改变,那么接下来看一下一些底层实现。

alloc

通过苹果官网开源的Objc库,我们可以发现总共调用了如下几个方法,虽然很长,然而有一些我们是可以忽略的,逐步分析。

1.这个方法很简单就是调用了_objc_rootAlloc函数。

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

2.调用了callAlloc函数,这个函数有三个参数,其中第一个参数不能为空,这是一个最基础的alloc方法的实现。

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

3.这里调用的代码就变多了,其实大部分都不需要看,关键就在class_createInstance上,这也是真实地创建实例的方法。其他代码主要是去判断是否自定义alloc等。

static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
    if (checkNil && !cls) return nil;
\#if __OBJC2__
    if (! cls->ISA()->hasCustomAWZ()) {
        // No alloc/allocWithZone implementation. Go straight to the allocator.
        // fixme store hasCustomAWZ in the non-meta class and 
        // add it to canAllocFast's summary
        if (cls->canAllocFast()) {
            // No ctors, raw isa, etc. Go straight to the metal.
            bool dtor = cls->hasCxxDtor();
            id obj = (id)calloc(1, cls->bits.fastInstanceSize());
            if (!obj) return callBadAllocHandler(cls);
            obj->initInstanceIsa(cls, dtor);
            return obj;
        }
        else {
            // Has ctor or raw isa or something. Use the slower path.
            id obj = class_createInstance(cls, 0);
            if (!obj) return callBadAllocHandler(cls);
            return obj;
        }
    }
#endif

    // No shortcuts available.
    if (allocWithZone) return [cls allocWithZone:nil];
    return [cls alloc];
}

4.这个函数则是调用了一个私有函数_class_createInstanceFromZone,从这里也能看出Objc其实已经不用zone了。

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

5.最终调用的其实是这一行代码obj = (id)calloc(1, size);到这里一个对象的初始化基本告一段落了,这中间跳过了很多,不过这次主要是探究一下和内存管理相关的,也无伤大雅。最后是用calloc分配了一个初始化为0的对象。

static __attribute__((always_inline)) 
id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone)
{
    if (!cls) return nil;

    assert(cls->isRealized());

    // Read class's info bits all at once for performance
    bool hasCxxCtor = cls->hasCxxCtor();
    bool hasCxxDtor = cls->hasCxxDtor();
    bool fast = cls->canAllocIndexed();

    size_t size = cls->instanceSize(extraBytes);

    id obj;
    if (!UseGC  &&  !zone  &&  fast) {
        obj = (id)calloc(1, size);
        if (!obj) return nil;
        obj->initInstanceIsa(cls, hasCxxDtor);
    } 
    else {
#if SUPPORT_GC
        if (UseGC) {
            obj = (id)auto_zone_allocate_object(gc_zone, size,
                                                AUTO_OBJECT_SCANNED, 0, 1);
        } else 
#endif
        if (zone) {
            obj = (id)malloc_zone_calloc ((malloc_zone_t *)zone, 1, size);
    } else {
            obj = (id)calloc(1, size);
        }
        if (!obj) return nil;

        // Use non-indexed isa on the assumption that they might be 
        // doing something weird with the zone or RR.
        obj->initIsa(cls);
    }

    if (hasCxxCtor) {
        obj = _objc_constructOrFree(obj, cls);
    }

    return obj;
}

retianCount

前面可能大家会很奇怪,为什么没有看到初始化为1的retainCount呢?其实这和Objc的retainCount实现原理有关,下面上源码:

1.这个方法很简单就是调用了rootRetainCount这个函数

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

2.这里有一个是否isTaggedPointer的判断,相关内容可以查看唐巧的这篇博客。这个可以跳过,主要就是调用了sidetable_retainCount这个函数。

inline uintptr_t 
objc_object::rootRetainCount()
{
    assert(!UseGC);
    if (isTaggedPointer()) return (uintptr_t)this;
    return sidetable_retainCount();
}

3.从名字上看这应该是一个和sidetable相关的函数,果然,里面使用了SideTable,从这里,我们大致可以看出Objc使用了类似散列表的结构来记录引用计数。并且在初始化的时候设为了一。不过在这里有一个很奇怪的点,为什么最后返回的refcnt_result是1加上it->second >> SIDE_TABLE_RC_SHIFT。这个也是苹果机智的地方,等下会介绍。

uintptr_t
objc_object::sidetable_retainCount()
{
    SideTable *table = SideTable::tableForPointer(this);

    size_t refcnt_result = 1;

    spinlock_lock(&table->slock);
    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;
    }
    spinlock_unlock(&table->slock);
    return refcnt_result;
}

retain

这个就比较简单了,总结下来其实就是找到散列表中得retainCount然后加1向左偏移SIDE_TABLE_RC_SHIFT的值,这里可能大家就知道为什么上面要有一个偏移值了。

1.还是简单地调用了Objc中得rootRetain函数

- (id)retain {
    return ((id)self)->rootRetain();
}

2.同样是判断是否isTaggedPointer,然后调用sidetable_retain。

inline id 
objc_object::rootRetain()
{
    assert(!UseGC);
    if (isTaggedPointer()) return (id)this;
    return sidetable_retain();
}

3.同样地取到SideTable,refcntStorage += SIDE_TABLE_RC_ONE,同样地加了一个偏移量。然后也能看出散列表的key是对象的指针。

id
objc_object::sidetable_retain()
{
#if SUPPORT_NONPOINTER_ISA
    assert(!isa.indexed);
#endif
    SideTable *table = SideTable::tableForPointer(this);

    if (spinlock_trylock(&table->slock)) {
        size_t& refcntStorage = table->refcnts[this];
        if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
            refcntStorage += SIDE_TABLE_RC_ONE;
        }
        spinlock_unlock(&table->slock);
        return (id)this;
    }
    return sidetable_retain_slow(table);
}

release

这里会解释SIDE_TABLE_RC_SHIFT这个偏移量存在的原因。前面两步和retain一样,不介绍了。

- (oneway void)release {
    ((id)self)->rootRelease();
}

inline bool 
objc_object::rootRelease()
{
    assert(!UseGC);

    if (isTaggedPointer()) return false;
    return sidetable_release(true);
}

3.主要看一下这里,其实加上SIDE_TABLE_RC_ONE这个偏移是为了空出SIDE_TABLE_DEALLOCATING这个标示,看下定义大家就能懂了

// 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_SHIFT 2

4.SIDE_TABLE_WEAKLY_REFERENCED是代表了有弱引用,SIDE_TABLE_DEALLOCATING代表了需要释放的引用,SIDE_TABLE_RC_ONE这个则是正常的的偏移。值是和SIDE_TABLE_RC_SHIFT偏移一样的。
release比retain稍微复杂的地方就是他需要判断最终是否需要调用dealloc,所以多了很多判断和赋值,大概步骤就是1.先遍历变量是否存在,如果不存在就调用dealloc,2.如果存在再判断是否小于SIDE_TABLE_DEALLOCATING,如果成立同1,3.否则就减去一个SIDE_TABLE_RC_ONE4.最后看do_dealloc是否需要调用dealloc。

bool 
objc_object::sidetable_release(bool performDealloc)
{
\#if SUPPORT_NONPOINTER_ISA
    assert(!isa.indexed);
\#endif
    SideTable *table = SideTable::tableForPointer(this);

    bool do_dealloc = false;

    if (spinlock_trylock(&table->slock)) {
        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;
        }
        spinlock_unlock(&table->slock);
        if (do_dealloc  &&  performDealloc) {
            ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
        }
        return do_dealloc;
    }

    return sidetable_release_slow(table, performDealloc);
}

dealloc

这里其实有很多细节的,比如说在dealloc的时候会做哪些操作等,这个放到下一次讲,这里主要看的是dealloc实现原理,前面3步很简单,略过。

- (void)dealloc {
    _objc_rootDealloc(self);
}

void
_objc_rootDealloc(id obj)
{
    assert(obj);

    obj->rootDealloc();
}

inline void
objc_object::rootDealloc()
{
    if (isTaggedPointer()) return;
    object_dispose((id)this);
}

4.这里能够清晰地看到在dealloc之前其实会调用一下objc_destructInstance,这里面做了很多操作,清理关联对象,weak引用等,最后free掉。

id 
object_dispose(id obj)
{
    if (!obj) return nil;
    objc_destructInstance(obj);

\#if SUPPORT_GC
    if (UseGC) {
        auto_zone_retain(gc_zone, obj); // gc free expects rc==1
    }
\#endif

    free(obj);

    return nil;
}

ARC的引子

(这里,为ARC开个头,具体内容请看下回分析。)

顾名思义,ARC就是交给编译器来管理引用计数,而大家都知道Objc是通过引用计数来管理内存的,也就是说现在的内存管理已经不需要程序员来操心了(大多数情况下)