iOS autoreleasepool 原理解析

一、介绍

autoreleasepool 自动释放池,在池子里的对象如果没有被强引用都会自动释放掉,自动释放池的主要底层数据结构是:__AtAutoreleasePoolAutoreleasePoolPage,调用了autorelease的对象最终都是通过 AutoreleasePoolPage 对象来管理的。

二、源码分析 - clang重写 @autoreleasepool

先关闭ARC,Build Setting 搜索 Objective-C Automatic Reference Counting Yes 改为 No,使用 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp 命令生成C++代码。
main.m代码如下:

    #import <Foundation/Foundation.h>
    #import "Person.h"

    int main(int argc, char * argv[]) {
        @autoreleasepool {
            Person *person = [[[Person alloc]init]autorelease];
        }
        return 0;
    }

main.cpp对应代码如下:

int main(int argc, char * argv[]) {
/* @autoreleasepool */ 
    { 
        __AtAutoreleasePool __autoreleasepool; 
        Person *person = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init")), sel_registerName("autorelease"));
    }
    return 0;
}

__AtAutoreleasePool 结构体对应的代码如下:

/// struct 有点类似 Class
struct __AtAutoreleasePool {
    /// 构造函数 创建对象
    __AtAutoreleasePool() {
        atautoreleasepoolobj = objc_autoreleasePoolPush();
    }
    /// 析构函数 销毁对象
    ~__AtAutoreleasePool() {
        objc_autoreleasePoolPop(atautoreleasepoolobj);
    }
    /// 声明的对象(接收构造函数生成的对象,也是传入析构函数的)
    void * atautoreleasepoolobj;
};

一个@autoreleasepool的 {} 相当于创建和销毁,整个作用域就在大括号内,那么其实开始的main.m里面的代码相当于如下代码:

int main(int argc, char * argv[]) {
    atautoreleasepoolobj = objc_autoreleasePoolPush();

    Person *person = [[[Person alloc]init]autorelease];

    objc_autoreleasePoolPop(atautoreleasepoolobj);
    return 0;
}
三、源码分析 AutoreleasePoolPage类对象

想要知道objc_autoreleasePoolPush和Pop做了什么,那就得去看看objc4源码里面具体做了啥,可以看到Push是调用了 AutoreleasePoolPage 类的 push() 方法,反之就是 pop()方法,所以自动释放池就是 AutoreleasePoolPage 对象来管理的。

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

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

AutoreleasePoolPage 类大概有600多行代码,截取部分代码展示如下:

class AutoreleasePoolPage
{
    magic_t const magic;
    /// 指向下一个可放对象的地址
    id *next;
    /// 在固定的一个线程执行,线程一一对应,线程之间的释放池是分开的
    pthread_t const thread;
    /// 链表父指针,指向上一个Page对象
    AutoreleasePoolPage * const parent;
    /// 链表子指针,指向下一个Page对象
    AutoreleasePoolPage *child;
    uint32_t const depth;
    uint32_t hiwat;

    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();
    }
    id * begin() {
        return (id *) ((uint8_t *)this+sizeof(*this));
    }
    id * end() {
        /// 以前这个SIZE = PAGE_MAX_SIZE = PAGE_SIZE = 4096
        /// 现在好像是SIZE = PAGE_MAX_SIZE = 1 << 14,就是二进制的100000000000000 十进制的1乘以2的14次方,即2的14次方 = 16384,还是说我下载的[obj4](https://opensource.apple.com/tarballs/objc4/)代码版本不对,望知道的大佬告诉我一下
        return (id *) ((uint8_t *)this+SIZE);
    }
    ~AutoreleasePoolPage()
    {
        check();
        unprotect();
        assert(empty());
        assert(!child);
    }
    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()
    {
        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);
    }
public:
    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;
    }
    /// 方法实现省略
    static inline void *push() {}
    static inline void pop(void *token) {}
    static void init() {}
    void print() {}
    static void printAll() {}
    static void printHiwat() 
};

