引用计数与autoreleasepool

892 阅读9分钟

引用计数与autorealeasepoll为ios中从ARC开始提及就非常多,当一个对象的引用计数release为0时,则会被释放掉,而autoreleasepool在其中也起到了非常重要的作用

补充一下:ARC是LVVM和runtime配合的结果,其禁止了引用计数相关的方法(MRC中可以使用):retain/release/retainCount/dealloc

引用计数

引用计数过程中通过retain可以使对象引用计数+1,而release则会使对象引用计数-1,当引用计数为0时,该对象会被释放(不会想临时变量一样出栈立即释放,因此后续autoreleasepool会介绍)

对象的isa在通过指针优化,里面会存放着extra_rc、shiftcls、magic、weakly_referenced、deallocating、has_sidetable_rc等参数

下面介绍下里面的参数:

nonpointer:表示是否对 isa 指针开启指针优化 0:纯isa指针,1:不⽌是类对象地址,isa中包含了类信息、对象的引⽤用计数等

has_assoc:关联对象标志位,0没有,1存在

has_cxx_dtor:该对象是否有C++或者Objc的析构器器,如果有析构函数,则需要做析构逻辑, 如果没有,则可以更更快的释放对象

shiftcls:存储类指针的值。开启指针优化的情况下,在arm64架构中有33位⽤用来存储类指针。

magic:⽤用于调试器器判断当前对象是真的对象还是没有初始化的空间

weakly_referenced:对象是否被指向或者曾经指向⼀个ARC的弱变量量,没有弱引⽤的对象可以更快释放。

deallocating:标志对象是否正在释放内存

has_sidetable_rc:当对象引⽤用技术大于10时,则需要借⽤该变量存储进位

extra_rc:当表示该对象的引⽤用计数值,实际上是引⽤用计数值减1,例如:如果对象的引⽤用计数为10,那么extra_rc为9。如果引⽤用计数⼤大于10, 则需要使⽤用到下⾯面的has_sidetable_rc

sidetable

sidetable相信不是第一次看到,当isa优化的引用计数空间不够用时,会存放到这里面,且其不仅仅存放这里的引用计数,前面的其中一篇文章weaktable也在这里面

sidetable中包含: Spinlock_t⾃自旋锁 RefcountMap引⽤用技术、weak_table_t弱引⽤用表

而sidetable是从总的sidetables中根据对象的哈希值来获取到的,RefcountMap为引用计数哈希表,在里面通过对象的哈希值来获取引用计数对象,然后通过retain和release来操作对象的引用计数(这原理与weaktable相似)

引用计数就介绍到这里,可以探究下,对象alloc之后的引用计数是多少?retainCount方法干了什么?

autoreleasepool(自动释放池)

研究自动释放池之前,我们就看看自动释放池使用的时候做了什么

首先main函数中实现这么一段代码

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSObject *obj = [NSObject alloc];
    }
    return NSApplicationMain(argc, argv);
}

进入目录,通过 clang 编译代码

clang -rewrite-objc main.m -o main.c

编译后的代码如下所示:

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
        NSObject *obj = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc"));
    }
    return NSApplicationMain(argc, argv);
}

这里面编译出来的代码出现了c++的结构__AtAutoreleasePool,如下所示,其中有构造函数和析构函数(~)

struct __AtAutoreleasePool {
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
  void * atautoreleasepoolobj;
};

第一个是构造函数:即创建的时候走这个方法,像声明临时变量一样声明后,即为创建

第二个是析构函数(~):销毁的时候走这个方法,即临时变量销毁的时机,即碰到autoreleasepool的大括号}的尾部即调用析构函数,即执行完里面的任务就会调用析构函数

即可以理解为如下代码

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ {
        objc_autoreleasePoolPush();
        
        NSObject *obj = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc"));
        
        objc_autoreleasePoolPop(atautoreleasepoolobj);
    }
    return NSApplicationMain(argc, argv);
}

接下来查看objc源码看看autoreleasepool的结构是怎么样的

通过搜索objc_autoreleasePoolPush方法可以找到相关入口

struct AutoreleasePoolPageData
{
    ...
    magic_t const magic;
    __unsafe_unretained id *next;
    pthread_t const thread;
    AutoreleasePoolPage * const parent;
    AutoreleasePoolPage *child;
    uint32_t const depth;
    uint32_t hiwat;
    ...
};

