搞懂Objective-C中的autorelease

前言

本文是上一篇:搞懂Objective-C中的ARC的延伸和补充

  • 上一篇讨论了下面几个问题

1、iOS操作系统内存分区
2、Objective-C中的指针
3、什么是ARC,引用计数存储在哪里,哪些对象是通过引用计数来管理内存的
4、ARC下哪些对象是autorelease对象

  • 本文将讨论下面几个问题

1、autorelease对象的释放时机
2、为什么需要手动添加autoreleasePool
3、autoreleasePool的源码实现
4、autoreleasePool为什么用双向链表来实现呢

从线程池说起

操作系统实现了一个线程工厂称为线程池,还有一个配合工作的管理器称为调度中心,iOS系统并没有接口去直接操作线程池,线程的创建、调度、销毁全权交给线程池,当我们试图去创建一个线程的时候,调度中心会检查线程池的运行状态、运行线程数、运行策略,决定接下来的执行流程,是直接申请线程执行还是放入缓冲队列中,待到执行的时候,线程池决定是开辟新的线程还是复用现有空闲线程,长时间空闲的线程会被回收(一般10s左右)

iOS线程对应的数据结构

iOS操作系统为线程运行配备了一套数据结构,一个栈、一个autoreleasePool、一个runloop(懒加载)以及一些用于控制状态的标志位变量

  • 栈:存放函数的参数值与局部变量的值等
  • autoreleasePool:存放当前线程产生的autorelease对象
  • runloop:一个while循环,用于线程可持续运行的一个循环,苹果将它封装成一个类,内部注册了一些观察者,实现了一套状态机,动可执行,静可休眠

好的~我们回到问题本身

autorelease对象的释放时机

  • autorelease对象何时释放,主线程与非主线程的释放时机有何不同?为什么需要手动添加autoreleasePool?
    这个问题是不是经常被问到,有真正理解过嘛~

先说结论:

  • 加入到autoreleasePool中的对象,会在线程销毁前,倾倒一次autoreleasePool,取出每个对象,发送一次release消息,对象的引用计数-1,此时对象是否释放取决与引用计数是否为0

  • 主线程的运行循环在app装载、链接以后就会在main函数中启动,这是常规设计,所以主线程不会被销毁直到app进程退出之前,这就导致上面的策略无法释放autorelease对象,于是苹果在runloop中加入这部分对象的释放时机,每次循环同步执行完,在进入休眠之前,会倾倒一次autoreleasePool

  • 通常我们执行完现有任务就无需再次使用这个线程,所以非主线程的runloop无需启动(特殊场景的需求除外),这就导致一个问题,非主线程的autorelease对象直到线程退出之前才会得到引用计数-1,当这个线程有长任务的时候,内存会一直涨,这与峰值无关,会一直涨到OOM

  • 基于以上问题,苹果公开了autoreleasePool的api,在非主线程的场景下,手动添加@autoreleasepool{},在autoreleasepool{}作用域结束前会倾倒一次

  • 所谓嵌套autoreleasepool,不过是一个线程对应了多个autoreleasepool,默认有一个,其他的是手动添加的,根本不存在嵌套一说,都只负责各自作用域产生的autorelease对象

  • 主线程同样有需要手动添加autoreleasepool的场景,可以降低内存峰值,iOS屏幕的刷新帧率是60,即一秒刷新60次,一次约16.7毫秒,如果在这个时间内,一次运行循环还没有执行完的话就会出现卡顿现象,因为系统的vsync信号过来的时候,app没有准备好下一帧图像,所以看上去就是卡顿了,根本原因是因为主线程阻塞导致16.7毫秒内没有把图像提交到缓冲区(说远了),基于以上OOM通常不会出现在主线程,因为已经卡爆了,调试过程中就会发现,所以这里用于降低内存峰值

再看个demo,这里只讨论非主线程:

- (void)testRelease {
    dispatch_queue_t queue = dispatch_queue_create("com.test.autorelease.queue", DISPATCH_QUEUE_SERIAL);
    dispatch_async(queue, ^{
        while (1) {
            NSString *heapAreastring = [[NSString alloc] initWithFormat:@"堆区string-release"];
            NSLog(@"当前线程:%@, 值:%@", [NSThread currentThread], heapAreastring);
        }
    });
}

如上创建了一个串行队列,写了个死循环,一直在创建堆区字符串,但是内存不会涨一丝一毫,原因上一篇文章搞懂Objective-C中的ARC讲过,这个对象直接会被release,不会加入autoreleasePool

修改一下:

- (void)testAutorelease {
    dispatch_queue_t queue = dispatch_queue_create("com.test.autorelease.queue", DISPATCH_QUEUE_SERIAL);
    dispatch_async(queue, ^{
        while (1) {
            NSString *stringAutorelease = [NSString stringWithFormat:@"堆区string-autorelease"];
            NSLog(@"当前线程:%@, 值:%@", [NSThread currentThread], heapAreastring);
        }
    });
}

会发现非主线程创建autorelease对象会导致内存无休止的在涨,直到OOM

再次修改:

- (void)testAutoreleaseWithAutoreleasepool {
    dispatch_queue_t queue = dispatch_queue_create("com.test.autorelease.queue", DISPATCH_QUEUE_SERIAL);
    dispatch_async(queue, ^{
        while (1) {
            @autoreleasepool {
                NSString *stringAutorelease = [NSString stringWithFormat:@"堆区string-autorelease"];
                NSLog(@"当前线程:%@, 值:%@", [NSThread currentThread], stringAutorelease);
            }
        }
    });
}

结果是内存也不会涨一丝一毫,数据刚好能对应上面的结论,对于有知其所以然需求的可以往下看

autoreleasePool的源码实现

