Runtime源代码解读10(内存管理Autorelease)

1,414 阅读15分钟

2019-12-19

一、概述

本文继续探究另一种内存管理方式 Autoreleasing,即通过自动释放池 autorelease pool 进行内存管理。以下摘自 Runtime 源代码中名为 Autorelease pool implementation 的注释:

A thread's autorelease pool is a stack of pointers. Each pointer is either an object to release, or POOL_SENTINEL which is an autorelease pool boundary. A pool token is a pointer to the POOL_SENTINEL 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 是与线程关联的;
  • Autorelease pool 中的指针要么指向需要release的对象,要么是POOL_SENTINELPOOL_SENTINEL是 autorelease pool 的边界
  • Autorelease pool 的 token是指向 autorelease pool 自身的POOL_SENTINEL的指针。当autorelease pool释放时,会释放所有比 tokenhotter更“热”的对象
  • Autorelease pool 中,指针的堆栈被划分到 分页中,分页使用双向链表的数据结构关联,分页可按需添加或删除;
  • 线程本地存储指向hot page“热”分页,“热”分页保存最新的 autorelease 的对象。

接下来第二章,会从 Runtime 源代码中,找到以上要点的实现原理。

二、源码分析

首先以每个 iOS app 工程都会包含main.m文件作为突破口,其源码如下:

#import <UIKit/UIKit.h>
#import "AppDelegate.h"

int main(int argc, char * argv[])
{
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

逻辑很简单:1、新建 autorelease pool;2、调用UIApplicationMain(...)函数新建一个UIApplication实例并将代理设置为AppDelegate,开始运行。这是能找到的关于autorelease 的最简短的代码。

2.1 @autoreleasepool块原理

打开命令行cdmain.m文件所在目录,使用clang -rewrite-objc main.mmain.m转化为 C/C++ 语言。这里可能会抛如下的错误。

执行clang命令可能会出错误

没关系,这里只关心@autoreleasepool的实现,可以把不关心的代码悉数删掉,只留下:

int main(int argc, char * argv[])
{
    @autoreleasepool {
        
    }
}

再重新执行clang命令,成功后会在main.m所在目录下生成一个main.cpp文件,摘出 autorelease pool 相关代码:

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

int main(int argc, char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

    }
}

其逻辑:

  • 在C语言的作用域{ }内声明一个__AtAutoreleasePool结构体__autoreleasepool会自动触发__AtAutoreleasePool()构建方法;
  • 超出作用域会释放变量占用的内存空间,即自动触发~__AtAutoreleasePool()方法。

最后,进一步剔除结构体的代码,@autoreleasepool{ }块实际等价于:

{
    void* token = objc_autoreleasePoolPush();

    objc_autoreleasePoolPop(token);
}

此处的token实际上就是第一章第3个要点所提到的 token。

再看objc_autoreleasePoolPush()函数以及objc_autoreleasePoolPop(...)函数的源代码:

void * objc_autoreleasePoolPush(void)
{
    if (UseGC) return nil;
    return AutoreleasePoolPage::push();
}

void objc_autoreleasePoolPop(void *ctxt)
{
    if (UseGC) return;
    AutoreleasePoolPage::pop(ctxt);
}

忽略UseGC判断逻辑,两者只是分别简单调用了AutoreleasePoolPagepush()pop(...)静态方法。至此,定位到实现 autorelease pool 的关键数据结构**AutoreleasePoolPage**。

2.2 AutoreleasePoolPage数据结构

AutoreleasePoolPage的实质是 双向链表节点

AutoreleasePoolPage的类图如下(忽略用于校验的magichiwat成员变量)。AutoreleasePoolPageparent成员指向当前节点的上一个节点,若parentnull则表示该节点为双向链表的开始节点;child成员指向当前节点的下一个节点;depth成员表示当前节点的深度,满足depth = parent->depth + 1,可以视为节点在双向链表中的索引;next成员指向AutoreleasePoolPage中下一个可分配的地址。

AutoreleasePoolPage类图.jpg