可以看到AutoreleasePoolPage继承自AutoreleasePoolPageData,且AutoreleasePoolPageData的参数如下,可以猜到其结构为一个双向链表(不是树,一个节点只有一个子分支),每一个节点都有前驱(parent)和后继(child),因此会有多个AutoreleasePoolPage存在

接下来通过objc_autoreleasePoolPush和objc_autoreleasePoolPop方法来分析其中原理

objc_autoreleasePoolPush


void *
objc_autoreleasePoolPush(void)
{
    return AutoreleasePoolPage::push();
}
//加入autoreleasepull
static inline void *push() 
{
    id *dest;
    if (slowpath(DebugPoolAllocation)) {
        // Each autorelease pool starts on a new pool page.
        dest = autoreleaseNewPage(POOL_BOUNDARY);
    } else {
        dest = autoreleaseFast(POOL_BOUNDARY);
    }
    ASSERT(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
    return dest;
}

最后进入autoreleaseFast方法来处理

static inline id *autoreleaseFast(id obj)
    {
        //获取autoreleasepoolpage
        AutoreleasePoolPage *page = hotPage();
        if (page && !page->full()) {
            //如果存在没满加入poolpage中
            return page->add(obj);
        } else if (page) {
            //存在page,且当前page满了
            return autoreleaseFullPage(obj, page);
        } else {
            //不存在page,创建新的page加入
            return autoreleaseNoPage(obj);
        }
    }

有page为满的情况,autoreleaseFullPage方法如下所示

static __attribute__((noinline))
    id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
    {
        // The hot page is full. 
        // Step to the next non-full page, adding a new page if necessary.
        // Then add the object to that page.
        ASSERT(page == hotPage());
        ASSERT(page->full()  ||  DebugPoolAllocation);
        
        //往后找page,如果都满了则创建新的page
        do {
            if (page->child) page = page->child;
            else page = new AutoreleasePoolPage(page);//创建新page
        } while (page->full());
        //设置新page
        setHotPage(page);
        //将对象加入到新的page中
        return page->add(obj);
    }

没有page的情况,autoreleaseNoPage实现如下所示

static __attribute__((noinline))
    id *autoreleaseNoPage(id obj)
    {
        bool pushExtraBoundary = false;
        if (haveEmptyPoolPlaceholder()) {
            pushExtraBoundary = true;
        }
        else if (obj != POOL_BOUNDARY  &&  DebugMissingPools) {
            // We are pushing an object with no pool in place, 
            // and no-pool debugging was requested by environment.
            _objc_inform("MISSING POOLS: (%p) Object %p of class %s "
                         "autoreleased with no pool in place - "
                         "just leaking - break on "
                         "objc_autoreleaseNoPool() to debug", 
                         objc_thread_self(), (void*)obj, object_getClassName(obj));
            objc_autoreleaseNoPool(obj);
            return nil;
        }
        else if (obj == POOL_BOUNDARY  &&  !DebugPoolAllocation) {
            // We are pushing a pool with no pool in place,
            // and alloc-per-pool debugging was not requested.
            // Install and return the empty pool placeholder.
            return setEmptyPoolPlaceholder();
        }

        //创建新page,并设置
        AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
        setHotPage(page);
        
        // Push a boundary on behalf of the previously-placeholder'd pool.
        //给第一个page加入POOL_BOUNDARY
        if (pushExtraBoundary) {
            page->add(POOL_BOUNDARY);
        }
        
        //加入到新的page中
        return page->add(obj);
    }

add方法实现如下所示,主要next指针位置保存新的obj,next指向后一位地址,还引入了AutoreleasePoolEntry,其功能与其相似

id *add(id obj)
    {
        ASSERT(!full());
        unprotect();
        id *ret;

#if SUPPORT_AUTORELEASEPOOL_DEDUP_PTRS
        if (!DisableAutoreleaseCoalescing || !DisableAutoreleaseCoalescingLRU) {
            if (!DisableAutoreleaseCoalescingLRU) {
                if (!empty() && (obj != POOL_BOUNDARY)) {
                    AutoreleasePoolEntry *topEntry = (AutoreleasePoolEntry *)next - 1;
                    for (uintptr_t offset = 0; offset < 4; offset++) {
                        AutoreleasePoolEntry *offsetEntry = topEntry - offset;
                        if (offsetEntry <= (AutoreleasePoolEntry*)begin() || *(id *)offsetEntry == POOL_BOUNDARY) {
                            break;
                        }
                        if (offsetEntry->ptr == (uintptr_t)obj && offsetEntry->count < AutoreleasePoolEntry::maxCount) {
                            if (offset > 0) {
                                AutoreleasePoolEntry found = *offsetEntry;
                                memmove(offsetEntry, offsetEntry + 1, offset * sizeof(*offsetEntry));
                                *topEntry = found;
                            }
                            topEntry->count++;
                            ret = (id *)topEntry;  // need to reset ret
                            goto done;
                        }
                    }
                }
            } else {
                if (!empty() && (obj != POOL_BOUNDARY)) {
                    AutoreleasePoolEntry *prevEntry = (AutoreleasePoolEntry *)next - 1;
                    if (prevEntry->ptr == (uintptr_t)obj && prevEntry->count < AutoreleasePoolEntry::maxCount) {
                        prevEntry->count++;
                        ret = (id *)prevEntry;  // need to reset ret
                        goto done;
                    }
                }
            }
        }
#endif
        ret = next;  // faster than `return next-1` because of aliasing
        *next++ = obj;
#if SUPPORT_AUTORELEASEPOOL_DEDUP_PTRS
        // Make sure obj fits in the bits available for it
        ASSERT(((AutoreleasePoolEntry *)ret)->ptr == (uintptr_t)obj);
#endif
     done:
        protect();
        return ret;
    }

objc_autoreleasePoolPop

objc_autoreleasePoolPop方法为释放pool内部引用的对象,实现如下所示,最终会调用popPage方法来释放pool内部对象

static inline void
pop(void *token)
{
    AutoreleasePoolPage *page;
    id *stop;
    if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
        // Popping the top-level placeholder pool.
        page = hotPage();
        if (!page) {
            // Pool was never used. Clear the placeholder.
            return setHotPage(nil);
        }
        // Pool was used. Pop its contents normally.
        // Pool pages remain allocated for re-use as usual.
        page = coldPage();
        token = page->begin();
    } else {
        page = pageForPointer(token);
    }

    stop = (id *)token;
    if (*stop != POOL_BOUNDARY) {
        if (stop == page->begin()  &&  !page->parent) {
            // Start of coldest page may correctly not be POOL_BOUNDARY:
            // 1. top-level pool is popped, leaving the cold page in place
            // 2. an object is autoreleased with no pool
        } else {
            // Error. For bincompat purposes this is not 
            // fatal in executables built with old SDKs.
            return badPop(token);
        }
    }

    if (slowpath(PrintPoolHiwat || DebugPoolAllocation || DebugMissingPools)) {
        return popPageDebug(token, page, stop);
    }

    return popPage<false>(token, page, stop);
}
    
 //释放内部对象实际方法