前辈这篇讲得很好了,本文会基于最新源码做个浅析(可能不是最新源码,作者开始看源码时候的版本objc4-781.2),顺便讲述下如何查阅源码,源码链接

在源码中搜索
AutoreleasePoolPage.png

会发现AutoreleasePoolPage是一个c++的类,继承AutoreleasePoolPageData,具体实现很长,这里省去部分函数实现

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;

    AutoreleasePoolPageData(__unsafe_unretained id* _next, pthread_t _thread, AutoreleasePoolPage* _parent, uint32_t _depth, uint32_t _hiwat)
        : magic(), next(_next), thread(_thread),
          parent(_parent), child(nil),
          depth(_depth), hiwat(_hiwat)
    {
    }
};

class AutoreleasePoolPage : private AutoreleasePoolPageData
{
    id *add(id obj)
    {
        ASSERT(!full());
        unprotect();
        id *ret = next;  // faster than `return next-1` because of aliasing
        *next++ = obj;
        protect();
        return ret;
    }

    void releaseAll() 
    {
        releaseUntil(begin());
    }

    void releaseUntil(id *stop) 
    {
        // Not recursive: we don't want to blow out the stack 
        // if a thread accumulates a stupendous amount of garbage
        
        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
            while (page->empty()) {
                page = page->parent;
                setHotPage(page);
            }

            page->unprotect();
            id obj = *--page->next;
            memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
            page->protect();

            if (obj != POOL_BOUNDARY) {
                objc_release(obj);
            }
        }

        setHotPage(this);

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

    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);
    }

    static void tls_dealloc(void *p) 
    {
        if (p == (void*)EMPTY_POOL_PLACEHOLDER) {
            // No objects or pool pages to clean up here.
            return;
        }

        // reinstate TLS value while we work
        setHotPage((AutoreleasePoolPage *)p);

        if (AutoreleasePoolPage *page = coldPage()) {
            if (!page->empty()) objc_autoreleasePoolPop(page->begin());  // pop all of the pools
            if (slowpath(DebugMissingPools || DebugPoolAllocation)) {
                // pop() killed the pages already
            } else {
                page->kill();  // free all of the pages
            }
        }
        
        // clear TLS value so TLS destruction doesn't loop
        setHotPage(nil);
    }

    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->kill();
            setHotPage(parent);
        } else if (allowDebug && DebugMissingPools  &&  page->empty()  &&  !page->parent) {
            // special case: delete everything for pop(top)
            // when debugging missing autorelease pools
            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();
            }
        }
    }
}


还是很长是不是,但是对于理解原理都重要,看看苹果自己的注释

/***********************************************************************
Autorelease pool implementation

A thread's autorelease pool is a stack of pointers.
Each pointer is either an object to release, or POOL_BOUNDARY which is
an autorelease pool boundary.
A pool token is a pointer to the POOL_BOUNDARY for that pool. When
the pool is popped, every object hotter than the sentinel is released.
The stack is divided into a doubly-linked list of pages. Pages are added
and deleted as necessary.
Thread-local storage points to the hot page, where newly autoreleased
objects are stored.
**********************************************************************/

/***********************************************************************
Autorelease pool 的实现
线程的Autorelease pool是一系列声明在栈上的指针。
每个指针要么是一个需要释放的对象,要么是 POOL_BOUNDARY,它是
Autorelease pool边界。
token是指向该池的 POOL_BOUNDARY 的指针。 什么时候
池被弹出,每个比哨兵更热的对象都被释放。
栈被分成一个双向链接的AutoreleasePoolPage列表。 Pages根据需要,添加
和删除。
线程本地存储指向最新被存储到自动释池所在的热点页面

**********************************************************************/

看完这个注释,我觉得都不用往下讲了😂,一个字,清晰透彻

autorelease对象加入到自动释放池的流程

    id *add(id obj)
    {
        ASSERT(!full());
        unprotect();
        id *ret = next;  // faster than `return next-1` because of aliasing
        *next++ = obj;
        protect();
        return ret;
    }

next指针作为游标指向栈顶最新add进来的autorelease对象的下一个位置,当有新的obj加入autoreleasePool的时候取到next指针的地址存储obj,next指针向栈顶方向移动8个字节

runloop没有启动的线程 autorelease对象的释放流程

每个autoreleasePool初始化的时候都会绑定当前线程,如下是AutoreleasePoolPage的初始化函数

    static void init()
    {
        int r __unused = pthread_key_init_np(AutoreleasePoolPage::key, 
                                             AutoreleasePoolPage::tls_dealloc);
        ASSERT(r == 0);
    }

第一个参数是AutoreleasePoolPage的标识,第二个参数是一个函数指针,线程退出之前会执行这个函数,就是tls_dealloc,调用栈如下:

1、tls_dealloc中会取AutoreleasePoolPage *page = coldPage()
2、page调用objc_autoreleasePoolPop(page->begin())函数
3、调用栈如下objc_autoreleasePoolPop->pop->popPage->releaseUntil
4、releaseUntil中取hotPage(),然后通过一个循环,取next游标指针id obj = *--page->next,调用objc_release(obj),然后游标指针向栈底移动8个字节
5、所有autorelease对象清理干净,最后调用kill()函数遍历释放所有autoreleasePage

runloop启动的线程 autorelease对象的释放流程

线程休眠之前,runloop会调用objc_autoreleasePoolPop,上面流程除了第一步,后面一样

autoreleasePool为什么用双向链表来实现呢

单向的可以吗?可以呀,但是查询速度会以指数级别降低,删除栈顶page后想删除的倒数第二个page,怎么办,从头开始遍历呗,遍历n-1次,然后遍历n-2次.......
所以为什么设计成双向链表,效率高,如此而已

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容