超级苦工
阅读 30
iOS 底层拾遗:AutoreleasePool

前言

在阳神的 黑幕背后的Autorelease 文章中已经把 AutoreleasePool 核心逻辑讲明白了,不过多是结论性的东西,笔者通读源码以探究更多的细节,验证一下老生常谈的一些结论。

源码基于 Runtime 750。

一、@autoreleasepool {} 干了些什么

main.m 文件代码:

int main(int argc, const char * argv[]) {
    @autoreleasepool {}
    return 0;
}
复制代码

使用 clang -rewrite-objc main.m 查看经过编译器前端处理的代码:

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

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; }
    return 0;
}
复制代码

可以看出@autoreleasepool{}会创建一个__AtAutoreleasePool类型的局部变量并包含在当前作用域,__AtAutoreleasePool构造和析构时分别调用了两个方法,所以简化过程如下:

void *context = objc_autoreleasePoolPush()
// 对象调用 autorelease 装入自动释放池
objc_autoreleasePoolPop(context)
复制代码

可以猜测 push 和 pop 操作是实现自动释放的关键。

二、AutoreleasePoolPage 内存分布

官方文档 中提到了,主线程以及非显式创建的线程(比如 GCD)都会有一个 event loop (RunLoop 就是具体实现),在 loop 的每一个循环周期的开始和结束会分别调用自动释放池的 push 和 pop 方法,由此来实现自动的内存管理。

objc_autoreleasePoolPush()objc_autoreleasePoolPop(...)实际上会调用到AutoreleasePoolPage类的push()pop()方法,先看一下这个类的数据结构:

class AutoreleasePoolPage {
    ...
    magic_t const magic;
    id *next;
    pthread_t const thread;
    AutoreleasePoolPage * const parent;
    AutoreleasePoolPage *child;

    static void * operator new(size_t size) {
        return malloc_zone_memalign(malloc_default_zone(), SIZE, SIZE);
    }
    id * begin() {
        return (id *) ((uint8_t *)this+sizeof(*this));
    }
    id * end() {
        return (id *) ((uint8_t *)this+SIZE);
    } 
    ...
}
复制代码
  • parentchild正是指向前驱和后继指针,自动释放池就是一个以AutoreleasePoolPage为节点的双向链表(后文验证)。
  • thread是指当前 page 所对应的线程。
  • magic用于校验内存是否损坏。
  • next指向当前可插入对象的地址。

内存对齐

重写了new运算符,使用了malloc_zone_memalign(...)进行内存分配:

extern void *malloc_zone_memalign(malloc_zone_t *zone, size_t alignment, size_t size) ;
    /* 
     * Allocates a new pointer of size size whose address is an exact multiple of alignment.
     * alignment must be a power of two and at least as large as sizeof(void *).
     * zone must be non-NULL.
     */
复制代码

注释说得很清楚了,这个方法以alignment对齐的地址分配size的内存空间。调用时两个参数都使用了SIZE宏,实际上就是虚拟内存页的大小:

#define I386_PGBYTES            4096
复制代码

一个 page 的内存空间设置过小会导致更多的开辟空间操作降低效率,大量的parent/child指针变量也会占用可观的内存;空间设置过大可能会导致一个 page 的利用率低浪费过多内存。设置为 4096 是比较考究的,在保证内存对齐的情况下最大化利用空间避免内存碎片。这么做过后 page 的地址总是 4096 的整数倍,可以让某些运算更便捷(比如后文会说的通过指针地址寻找对应的 page)。

begin() 与 end()

AutoreleasePoolPage本身的大小远不及 4096,而超出的空间正是用来存放“期望被自动管理的对象”。begin()end()方法标记了这个范围。

sizeof(*this)表示AutoreleasePoolPage本身的大小,那么(uint8_t *)this+sizeof(*this)就是最低地址,(uint8_t *)this+SIZE就是最高地址。逐个插入对象时,next指针从begin()end()逐个移动,后面的full()方法就是指next == end()empty()就是指next == begin()

值得注意的是next/end()/begin()等都是id *类型的,即指向指针的指针,进行 +1 -1 运算时移动的是一个id大小的距离。

三、push 逻辑

push()方法会调用autoreleaseFast(POOL_BOUNDARY)

    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);
        }
    }
复制代码

hotPage 指的是当前可插入对象的 page,放到后面一点分析,先来看插入对象的逻辑,分三种情况:

1、当 page 存在且没满时,直接添加对象:

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

unprotect()/protect()内部使用了int mprotect(void *a, size_t b, int c),设置内存起点a长度b的内存区域为c类型的访问限制:

    inline void protect() {
#if PROTECT_AUTORELEASEPOOL
        mprotect(this, SIZE, PROT_READ);
        check();
#endif
    }
    inline void unprotect() {
#if PROTECT_AUTORELEASEPOOL
        check();
        mprotect(this, SIZE, PROT_READ | PROT_WRITE);
#endif
    }
