分析自动释放池aureleasePool的原理
一、简介
aureleasePool,顾名思义,自动释放池。它在iOS系统的内存管理中,有着极其重要的作用。放入其池中的对象,最终系统通过它进行释放,不用程序员手动去管理。在MRC中,当然还是需要手动调用一个autorelase方法将对象添加进自动释放池,在ARC中,这一步直接省略,只需要在 @autoreleasepool { ... }中创建对象,然后执行代码即可。最后,在对象需要释放的时候,自动释放池自动帮助完成这一行为。
二、概念
自动释放池到底是什么?其实,在对源码进行分析的过程中,发现它是由双向链表构成的,通过栈来管理对象。一句话总结就是,它是以栈为节点通过双向链表实现的一种存储结构。
三、示例
说了这么多,那么放入自动释放池的对象,到底是不是由它来管理内存的呢?现在来验证一下,看看到底是不是由它来释放的,示例代码如下:main.m
#import <Foundation/Foundation.h> __weak NSObject *weak_obj1; __weak NSObject *weak_obj2; int main(int argc, const char * argv[]) { NSObject *obj1 = [[NSObject alloc] init]; weak_obj1 = obj1; NSLog(@"before---weak_obj1--%p",weak_obj1); @autoreleasepool { NSObject *obj2 = [[NSObject alloc] init]; weak_obj2 = obj2; NSLog(@"before---weak_obj2--%p",weak_obj2); } NSLog(@"after---weak_obj1--%p",weak_obj1); NSLog(@"after---weak_obj2--%p",weak_obj2); return 0; }
创建了两个weak指针分别指向对象obj1和ob2,obj1对象在自动释放池之外创建的,obj2对象则是在自动释放池之内创建的。按照上面简介的说明,猜想打印的结果一定是:
- obj1 在 before 和 after 这里,指针的值一直存在,除非到程序执行结束,也即main函数执行完毕,它才被系统释放回收。
- obj2 在 before 这里,指针的值是存在的,但是出了自动释放池的作用域外面后,在after 这里, 指针的值一定为空,表明它由自动释放池进行释放回收了。
打印结果,果然印证了这个猜想,如下所示:
2021-04-16 17:11:04.250446+0800 原理剖析[82323:4507357] before---weak_obj1--0x1031adbe0 2021-04-16 17:11:04.250835+0800 原理剖析[82323:4507357] before---weak_obj2--0x103304830 2021-04-16 17:11:04.250887+0800 原理剖析[82323:4507357] after---weak_obj1--0x1031adbe0 2021-04-16 17:11:04.250917+0800 原理剖析[82323:4507357] after---weak_obj2--0x0 Program ended with exit code: 0
四、结构
结果是喜人的,接着,我们就需要去一探究竟了。它的结构是什么的?如何实现的? 为了更好更直观的分析结构,我们需要做一些前期工作,就是把OC代码转成C++代码。xcode自带了clang编译器,可以借助它进行语法的转换。转换命令行如下:
/* xcrun: xcode run运行。 -sdk:表示转成什么平台下的C++代码。例如 iphoneos,代表手机系统,可以是模拟器,也可以是真机。如果不写,会兼容所有的平台,macos、iphoneos、windowos。 clang:编译器 -arch:表示在选择的此平台下,采用什么样的架构。arm64是真机64bit的,i386是模拟器32bit的。如果不写,会兼容该平台下的所有架构。 -fobjc-arc:表示内存的管理方式,可以不用写,默认ARC。 -fobjc-runtime=ios-8.0.0:weak指针修饰的变量,需要低系统环境支持,此处指定iOS8.0。 -o:output 输出文件 */ // weak 需要运行时环境的支持。当你选择了一个合适的最低版本后,才能编译有 weak 修饰变量的源码。 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m -o mian_arm64.cpp xcrun clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-8.0.0 main.m -o mian.cpp //【忽略:64 warnings generated.】 // 非weak 修饰变量的源码。 xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o mian_arm64.cpp xcrun clang -rewrite-objc main.m -o mian.cpp //【忽略:64 warnings generated.】
可以看到,不同的配置选择,生成c++代码文件大小是不一样的,如下所示:
选择main_arm64.cpp文件点击查看,代码如下,可以发现,在自动释放池那里,其实是生成了一个代码块,内部创建了一个自动释放池对象_autorelasepool,它的类型是__AtAutoreleasePool。
#pragma clang assume_nonnull end __attribute__((objc_ownership(weak))) NSObject *weak_obj1; __attribute__((objc_ownership(weak))) NSObject *weak_obj2; int main(int argc, const char * argv[]) { NSObject *obj1 = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init")); weak_obj1 = obj1; NSLog((NSString *)&__NSConstantStringImpl__var_folders_62_ptthn0jn271412kr3p6_0d3h0000gp_T_main_d51538_mi_0,weak_obj1); /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; NSObject *obj2 = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init")); weak_obj2 = obj2; NSLog((NSString *)&__NSConstantStringImpl__var_folders_62_ptthn0jn271412kr3p6_0d3h0000gp_T_main_d51538_mi_1,weak_obj2); } NSLog((NSString *)&__NSConstantStringImpl__var_folders_62_ptthn0jn271412kr3p6_0d3h0000gp_T_main_d51538_mi_2,weak_obj1); NSLog((NSString *)&__NSConstantStringImpl__var_folders_62_ptthn0jn271412kr3p6_0d3h0000gp_T_main_d51538_mi_3,weak_obj2); return 0; } static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };
全局搜索这个__AtAutoreleasePool,发现它就是一个struct结构体,内部有一个构造函数、一个析构函数。创建时调用objc_autoreleasePoolPush函数,销毁时调用objc_autoreleasePoolPop函数。
struct __AtAutoreleasePool { __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();} ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);} void * atautoreleasepoolobj; };
了解了这个结构后,我们来稍微改造一下当前的这个main_arm64.cpp函数,在大括号起始位置“{”处调用push函数,在大括号结束位置“}”处调用pop函数,更加清晰直观,如下所示:
#pragma clang assume_nonnull end __attribute__((objc_ownership(weak))) NSObject *weak_obj1; __attribute__((objc_ownership(weak))) NSObject *weak_obj2; int main(int argc, const char * argv[]) { NSObject *obj1 = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init")); weak_obj1 = obj1; NSLog((NSString *)&__NSConstantStringImpl__var_folders_62_ptthn0jn271412kr3p6_0d3h0000gp_T_main_d51538_mi_0,weak_obj1); /* @autoreleasepool */ // { __AtAutoreleasePool __autoreleasepool; void * atautoreleasepoolobj = objc_autoreleasePoolPush(); NSObject *obj2 = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init")); weak_obj2 = obj2; NSLog((NSString *)&__NSConstantStringImpl__var_folders_62_ptthn0jn271412kr3p6_0d3h0000gp_T_main_d51538_mi_1,weak_obj2); objc_autoreleasePoolPop(atautoreleasepoolobj) //} NSLog((NSString *)&__NSConstantStringImpl__var_folders_62_ptthn0jn271412kr3p6_0d3h0000gp_T_main_d51538_mi_2,weak_obj1); NSLog((NSString *)&__NSConstantStringImpl__var_folders_62_ptthn0jn271412kr3p6_0d3h0000gp_T_main_d51538_mi_3,weak_obj2); return 0; } static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };
五、源码
现在需要去runtime源码中,看看 objc_autoreleasePoolPush函数 和 objc_autoreleasePoolPop函数的实现了,核心代码如下:
//push入栈 void * objc_autoreleasePoolPush(void) { return AutoreleasePoolPage::push(); } //pop出栈 NEVER_INLINE void objc_autoreleasePoolPop(void *ctxt) { AutoreleasePoolPage::pop(ctxt); }
从上面的方法实现,可以得出结论,自动释放池的实现其实是依赖类AutoreleasePoolPage的。AutoreleasePoolPage继承自结构体AutoreleasePoolPageData,也即
class AutoreleasePoolPage : private AutoreleasePoolPageData { static size_t const SIZE = PAGE_MIN_SIZE; //每一个page最大容量是4096字节 ......... }
所以,接着AutoreleasePoolPageData的最终内部构成,可以看到,有父亲parent page,也有孩子child page,还有深度depth,说明它是个双向链表。里面还有一个next指针,可自增自减,进行存放位置的读取,是一个指针栈,如下所示:
struct AutoreleasePoolPageData { magic_t const magic; //用来校验AutoreleasePoolPage的结构是否完整 __unsafe_unretained id *next; //指向下一个即将产生的autoreleased对象的存放位置(当next == begin()时,表示AutoreleasePoolPage为空;当next == end()时,表示AutoreleasePoolPage已满。 pthread_t const thread; //指向当前线程,一个AutoreleasePoolPage只会对应一个线程,但一个线程可以对应多个AutoreleasePoolPage; AutoreleasePoolPage * const parent;//指向父结点,第一个结点的 parent 值为 nil; AutoreleasePoolPage *child;//指向子结点,最后一个结点的 child 值为 nil uint32_t const depth; //代表深度,第一个page的depth为0,往后每递增一个page,depth会加1; 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) { } };
对AutoreleasePoolPage有了基本认识后,现在深入分析一下它的push函数。继续,分析源代码如下。如果是debug模式下,每个自动释放池从一个新的池页开始;否则,执行快速释放池逻辑。其中,都传入了一个参数POOL_BOUNDARY,翻译为池边界,在这里则表示为哨兵对象,用来作为page存储对象的起始边界条件。
//哨兵入栈
static inline void *push() { id *dest; if (slowpath(DebugPoolAllocation)) { // Each autorelease pool starts on a new pool page. dest = autoreleaseNewPage(POOL_BOUNDARY); } else { dest = autoreleaseFast(POOL_BOUNDARY); } ASSERT(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY); return dest; }
单个autoreleasepool就只有一个哨兵对象,多个嵌套autoreleasepool,就有多个哨兵对象。借用另一位博主的图例如下, abc为图一,单个释放池。d为图二,嵌套释放池。
(a) (b) (c) (d)
在自动释放池创建的对象,系统会自动给每一个对象发送autorelease消息,这样它们就可以被添加到自动释放池。存入自动释放池的对象地址在哨兵对象后面依次递增,最终释放对象的时候,系统会给每一个对象再次发送release消息进行释放,直到遇到哨兵对象后停止,表明对象全部释放完全。
// 对象进自动释放池 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; }
autoreleaseNewPage函数 和 autoreleaseFast函数,它们的实现存在较多相似地方,我们直接去看对应的实现,代码分析如下。
- 首先获取当前的page栈节点,如果存在且未填满,则直接将对对象添加到此page栈中;
- 如果存在但是已经填满,则去寻找它的子page栈,当子page栈不存在的时候,就新建一个子page栈并设置为当前page栈,接着将对对象添加到此page栈中;
- 如果不存在,则懒加载新建一个子page栈并设置为当前page栈,这里会通过haveEmptyPoolPlaceholder函数判断一下是否存在嵌套的自动释放池,然后决定添加新的哨兵对象,最后接着将对象添加到此page栈中。
// 快速处理
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); } }
// 满页处理 static __attribute__((noinline)) id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page) { // The hot page is full. // Step to the next non-full page, adding a new page if necessary. // Then add the object to that 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); }
// 懒加载处理 static __attribute__((noinline)) id *autoreleaseNoPage(id obj) { // "No page" could mean no pool has been pushed // or an empty placeholder pool has been pushed and has no contents yet ASSERT(!hotPage()); bool pushExtraBoundary = false; if (haveEmptyPoolPlaceholder()) { // We are pushing a second pool over the empty placeholder pool // or pushing the first object into the empty placeholder pool. // Before doing that, push a pool boundary on behalf of the pool // that is currently represented by the empty placeholder. pushExtraBoundary = true; } else if (obj != POOL_BOUNDARY && DebugMissingPools) { // We are pushing an object with no pool in place, // and no-pool debugging was requested by environment. _objc_inform("MISSING POOLS: (%p) Object %p of class %s " "autoreleased with no pool in place - " "just leaking - break on " "objc_autoreleaseNoPool() to debug", objc_thread_self(), (void*)obj, object_getClassName(obj)); objc_autoreleaseNoPool(obj); return nil; } else if (obj == POOL_BOUNDARY && !DebugPoolAllocation) { // We are pushing a pool with no pool in place, // and alloc-per-pool debugging was not requested. // Install and return the empty pool placeholder. return setEmptyPoolPlaceholder(); } // We are pushing an object or a non-placeholder'd pool. // Install the first page. AutoreleasePoolPage *page = new AutoreleasePoolPage(nil); setHotPage(page); // Push a boundary on behalf of the previously-placeholder'd pool. if (pushExtraBoundary) { page->add(POOL_BOUNDARY); } // Push the requested object or pool. return page->add(obj); } // 新创建栈页 static __attribute__((noinline)) id *autoreleaseNewPage(id obj) { AutoreleasePoolPage *page = hotPage(); if (page) return autoreleaseFullPage(obj, page); else return autoreleaseNoPage(obj); }
不难发现,将对象入栈的最终调用的都是add函数,代码比较简单,就是栈指针移动,将对象保存到指定栈中位置,如下所示:
id *add(id obj) { ASSERT(!full()); unprotect(); id *ret = next; // faster than `return next-1` because of aliasing *next++ = obj; protect(); return ret; }
同样地,将对象出栈进行释放,调用的是releaseUntil函数。从后往前遍历父page栈,直到位置为stop时才结束遍历。遍历的过程中,每从栈中取出一个obj对象,next指针递减,同时,只要该obj对象不是哨兵对象,就发送objc_release消息,进行释放。
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); }
} }
objc_release调用到runtime中,如下所示,最终结束流程。
__attribute__((aligned(16), flatten, noinline)) void objc_release(id obj) { if (!obj) return; if (obj->isTaggedPointer()) return; return obj->release(); }
inline void objc_object::release() { ASSERT(!isTaggedPointer()); if (fastpath(!ISA()->hasCustomRR())) { rootRelease(); return; } ((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(release)); }
还有最后一点,push是将哨兵入栈,那哨兵对象出栈怎么实现的呢。其实,对应的就是pop函数。先调用releaseUntil函数将所有的非哨兵对象出栈,然后再将哨兵对象出栈。最终所有page均指向nil。代码如下:
//pop出栈 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(); } } } //栈清空 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); }
完结。