iOS随记

从Runtime源代码解读内存管理机制——Autorelease

2019-07-21  本文已影响0人  Luminix

2019-07-10

一、概述

上一篇文章 从Runtime源代码解读内存管理机制——Retain/Release,学习了 Objective-C 的引用计数内存管理的实现机制,本文继续探究另一种内存管理方式 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.

其表达的要点如下:

接下来第二章,会从 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; 

    }
}

其逻辑:

最后,进一步剔除结构体的代码,@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);
}

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。具体过程是:

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。步骤如下:

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。其逻辑:

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。其逻辑:

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添加到该分页。其逻辑:

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)方法的处理逻辑如下:

采用上述最后一点的处理策略是为了清理掉无用的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

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

三、总结

总结本文要点如下:

参考文章:
[1] 内存管理总结-autoreleasePool
[2] 深入理解RunLoop
[3] opensource-apple/objc4

上一篇 下一篇

猜你喜欢

热点阅读