从类图的成员变量中,似乎找不到用于存储 autorelease object 的成员变量。那AutoreleasePoolPage是如何保存autorelease object的呢?AutoreleasePoolPage重载了new运算符,指定构建AutoreleasePoolPage实例分配定长的4096字节内存空间。

/** 来自系统架构头文件 */
#define I386_PGBYTES            4096
#define PAGE_SIZE               I386_PGBYTES
#define PAGE_MAX_SIZE           PAGE_SIZE

/** 来自AutoreleasePoolPage */
static size_t const SIZE = PAGE_MAX_SIZE; 
static void * operator new(size_t size) {
    return malloc_zone_memalign(malloc_default_zone(), SIZE, SIZE); 
}

为什么构建只占用56个字节的AutoreleasePoolPage实例却分配了4096字节的空间呢?因为除开保存AutoreleasePoolPage实例的sizeof(this)长度的空间,其余空间(其中包括用于保存POOL_SENTINEL的8个字节)均用于 以堆栈后入先出的方式 保存 autorelease object。

不妨将这段空间称为**AutoreleasePoolPage实例的堆栈空间**。以下是一个堆栈空间为空的AutoreleasePoolPage占用内存的示例:

AutoreleasePoolPage的实例空间和堆栈空间.jpg

接下来的章节开始介绍AutoreleasePoolPage是如何操作其堆栈空间的。

2.2.1 AutoreleasePoolPage的堆栈空间

AutoreleasePoolPage定义了以下实例方法 查询其堆栈空间的属性及状态。

id * begin() {
    return (id *) ((uint8_t *)this+sizeof(*this));
}

id * end() {
    return (id *) ((uint8_t *)this+SIZE);
}

bool empty() {
    return next == begin();
}

bool full() { 
    return next == end();
}

bool lessThanHalfFull() {
    return (next - begin() < (end() - begin()) / 2);
}
  • begin()返回指向堆栈空间的初始地址的指针;
  • end()返回指向堆栈空间的结束地址;
  • empty()返回堆栈空间是否为空;
  • full()返回堆栈空间是否已满;
  • lessThanHalfFull()返回堆栈空间是否分配过半。

AutoreleasePoolPage的关键指针的指向如下图所示:

AutoreleasePoolPage示例.jpg

2.2.2 AutoreleasePoolPage基本操作(Private实例方法)

正式分析本节代码之前,需要先弄清楚 hotPage 、coldPage 的概念。hotPage 保存 autorelease pool 当前所分配到的AutoreleasePoolPage;相应地, coldPage 是从 hotPage 开始沿parent指针链回溯找到的第一个分配的AutoreleasePoolPage

源代码如下:

static pthread_key_t const key = AUTORELEASE_POOL_KEY;

static inline AutoreleasePoolPage *hotPage() 
{
    AutoreleasePoolPage *result = (AutoreleasePoolPage *) tls_get_direct(key);
    return result;
}

static inline void setHotPage(AutoreleasePoolPage *page) 
{
    tls_set_direct(key, (void *)page);
}

static inline AutoreleasePoolPage *coldPage() 
{
    AutoreleasePoolPage *result = hotPage();
    if (result) {
        while (result->parent) {
            result = result->parent;
        }
    }
    return result;
}

static inline void *tls_get_direct(tls_key_t k) 
{ 
    assert(is_valid_direct_key(k));

    if (_pthread_has_direct_tsd()) {
        return _pthread_getspecific_direct(k);
    } else {
        return pthread_getspecific(k);
    }
}
static inline void tls_set_direct(tls_key_t k, void *value) 
{ 
    assert(is_valid_direct_key(k));

    if (_pthread_has_direct_tsd()) {
        _pthread_setspecific_direct(k, value);
    } else {
        pthread_setspecific(k, value);
    }
}

其中,tls_get_direct()tls_set_direct()函数分别通过pthread_getspecific()pthread_setspecific()函数,使用Key-Value方式访问线程的私有空间。显然 hotPage 是与线程关联的,在不同的线程上调用AutoreleasePoolPage类的hotPage()静态方法返回的是不同的AutoreleasePoolPage实例。代码中的keyAutoreleasePoolPage的一个const静态变量。