template<bool allowDebug>
static void
popPage(void *token, AutoreleasePoolPage *page, id *stop)
{
    if (allowDebug && PrintPoolHiwat) printHiwat();

    page->releaseUntil(stop);

    // memory: delete empty children
    if (allowDebug && DebugPoolAllocation  &&  page->empty()) {
        // special case: delete everything during page-per-pool debugging
        AutoreleasePoolPage *parent = page->parent;
        //page为空,清理掉当前page
        page->kill();
        setHotPage(parent);
    } else if (allowDebug && DebugMissingPools  &&  page->empty()  &&  !page->parent) {
        // special case: delete everything for pop(top)
        // when debugging missing autorelease pools
        //page为空,清理掉当前page
        page->kill();
        setHotPage(nil);
    } else if (page->child) {
        // hysteresis: keep one empty child if page is more than half full
        if (page->lessThanHalfFull()) {
            page->child->kill();
        }
        else if (page->child->child) {
            page->child->child->kill();
        }
    }
}

kill方法为清除掉当前page,代码如下所示

 void kill() 
    {
        // Not recursive: we don't want to blow out the stack 
        // if a thread accumulates a stupendous amount of garbage
        AutoreleasePoolPage *page = this;
        while (page->child) page = page->child;

        AutoreleasePoolPage *deathptr;
        do {
            deathptr = page;
            page = page->parent;
            if (page) {
                page->unprotect();
                page->child = nil;
                page->protect();
            }
            delete deathptr;
        } while (deathptr != this);
    }