复制代码

unprotect()设置为可读可写,protect()设置为只读,所以这里的目的是保证 page 写安全。不过有#define PROTECT_AUTORELEASEPOOL 0定义说明目前版本还没有开放这个保护功能。

2、当 page 存在且满了时,拓展 page 节点并添加对象:

    static __attribute__((noinline))
    id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
    {   ...
        do {
            if (page->child) page = page->child;
            else page = new AutoreleasePoolPage(page);
        } while (page->full());

        setHotPage(page);
        return page->add(obj);
    }
复制代码

循环的逻辑:从 child 方向找到未满的 page,若找不到则创建一个新 page 拼接到链表尾部(AutoreleasePoolPage 构造方法会把传入的 page 参数作为 parent 前驱对象)。后面再设置最新的 page 为 hotpage 并将 obj 添加进 page。

3、当 page 不存在时,初始化一个

    static __attribute__((noinline))
    id *autoreleaseNoPage(id obj) 
    {   ...
        AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
        setHotPage(page);
        ...
        return page->add(obj);
    }
复制代码

这个方法核心就是创建第一个 page 然后加入线程局部存储。

hotPage

从上面的push()方法分析可知,被自动管理的对象会不断插入双向链表从前到后第一个未满 page ,hotPage()其实就是指向这个 page,还有个coldPage()方法是根据hotPage()找到第一个 page。

既然自动释放池是由AutoreleasePoolPage组成的双向链表,那这个链表该如何访问呢?可能常规的思路是创建一个全局变量来访问它,不过这里使用了另外一个方式:

    static inline AutoreleasePoolPage *hotPage() 
    {
        AutoreleasePoolPage *result = (AutoreleasePoolPage *)
            tls_get_direct(key);
        // EMPTY_POOL_PLACEHOLDER 表示没有 page
        if ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil;
        if (result) result->fastcheck();
        return result;
    }
    static inline void setHotPage(AutoreleasePoolPage *page) 
    {
        if (page) page->fastcheck();
        tls_set_direct(key, (void *)page);
    }
复制代码

tls_get_direct(...)tls_set_direct(...)内部就是使用线程的局部存储(TLS: Thread Local Storage)将 page 存储起来,这样可以避免维护额外的空间来记录尾部的 page。由此也验证了自动释放池与线程一一对应的关系。

在 YYKit 中有一个使用广泛的技巧:将某个对象最后使用时放在异步线程,如果这个对象释放就(可能?)会在这个异步线程,从而降低主线程压力。实际上就是编译器插入 autorelease 代码将对象加入到异步线程的自动释放池,而如果异步线程的释放池先于主线程的释放池pop()而调用对象的release()方法,那么这个对象如果释放就会在异步线程。所以笔者认为这个优化并非绝对有效(这里衍生出一个问题:一个对象被多个自动释放池管理,若对象释放这些释放池怎么避免的野指针问题?)。

POOL_BOUNDARY

push()方法调用autoreleaseFast(POOL_BOUNDARY)时传入的是一个 POOL_BOUNDARY 并非需要被管理的对象,它的定义如下:

#   define POOL_BOUNDARY nil
复制代码

在调用autoreleaseFast(obj)方法会返回指向obj指针的指针,它是一个id *类型,也就是说,这个返回值关心的只是obj指针的地址,而不是obj值的地址,obj指针的地址就是对应AutoreleasePoolPage对象内存中的某段区域。

再看一下上层调用:

void *context = objc_autoreleasePoolPush()
...
objc_autoreleasePoolPop(context)
复制代码

pop 时会将这个obj指针的地址传入进去。pop 的逻辑是把 hotPage 里面装的对象依次移除并发送 release 消息(后面会详细分析),当前 page 移除完了,继续移除 parent 节点内的对象,以此反复,而移除对象操作何时停止就是到这个obj指针的地址。

所以,push 操作加入一个 POOL_BOUNDARY 实际上就是加一个边界,pop 操作时根据边界判断范围,这就是一个入栈与出栈的过程。

magic 校验

多次出现的check()方法如下:

    void check(bool die = true)  {
        if (!magic.check() || !pthread_equal(thread, pthread_self())) busted(die);
    }
    void fastcheck(bool die = true)  {
//补充:#define CHECK_AUTORELEASEPOOL (DEBUG)
#if CHECK_AUTORELEASEPOOL
        check(die);
#else
        if (! magic.fastcheck()) busted(die);
#endif
    }
复制代码