上面有个 end() 方法可以知道每个 AutoreleasePoolPage 对象的大小,以前这个SIZE = PAGE_MAX_SIZE = PAGE_SIZE = 4096,现在好像是SIZE = PAGE_MAX_SIZE = 1 << 14,就是二进制的100000000000000 十进制的1乘以2的14次方,即2的14次方 = 16384,还是说我下载的obj4代码版本不对,望知道的大佬告诉我一下,以下就先按照4096来讲。

每个 AutoreleasePoolPage 对象占用4096字节内存,除了用来存放它内部的成员变量,剩下的空间用来存放 autorelease 对象(调用了autorelease方法的对象,如上面的person对象)的地址所有的 AutoreleasePoolPage 对象通过双向链表的形式连接在一起,第一个对象的parent指向的是空,child指向下一个对象,下一个对象的parent指向上一个对象,child指向下一个对象,这样循环最后一个对象的child指向是空,这就是双向链表结构。

image.png

假设第一个 AutoreleasePoolPage 对象的起始地址是 0x1000,那么结束地址就是 0x2000,16进制换算一下 0x1000 = 4096,自己所带的7个成员变量占56个字节,所以能够存放 autorelease 对象的地址就是0x1038~0x2000,也可以通过 begin()方法算出开始地址是多少。

四、 objc_autoreleasePoolPush,objc_autoreleasePoolPop,autorelease 方法做了什么
  • AutoreleasePoolPage 类对象中*next 指向了下一个能存放 autorelease 对象地址的区域 。

  • 调用 objc_autoreleasePoolPush 方法会将一个 POOL_BOUNDARY 入栈(数据结构算是栈结构),并且返回其存放的内存地址。

  • 调用 objc_autoreleasePoolPop 方法时传入一个 POOL_BOUNDARY 的内存地址,会从最后一个入栈的对象调用对象的 release 方法,直到遇到这个 POOL_BOUNDARY,可以在上面的部分源码 releaseAll()方法里面查看到。

  • 对象的 autorelease 方法 通过断点看汇编代码发现方法实际是调用了 objc_autorelease() 方法,把对象添加到Page对象中去,通过objc4源码可以看到调用顺序如下。

1、obj->autorelease()
2、rootAutorelease()
3、rootAutorelease2()
4、AutoreleasePoolPage类的autorelease()
5、autoreleaseFast()
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);
        }
    }

objc_autoreleasePoolPush,objc_autoreleasePoolPop

int main(int argc, char * argv[]) {
    @autoreleasepool {
        for (int i = 0; i<1000; i++) {
            Person *person = [[[Person alloc]init]autorelease];
        }
    }
    /**上面代码 翻译过来基本就是下面这样*/
    /// 将 POOL_BOUNDARY 入栈 返回地址其实就是 0x1038 因为第一个可存放对象的地址就是它
    atautoreleasepoolobj = objc_autoreleasePoolPush();
    // atautoreleasepoolobj = 0x1038;

    /// 循环将 person 对象放入到 AutoreleasePoolPage 对象去,不够就新建一个Page对象child指针指向
    for (int i = 0; i<1000; i++) {
        Person *person = [[[Person alloc]init]autorelease];
    }
    /// 将 0x1038 传入,从栈顶开始释放对象,一直释放到 0x1038 对象为止
    objc_autoreleasePoolPop(atautoreleasepoolobj);
    // objc_autoreleasePoolPop(0x1038);
    return 0;
}

如果是 autoreleasepool 嵌套,其实也是一样的,没有用完一个page对象的空间不会再生成一个新的page对象,而是在原对象中继续压入POOL_BOUNDARY来区分。