releaseUntil方法为对释放池内的对象进行release,减少引用计数,引用计数归零的会被系统自动回收,代码如下所示

void releaseUntil(id *stop) 
    {
        // Not recursive: we don't want to blow out the stack 
        // if a thread accumulates a stupendous amount of garbage
        
        //遍历释放池page,对内部对象进行释放
        while (this->next != stop) {
            // Restart from hotPage() every time, in case -release 
            // autoreleased more objects
            AutoreleasePoolPage *page = hotPage();

            // fixme I think this `while` can be `if`, but I can't prove it
            //从头开始找到不为空的page
            while (page->empty()) {
                page = page->parent;
                setHotPage(page);
            }

            page->unprotect();
            //知道page->next指向其上一位,并释放page->next指向的对象obj
#if SUPPORT_AUTORELEASEPOOL_DEDUP_PTRS
            AutoreleasePoolEntry* entry = (AutoreleasePoolEntry*) --page->next;

            // create an obj with the zeroed out top byte and release that
            
            id obj = (id)entry->ptr;
            int count = (int)entry->count;  // grab these before memset
#else
            id obj = *--page->next;
#endif
            memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
            page->protect();

            if (obj != POOL_BOUNDARY) {
#if SUPPORT_AUTORELEASEPOOL_DEDUP_PTRS
                // release count+1 times since it is count of the additional
                // autoreleases beyond the first one
                for (int i = 0; i < count + 1; i++) {
                    objc_release(obj);
                }
#else
                objc_release(obj);
#endif
            }
        }

        setHotPage(this);

#if DEBUG
        // we expect any children to be completely empty
        for (AutoreleasePoolPage *page = child; page; page = page->child) {
            ASSERT(page->empty());
        }
#endif
    }

_objc_rootAutorelease

这个和autoreleasepool的一样,最后会调用到autoreleaseFast方法,经过了下面几个函数,最后到达autoreleaseFast

_objc_rootAutorelease->rootAutorelease->rootAutorelease2->autorelease->autoreleaseFast

runloop与autoreleasepool

上面讲到的是autoreleasepool的基本功能,其与runloop配合才会发挥巨大功效

runloop顾名思义是一个运行循环,其与线程绑定依赖线程而存在,基本上我们写的东西运行全靠它执行,可以参考前面的runloop

全局的autoreleasepool

runloop每一轮循环都会执行source、observer等,我们在运行过程创建的临时变量都被会捕获进入全局的autorelease中,当runloop一轮周期执行完毕之后,runloop会将自动释放池中的autorelease临时对象全部release一遍,引用计数不为零的仍然存活,引用计数清零的直接嗝屁

处理临时对象的autoreleasepool

另外我们在一个方法循环中创建的autoreleasepool,为了临时变量造成的内存过大,其代码如下所示:

for (NSInteger i = 0; i < NSIntegerMax; i++) {
    @autoreleasepool {
        //这里只是小对象,但数量很多,也有很大的对象更要注意
        id obj = [NSObject alloc];
    } //执行到这个括号时会自动调用autorelease的析构函数(学c++时应该会有介绍)
}

因此为了避免内存峰值过大,甚至可能会被系统看门狗强行kill应用,必须要使用autoreleasepool,前面也看到了autoreleasepool编译后的代码,会在当前栈区执行其构造和析构函数,会自动对此此时加入的autorelease对象进行一次释放,因此内存性能不会出现较大波动

注意:不要因为在函数栈内创建的的都是存在栈区,通过alloc创建的对象都是堆区,不会立即释放,而此时存放在栈区的obj只是一个临时的指针罢了,真实的对象在堆区,需要等到autoreleasepool对其进行release了,如果没有此处的autoreleasepool,则需要等到此轮runloop执行完毕,等到整体对新加入对象的依次release

最后

了解autoreleasepool后,可以更加清楚地理解ARC下对象的释放时机,以及更好的把控对象的释放时机

最后,内存优化的知识又多了一角