可以看到,它们都调用了magic的 check 方法,在 DEBUG 时还会去检查当前线程是否与 page 的线程一致。

magicmagic_t类型的,这个结构体主要是有个uint32_t m[4];数组,构造时内存直接会写为0xA1A1A1A1 AUTORELEASE!,然后check()逻辑就是判断构造时的值是否发生了改变,若发生改变说明这个 page 已经被破坏。

四、autorelease 逻辑

上层对象调用 autorelease 方法会调用到AutoreleasePoolPage的以下方法:

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

显然,最终还是会调用前面解析的autoreleaseFast(...)方法进行对象插入。由此也可以推断,在一个 Thread 没有 Runloop 自动执行自动释放池的 push 和 pop 时,对象进行 autorelease 时若发现没有自动释放池节点会自动创建 page 并加入线程局部存储(参考前面的autoreleaseNoPage(...)方法分析)。

五、pop 逻辑

objc_autoreleasePoolPop(context)context参数是objc_autoreleasePoolPush()返回的,实际上就是POOL_BOUNDARY对应的在AutoreleasePoolPage中的地址。最终会调用到pop()方法:

    static inline void pop(void *token) 
    {
        AutoreleasePoolPage *page;
        id *stop;
        ...
        // 拿到 token 边界对应的 page
        page = pageForPointer(token);
        stop = (id *)token;
        ...
        // pop 内部对象直到 stop 边界
        page->releaseUntil(stop);
        ...
        // 删除空的 child 链表节点,如果当前页对象超过一半,保留下一个空节点
        if (page->lessThanHalfFull()) {
            page->child->kill();
        }
        else if (page->child->child) {
            page->child->child->kill();
        }
    }
复制代码

pop()的逻辑应该很好理解了,token参数就是边界,下面分别分析步骤:

找到边界对应的 page

    static AutoreleasePoolPage *pageForPointer(const void *p) {
        return pageForPointer((uintptr_t)p);
    }
    static AutoreleasePoolPage *pageForPointer(uintptr_t p) {
        AutoreleasePoolPage *result;
        uintptr_t offset = p % SIZE;
        ....
        result = (AutoreleasePoolPage *)(p - offset);
        result->fastcheck();
        return result;
    }
复制代码

看上面个函数,const void *p是指针的指针,((uintptr_t)p)才表示POOL_BOUNDARY指针在对应 page 中的地址。

看下面个函数,前面分析过内存对齐的处理,那么 page 的起始地址必然是 SIZE (也就是页大小 4096) 的倍数,那么p % SIZE就得到了这个p在 page 中的地址偏移,最后通过p - offset就拿到了 page 的起始地址,这个处理比较秀。

移除被管理对象并发送 release 消息

    void releaseUntil(id *stop)  {
        while (this->next != stop) {
            AutoreleasePoolPage *page = hotPage();
            // 如果当前 page 空了,指向 parent
            while (page->empty()) {
                page = page->parent;
                setHotPage(page);
            }
            // 将即将要移除对象对应 page 中的内存置为 SCRIBBLE
            page->unprotect();
            id obj = *--page->next;
            memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
            page->protect();
            // 调用对象的 release 方法
            if (obj != POOL_BOUNDARY) {
                objc_release(obj);
            }
        }
        // 把当前 page 设置 hotpage(调用时 this 就是对应期望释放边界的 page)
        setHotPage(this);
        ...
    }
复制代码

清除 child

    void kill() {
        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);
    }
复制代码

这个逻辑一目了然了:找到当前 page 的 child 方向尾部 page,然后反向挨着释放并且把其 parent 节点的 child 指针置空。前面也说明了unprotectprotect内部并没有开启写入安全保护。

后语

以上就是自动释放池大部分源码的分析了,这部分源码没有涉及汇编并且代码量比较少,所以看起来相对容易。多理解一些内存管理底层有利于理解各种上层特性、定位内存难题,也有助于写出更稳定的代码。并且在这个过程中,不可避免需要接触操作系统和编译原理相关知识,也算是能培养通识性能力。

读源码远比记结论重要,遇到某些优秀的代码细节往往令人惊喜,不失为一种乐趣。

关注下面的标签,发现更多相似文章
评论
相关推荐
Android 使用 HTTPS

来源: 简书 原文: Android 使用 HTTPS 如果你的项目的网络框架是okhttp,那么使用https还是挺简单的,因为okhttp默认支持HTTPS。传送门 Android 使用 HTTP...

Android 适配一篇就够 - 编译版本?support?API 兼容?图片适配?

来源: 简书 原文: Android 适配一篇就够 - 编译版本?support?API 兼容?图片适配? 本文介绍 Android 不同系统及图片资源的常见适配问题。 compileSdkVersi...

[Kotlin Tutorials 11] Kotlin和Java的双向互操作