int main(int argc, char * argv[]) {
    /// 创建一个 AutoreleasePoolPage 对象
    @autoreleasepool { /// pool_boundary_1 = objc_autoreleasePoolPush(), POOL_BOUNDARY 压进去
        /// person1 对象 压入
        Person *person1 = [[[Person alloc]init]autorelease];
        /// person2 对象 压入
        Person *person2 = [[[Person alloc]init]autorelease];
    
        /// 没有超出 4096 字节,不用再创建 AutoreleasePoolPage 对象
        @autoreleasepool { /// pool_boundary_2 = objc_autoreleasePoolPush(), POOL_BOUNDARY 再压进去
            /// person3 对象 压入
            Person *person3 = [[[Person alloc]init]autorelease];
            /// person4 对象 压入
            Person *person4 = [[[Person alloc]init]autorelease];
        } /// objc_autoreleasePoolPop(pool_boundary_2), 从栈顶开始释放到 pool_boundary_2 为止 

        /// person5 对象 压入
        Person *person5 = [[[Person alloc]init]autorelease];
        /// person6 对象 压入
        Person *person6 = [[[Person alloc]init]autorelease];
    } /// objc_autoreleasePoolPop(pool_boundary_1), 从栈顶开始释放到 pool_boundary_1 为止
    return 0;
}
五、通过 _objc_autoreleasePoolPrint 方法查看自动释放池的具体情况

objc4 源码的 NSObject.mm 文件中有一个 _objc_autoreleasePoolPrint 方法,实际调用了 AutoreleasePoolPage的打印输出方法,我们来看看能看到哪些信息。

void _objc_autoreleasePoolPrint(void)
{
    AutoreleasePoolPage::printAll();
}

例子代码如下

/// 需要先这样定义一下方法名,这样才能调用,runtime才会去Foundation框架去找实现的方法
extern void _objc_autoreleasePoolPrint(void);

int main(int argc, char * argv[]) {
    @autoreleasepool {
        Person *person1 = [[[Person alloc]init]autorelease];
        Person *person2 = [[[Person alloc]init]autorelease];
    
        _objc_autoreleasePoolPrint();
    
        @autoreleasepool {
            Person *person3 = [[[Person alloc]init]autorelease];
            Person *person4 = [[[Person alloc]init]autorelease];
        }

    }
    return 0;
}

打印如下,

  • 3 release pending 是因为第一个是Push压入的 POOL_BOUNDARY 所以 POOL 0x7fe4e380e038 代表的就是,然后才是person1,person2对象。
  • PAGE (hot) (cold) 代表的就是一个 AutoreleasePoolPage 对象,(hot) 代表的是当前正在使用的
objc[10164]: ##############
objc[10164]: AUTORELEASE POOLS for thread 0x103d34600
objc[10164]: 3 releases pending.
objc[10164]: [0x7fe4e380e000]  ................  PAGE  (hot) (cold)
objc[10164]: [0x7fe4e380e038]  ################  POOL 0x7fe4e380e038
objc[10164]: [0x7fe4e380e040]    0x6000020440c0  Person
objc[10164]: [0x7fe4e380e048]    0x6000020440d0  Person
objc[10164]: ##############

再换一个例子

/// 需要先这样定义一下方法名,这样才能调用,runtime才会去Foundation框架去找实现的方法
extern void _objc_autoreleasePoolPrint(void);

int main(int argc, char * argv[]) {
    @autoreleasepool {
        Person *person1 = [[[Person alloc]init]autorelease];
        Person *person2 = [[[Person alloc]init]autorelease];
    
        @autoreleasepool {
            for (int i = 0; i<1000; i++) {
                Person *person3 = [[[Person alloc]init]autorelease];
            }
        
            _objc_autoreleasePoolPrint();
        }
    }
    return 0;
}

打印如下,第一个 PAGE 多了一个 (full),代表第一个对象满了,没有了 (hot) 代表不是当前使用页,当前使用页切到下一个 PAGE 对象上了。