2.2.2.1 添加对象

id *add(id obj)实例方法用于向当前AutoreleasePoolPage节点的堆栈空间添加 autorelease object。具体过程是:

  • obj写入next指向的内存地址;
  • next递增(由于next指针占8个字节,因此next递增实质是所指向的地址增加8);
id *add(id obj)
{
    assert(!full());
    id *ret = next;  // faster than `return next-1` because of aliasing
    *next++ = obj;
    return ret;
}

注意:assert(!full())断言仅在堆栈空间未满的情况下才能调用add(...)

2.2.2.2移除对象

void releaseUntil(id *stop)实例方法用于释放AutoreleasePoolPage中,比*stop更晚推入堆栈空间的所有 autorelease object。步骤如下:

  • this->next到达stop之前,进行以下迭代;
  • 找到 hotPage,若 hotPage为空,则沿parent指针一直回溯找到第一个非空的AutoreleasePoolPage,并将其设为 hotPage;
  • next指针递减,obj指向next的内容,重置page->next内存地址中的内容为0xA3A3A3A3
  • obj != POOL_SENTINEL,则调用objc_release(obj)释放obj
  • 完成以上迭代后,将当前AutoreleasePoolPage设置为 hotPage。
static uint8_t const SCRIBBLE = 0xA3;  // 0xA3A3A3A3 after releasing
void releaseUntil(id *stop) 
{
    while (this->next != stop) {
        AutoreleasePoolPage *page = hotPage();
        while (page->empty()) {
            page = page->parent;
            setHotPage(page);
        }

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

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

    setHotPage(this);

#if DEBUG
    for (AutoreleasePoolPage *page = child; page; page = page->child) {
        assert(page->empty());
    }
#endif
}

注意:#if DEBUG块中表示,调用releaseUtil()后,stop指针所在的AutoreleasePoolPagechild链上的所有节点都应该为空。

void releaseAll()删除堆栈空间中的所有对象。

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

void kill()从双向链表中移除当前AutoreleasePoolPage节点后的所有节点,包括节点本身。注意kill()不包含释放对象的操作,只是简单移除节点。

void kill() 
{
    AutoreleasePoolPage *page = this;
    while (page->child) page = page->child;

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

2.2.3 AutoreleasePoolPage基本操作(Private类方法)

id *autoreleaseNewPage(id obj)方法仅在DebugPoolAllocation调试配置项打开时才会使用,忽略。

2.2.3.1 向填满的 page 添加对象

autoreleaseFullPage(id obj, AutoreleasePoolPage *page)向已填满的page添加对象obj。其逻辑:

  • 顺着pagechild指针链循环,找到第一个未填满的AutoreleasePoolPage节点赋值给page;若最后一个AutoreleasePoolPage也是满的,则新建一个AutoreleasePoolPage赋值给page,接到双向链表末尾。退出循环;
  • page其设置为hotPage,将obj推入page的堆栈空间。
static __attribute__((noinline))
id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
{
    assert(page == hotPage());
    assert(page->full()  ||  DebugPoolAllocation);

    do {
        if (page->child) page = page->child;
        else page = new AutoreleasePoolPage(page);
    } while (page->full());

    setHotPage(page);
    return page->add(obj);
}
/** 新建以传入参数newParent为父节点的AutoreleasePoolPage节点 */
AutoreleasePoolPage(AutoreleasePoolPage *newParent) 
    : magic(), next(begin()), thread(pthread_self()),
      parent(newParent), child(nil), 
      depth(parent ? 1+parent->depth : 0), 
      hiwat(parent ? parent->hiwat : 0)
{ 
    if (parent) {
        parent->check();
        assert(!parent->child);
        parent->unprotect();
        parent->child = this;
        parent->protect();
    }
    protect();
}
2.2.3.2 向空 autorelease pool 添加对象

autoreleaseNoPage(id obj)向空 autorelease pool 添加autorelease object,空AutoreleasePool的判断标准是 hotPage 为nil。其逻辑:

  • 创建一个parentnilAutoreleasePoolPage实例page,并设置为hotPage;
  • obj != POOL_SENTINEL,则添加POOL_SENTINELpage
  • 添加objpage
static __attribute__((noinline))
id *autoreleaseNoPage(id obj)
{
    assert(!hotPage());

    AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
    setHotPage(page);

    if (obj != POOL_SENTINEL) {
        page->add(POOL_SENTINEL);
    }

    return page->add(obj);
}

至此可对POOL_SENTINEL有第一步认识:AutoreleasePoolPage节点组成的 双向链表的 开始节点 的堆栈空间 的首地址中,必然保存POOL_SENTINEL

2.2.3.3 向 autorelease pool 添加对象的通用方法

static inline id *autoreleaseFast(id obj)方法用于将obj添加到autorelease pool,即找到合适的AutoreleasePoolPage(实际上就是 hotPage)并调用其add()方法将obj添加到该分页。其逻辑:

  • 若 hotPage 存在且未填满,则将obj直接添加到 hotPage;
  • 若 hotPage 存在且已填满,则调用autoreleaseFullPage(obj, page)
  • 若 hotPage 不存在,则调用autoreleaseNoPage(obj)
static inline id *autoreleaseFast(id obj)
{
    AutoreleasePoolPage *page = hotPage();
    if (page && !page->full()) {
        return page->add(obj);
    } else if (page) {
        return autoreleaseFullPage(obj, page);
    } else {
        return autoreleaseNoPage(obj);
    }
}

2.2.4 AutoreleasePoolPage基本操作(Public类方法)

该节介绍 autorelease pool 暴露给外部的实现 autorelease object 管理的公有类方法。

2.2.4.1 新建 autorelease pool

调用AutoreleasePoolPage::push()新建 autorelease pool。其实现只是简单调用了dest = autoreleaseFast(POOL_SENTINEL)并返回destdest实际指向 autorelease pool 的首个AutoreleasePoolPage节点的 堆栈空间的 起始地址,必定是个POOL_SENTINEL

至此可对POOL_SENTINEL有第二步认识:autorelease pool 的堆栈空间必然以POOL_SENTINEL为起始

static inline void *push() 
{
    id *dest;
    dest = autoreleaseFast(POOL_SENTINEL);

    assert(*dest == POOL_SENTINEL);
    return dest;
}
2.2.4.2 释放 autorelease pool

调用AutoreleasePoolPage::pop(void *token)释放 autorelease pool。

首先需要了解pageForPointer(const void *token)方法。该方法是用来获取token指针所在分页,其实现是offset = token % SIZE获取token的偏移量,然后result = (AutoreleasePoolPage *)(token - offset)获得所在分页的地址。为什么可以这么计算呢?2.2节中提到的AutoreleasePoolPage类的new运算符重载是关键:malloc_zone_memalign(malloc_default_zone(), SIZE, SIZE)指定了分配内存时按SIZE对齐,也就是说__AutoreleasePoolPage实例的内存首地址一定是4096的整数倍__。

实现代码稍微较长,删除不关心的逻辑得到以下代码。

static inline void pop(void *token) 
{
    AutoreleasePoolPage *page;
    id *stop;

    page = pageForPointer(token);
    stop = (id *)token;
    if (*stop != POOL_SENTINEL) {
        _objc_fatal("invalid or prematurely-freed autorelease pool %p; ", 
                    token);
    }

    page->releaseUntil(stop);

    if (page->child) {
        if (page->lessThanHalfFull()) {
            page->child->kill();
        }
        else if (page->child->child) {
            page->child->child->kill();
        }
    }
}

static AutoreleasePoolPage *pageForPointer(const void *p) 
{
    return pageForPointer((uintptr_t)p);
}

static AutoreleasePoolPage *pageForPointer(uintptr_t p) 
{
    AutoreleasePoolPage *result;
    uintptr_t offset = p % SIZE;

    assert(offset >= sizeof(AutoreleasePoolPage));

    result = (AutoreleasePoolPage *)(p - offset);
    return result;
}

pop(void *token)方法的处理逻辑如下:

  • token记为stop,stop命名更能表达传入参数的在方法内部的角色,表示 autorelease pool 释放对象到stop终止;
  • 找到stop所在的AutoreleasePoolPage赋值给page
  • 限定stop必须为POOL_SENTINEL,否则抛出异常;
  • page->releaseUntil(stop)从 autorelease pool 的堆栈空间弹出对象,直到stop地址为止;
  • page->child为非空则需要调用kill()方法清理 autorelease pool 中不必要的空节点:判断若page填满未过半,则删掉pagechild链上的所有节点;若page填满过半且page->child->child为非空,则保留page->child节点而删掉page->childchild链上的所有节点。

采用上述最后一点的处理策略是为了清理掉无用的AutoreleasePoolPage占用空间的同时,又保留一定的缓冲空间,以避免刚释放完AutoreleasePoolPage又不得不马上新建的情况。

2.2.4.3 添加对象到 autorelease pool

使用id autorelease(id obj)类方法添加 autorelease object 到 autorelease pool,只是简单调用了autoreleaseFast(obj)私有类方法。

static inline id autorelease(id obj)
{
    assert(obj);
    assert(!obj->isTaggedPointer());
    id *dest __unused = autoreleaseFast(obj);
    assert(!dest  ||  *dest == obj);
    return obj;
}

AutoreleasePoolPage暴露的三个主要接口可以看出,autorelease pool 对 autorelease object 的操作,遵循 逐个添加、批量释放的原则

2.2.5 Autorelease Pool 与线程

前文提及 hotPage 和 codePage 的实现,线程中私有空间中保存了 hotPage 的地址,因此在不同的线程上调用AutoreleasePoolPage类的hotPage()静态方法时,返回的是不同的AutoreleasePoolPage实例。AutoreleasePoolPage双向链表中的所有节点的堆栈空间,实际是统一的整体,它是一条线程上创建的所有 autorelease pool 的堆栈。

线程与AutoreleasePoolPage的关系如下图所示。假设App使用了三条线程主线程Thread_Main、后台线程Thread_A及Thread_B,其中红色箭头表示线程与AutoreleasePoolPage之间的关联,线程通过私有空间中的 Key-Value 映射可以获取到该线程的 hotPage,AutoreleasePoolPage 通过thread指针可获取其关联线程;蓝色箭头表示AutoreleasePoolPage之间使用双向链表通过parentchild指针关联;

autorelease pool 与线程.jpg

Autorelease pool 与 RunLoop 也有非常紧密的关系。App 启动后再主线程 RunLoop 会注册两个 Observer,第一个 Observer 监听 Entry 事件,其回调会调用objc_autoreleasePoolPush()函数创建自动释放池;第二个Observer监听两个事件,监听到BeforeWaiting(即将进入休眠)时调用objc_autoreleasePoolPop()函数释放旧的 autorelease pool 并调用objc_autoreleasePoolPush()函数建立新的 autorelease pool ;监听到 Exit 事件时,调用objc_autoreleasePoolPop(void *ctxt)函数释放 autorelease pool (顺便一提:调用pop传入的ctxt参数实际上是调用push新建 autorelease pool 时返回的POOL_SENTINEL的地址)。

2.2.6 理解 POOL_SENTINEL

Autorelease pool 的本质是__AutoreleasePoolPage双向链表中的某段堆栈空间__。双向链表上的 autorelease pool 之间通过POOL_SENTINEL分隔POOL_SENTINEL是全面理解 autorelease pool 实现的关键。

POOL_SENTINEL的定义其实非常简单:

#define POOL_SENTINEL nil

首先autorelease(id obj)方法中assert(obj)断言限定 添加到 autorelease pool 中的对象不能为空,但这只是限定了对象添加到 autorelease pool 当时不能为空;其次在添加后,堆栈空间中的对象引用也不可能变为空,因为堆栈空间中已分配的存储单元(8个字节空间)存储的是指向对象的指针,实质为对象的内存地址,无论对象是否已释放,在 autorelease pool 释放之前,该指针的值始终会是该内存地址。因此,autorelease pool 的已分配堆栈空间中,除了POOL_SENTINEL外不可能存在其他nil指针

如果使用weak指针呢?如以下代码,释放objweakRef指针自动置nil后,autorelease pool 堆栈空间中对应的指针是否也被置nil呢?

id obj = [[NSObject alloc] init]; //引用计数:1
__weak id weakRef = obj;

[weakRef autorelease]; //引用计数:1;weakRef:NSObject
[obj release];  // 引用计数:0;weakRef:nil
obj = nil;

用两张图表示上述5句代码执行过程中,内存中到底发生了什么:

第1、2、3句代码.jpg

第4、5句代码.jpg

  • 第1句:见图一绿色文字,在内存栈中分配8个字节保存obj指针,在内存堆中分配连续空间保存实例化的NSObject的实例,假设实例地址为0x60ACC008800,置obj指向0x60ACC008800
  • 第2句:见图一蓝色文字,在内存栈中分配8个字节保存weakRef指针(忽略weak指针实现、引用计数实现等机制 使用到的其他内存),置weakRef指向0x60ACC008800
  • 第3句:见图一红色文字,将obj指针指向的对象的引用添加到 autorelease pool,实质上是将对象的内存地址0x60ACC008800推入 autorelease pool 堆栈空间的栈顶;
  • 第4、5句:见图二黄色文字,执行完成后,内存堆中保存对象的内存被释放,内存栈中的weakRef弱指针被自动置nilobj被代码手动置nil,其值均变为0x0,然而 autorelease pool 堆栈空间中原本指向对象的指针则成为 野指针

因此,上述代码不仅不能达到目的,且__运行以上代码程序会崩溃__。原因是:对象release操作后,obj对象被释放,堆栈空间中指向obj的指针就变成了野指针,autorelease pool 释放对象调用指针指向对象的release方法时必然抛EXC_BAD_ACCESS错误。

三、总结

总结本文要点如下:

  • Autorelease 是将内存堆中的对象统一交由 autorelease pool 管理的一种内存管理方式。NSObject对象调用autorelease方法时,对象被添加到 autorelease pool 中(内部逻辑不调用retain方法因此refCount不会递增)。在合适的时候,如@autoreleasepool块结尾、或者调用了NSAutoreleasePooldrain方法、或者 autorelease pool 所在线程的 Runloop 的 Observer 观察到 Runloop 的 BeforeWaiting 或 Exit 通知等,系统将调用objc_autoreleasePoolPop()函数释放其中所有的autorelease object(内部逻辑有调用objc_release函数因此refCount会递减);

  • Autorelease体现在 Cocoa框架的工厂方法使用中(创建对象会将其自动添加到当前线程的默认 autorelease pool 中)是一种内存的 延迟释放机制,如[UIImage imageNamed:@"xxx"],在短时间内需频繁创建占用内存较大的对象的场景中,需要慎用这些工厂方法;体现在@autoreleasepool块的使用中,则是一种内存的 提前释放机制,对上述场景则可使用@autoreleasepool块提前释放内存;

  • Autorelease pool 的本质是__AutoreleasePoolPage双向链表中的某段堆栈空间__。双向链表上的 autorelease pool 之间通过POOL_SENTINEL分隔

  • 线程的私有空间中保存了AutoreleasePoolPage双向链表的 hotPage 的地址。一条AutoreleasePoolPage双向链表与一条线程关联;

  • 新建 autorelease pool 需要记录返回POOL_SENTINEL的地址;释放 autorelease pool 时,以该地址为token释放其对应的 autorelease pool 中的所有对象;

  • NSObject对象调用autorelease方法时,将对象的内存地址推入 autorelease pool 的堆栈空间,autorelease pool 的已分配堆栈空间中,除了POOL_SENTINEL外不可能存在其他nil指针


参考文章:

[1] 内存管理总结-autoreleasePool

[2] 深入理解RunLoop

[3] opensource-apple/objc4