来源: 简书 原文: [Kotlin Tutorials 11] Kotlin和Java的双向互操作 Kotlin和Java的双向互操作 Kotlin和Java是有互操作性的(Interoperabi...

Android跳转到获取应用通知权限

来源: 简书 原文: Android跳转到获取应用通知权限 1.判断是否有通知权限 官方只最低支持到API 19(4.4),低于19的只会返回true,目前暂时没有办法获取19以下的系统是否开启了某个...

Android--PathMeasure基本用法

来源: 简书 原文: Android--PathMeasure基本用法 PathMeasure是一个用来测量Path的类 构造方法 //创建一个空的PathMeasure public PathMea...

Android屏幕适配的总结

来源: 简书 原文: Android屏幕适配的总结 屏幕适配的核心:其一,就是适配的效率,即把设计图转化为App界面的过程是否高效,其二如何保证实现UI界面在不同尺寸和分辨率的手机中UI的一致性。 背...

Android热修复之-Frameworks层修复原理分析

来源: 简书 原文: Android热修复之-Frameworks层修复原理分析 说到热修复主要有两种修复方案一种是通过dex替换的方式来达到修复效果、一种是基本native层的修复。dex替换的方式...

[译文]MongoDB WiredTiger引擎调优技巧

来源: 简书 原文: [译文]MongoDB WiredTiger引擎调优技巧 MongoDB从3.0开始引入可插拔存储引擎的概念。当前,有不少存储引擎可供选择:MMAPV1、WiredTiger、M...

知道了这些 MongoDB设计技巧,提升效率50%

来源: 掘金 原文: 知道了这些 MongoDB设计技巧,提升效率50% 范式化设计还是反范式 考虑下这样的场景,我们的订单数据是这样的 商品: { "_id": productI...

Mongo实时聚合千万文档数据

来源: 掘金 原文: Mongo实时聚合千万文档数据 1.前言 大数据的聚合分析在企业中非常有用,有过大数据开发经验的人都知道ES、Mongo都提供了专门的聚合方案来解决这个问题。但是大量数据的实时聚...

历时七天,史上最强MySQL优化总结,从此优化So Easy!

来源: 掘金 原文: 历时七天,史上最强MySQL优化总结,从此优化So Easy! 一、概述 1. 为什么要优化 一个应用吞吐量瓶颈往往出现在数据库的处理速度上 随着应用程序的使用,数据库数据逐渐增...

Mysql 百问系列:B+Tree 到底是什么

来源: 掘金 原文: Mysql 百问系列:B+Tree 到底是什么 前言: 以前看过许多关于B+ Tree的文章,当时看了总觉得明白了,可是没过多久就又要忘了。直到我看了掘金小册:Mysql是怎么运...

LRU算法及其优化策略——Mysql篇

来源: 掘金 原文: LRU算法及其优化策略——Mysql篇 在上一篇文章中,介绍了LRU算法在Redis之中的应用,本篇继续给各位道友介绍在Mysql的InnobDB引擎中,是如何使用LRU算法的。...

mysql order by 优化

来源: 掘金 原文: mysql order by 优化 version : 5.7, from 8.2.1.14 ORDER BY Optimization 本节描述MySQL何时可以使用索引来满足...

MYSQL-多表查询

来源: 掘金 原文: MYSQL-多表查询 首先创建数据表tb_departments create table tb_departments (dept_id INT PRIMARY KEY, de...

MySQL索引和SQL调优

来源: 掘金 原文: MySQL索引和SQL调优 [TOC] MySQL索引和SQL调优 本文有参考网上其他相关文章,本文最后有附参考的链接 MySQL索引 MySQL支持诸多存储引擎,而各种存储引擎...

iOS 底层拾遗:AutoreleasePool

来源: 掘金 原文: iOS 底层拾遗:AutoreleasePool 前言 在阳神的 黑幕背后的Autorelease 文章中已经把 AutoreleasePool 核心逻辑讲明白了,不过多是结论性...

iOS app秒开H5优化探索

来源: 掘金 原文: iOS app秒开H5优化探索 背景 为了快递迭代、更新,公司app有一大模块功能使用H5实现,但是体验比原生差,这就衍生了如何提高H5加载速度,优化体验的问题。此文,记录一下自...

iOS常用宏 定义

来源: 简书 原文: iOS常用宏 定义 iOS开发过程中,使用的一些常用宏定义 字符串是否为空 #define kStringIsEmpty(str) ([str isKindOfClass:[NS...

比较一下iOS中的三种定时器

来源: 掘金 原文: 比较一下iOS中的三种定时器 NSTimer NSTimer是iOS开发中的最常见的定时器。 Timers work in conjunction with run loops....