objc[10354]: ##############
objc[10354]: AUTORELEASE POOLS for thread 0x110cf5600
objc[10354]: 1004 releases pending.
objc[10354]: [0x7fd8fb009000]  ................  PAGE (full)  (cold)
objc[10354]: [0x7fd8fb009038]  ################  POOL 0x7fd8fb009038
objc[10354]: [0x7fd8fb009040]    0x600000f1c020  Person
objc[10354]: [0x7fd8fb009048]    0x600000f1c030  Person
objc[10354]: [0x7fd8fb009050]  ################  POOL 0x7fd8fb009050
objc[10354]: [0x7fd8fb009058]    0x600000f1c040  Person
......
......
......
objc[10354]: [0x7fd8fb00b000]  ................  PAGE  (hot)
......
......
......
objc[10354]: [0x7fd8fb00bfc8]    0x600000f1feb0  Person
objc[10354]: ##############
六、Runloop与Autorelease的关系(对象到底在什么时候调用对象的release方法)

先来看一段代码,MRC演示环境。

image.png
根据上面的源码跟读我们可以知道,第一个 person 对象在 autoreleasepool的大括号结束的时候会调用对象的 release 方法。一眼看代码,其实感觉第二个person对象可能是加入到 main 函数里面的 autoreleasepool 里面去了,如果是那么理论上就是App不结束都不会释放,如果创建巨多的对象,那这样显然不合理,所以我们看到其实第二个 person 对象也释放了,那么第二个对象是在viewDidload大括号结束后就立即调用release方法引用计数-1的么?
image.png
按照上图的例子来看好像并不是这样,先执行的 viewwillAppear 方法,那么具体原因是为何呢,其实和 RunLoop 有关,iOS在主线程的Runloop中注册了2个跟 Autoreleasepool 相关的 Observer

1、第1个 Observer 监听了 kCFRunLoopEntry 事件,会调用 objc_autoreleasePoolPush()

2、第2个 Observer 监听了 kCFRunLoopBeforeWaiting 事件,会调用objc_autoreleasePoolPop()、objc_autoreleasePoolPush() ,监听了kCFRunLoopBeforeExit事件,会调用objc_autoreleasePoolPop()

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0), /// 1
    kCFRunLoopBeforeTimers = (1UL << 1), /// 2
    kCFRunLoopBeforeSources = (1UL << 2),/// 4
    kCFRunLoopBeforeWaiting = (1UL << 5),/// 32
    kCFRunLoopAfterWaiting = (1UL << 6),/// 64 
    kCFRunLoopExit = (1UL << 7),/// 128
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};
image.png

以前打印当前RunLoop是可以看到注册的2个Observer的,两个 callout = _wrapRunLoopWithAutoreleasePoolHandleractivities = 0x1 = 1 = kCFRunLoopEntryactivities = 0xa0 = 160 = kCFRunLoopBeforeWaiting | kCFRunLoopExit,但是现在打印看不到了,知道原因的大佬可以留言告诉我一下,谢谢!

结论:

  • MRC时候对象调用 autorelease 方法加入到自动释放池后,release 的时机是在所在的一次 RunLoop 结束的时候释放。
  • ARC 时候由系统管理,系统没有调用 autorelease 方法,而是在合适的地方(离开对象作用域之前)底层调用了 person = nil 方法。
    image.png
    image.png

    image.png

从汇编代码看确实相当于作用域离开前调用了 person = nil; 也就是 objc_storeStrong(&person,nil)

void objc_storeStrong(id *location, id obj)
{
    id prev = *location;
    if (obj == prev) {
        return;
    }
    objc_retain(obj);
    *location = obj;
    objc_release(prev);
}
七、扩展 Tagged Pointer
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;
}

有没有发现 obj->isTaggedPointer() 这里断言了,如果是Tagged Pointer类型对象就不加入到自动释放池里去,因为这种类型数据是存在指针里面的,指针本身也是有内存地址的,本身也能存储少量的数据。Tagged Pointer 详解

如有错误请帮忙指出,谢谢!转载请注明出处,喜欢的话,请点个赞吧!

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

推荐阅读更多精彩内容