主要讲解Autoreleasepool的原理;
本文中的测试代码主要为 MRC 环境;
文中使用的 runtime源码是objc4-781版本;
iOS 内存管理 部分一
iOS 内存管理 部分二
iOS 内存管理 部分三
iOS 内存管理 部分四
1. 什么是 Autoreleasepool?
从字面意思即可得知是自动释放池
, 其作用就是在@autoreleasepool
结束的时候会将其中的对象进行回收,释放内存空间;
它有如下特点;
-
Autoreleasepool
的底层也是一个结构体, 其存储结构类似栈,先入的autorelease
对象后被释放;编译器会将@autoreleasepool{autorelease对象}
变为以下代码struct{ /* push 函数入参POOL_BOUNDARY返回一个地址, 这个地址是起始位置, 从这个开始, 下一个位置开始存放 autorelease 对象, */ void *ctx = objc_autoreleasePoolPush(); push进autorelease对象 push进autorelease对象 push进autorelease 对象 objc_autoreleasePoolPop(ctx); }
-
-
@autoreleasepool
是由一个一个的autoreleasepoolpage
组成, 所有的autoreleasepoolpage
通过双向链表连接在一次, 每个autoreleasepoolpage
占用4096个字节;除了自身成员变量所占用空间, 其余的都是用来存放autorelease
对象(MRC
环境);
-
class AutoreleasePoolPage;
struct AutoreleasePoolPageData
{
magic_t const magic;
__unsafe_unretained id *next;
pthread_t const thread;
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t hiwat;
};
magic用来校验AutoreleasePoolPage结构是否完整;
next指向当前栈顶第一个可用的地址;
thread指向当前的线程;
parent双向链表中指向父指针;
child双向链表中指向子指针;
通过双向链表将多个autoreleasepoolpage
连接起来;
3. Autoreleasepool的流程;
我们知道Autoreleasepool
底层是一个或者多个AutoreleasepoolPage
通过child
和parent
连接起来的双向链表结构, 这就类似栈的结构, 后面加入的总能最先被使用或者释放, 因此如果有多个AutoreleasepoolPage
时, 肯定是前面的AutoreleasepoolPage
空间被用完了, 而链表尾部AutoreleasepoolPage
空间未满仍可以使用, 从源码中也可以看到hotPage
就是链表尾部的AutoreleasepoolPage
, 而 coldPage
则是栈底的AutoreleasepoolPage
;因此我们可以整理Autoreleasepool
使用时的执行流程如下
- 3.1 首先
Autoreleasepool
底层转化为结构体, 实际上一个一个AutoreleasePage
双向链表结构连接起来; - 3.2 执行
Autoreleasepool
代码时, 也就是执行AutoreleasepoolPage
的push
操作时, 首先 判断pool
中是否有Autorelease
对象, 如果没有则创建一个占位pool
并返回结束; - 3.3 如果有
Autorelease
对象, 判断是否有hotPage
;a. 如果有
hotPage
则判断是否够用, 不够用则创建新的page
并通过child
和parent
指针连接, 同时设置hotPage
;
b. 如果没有hotPage
则创建新的page
并设置为hotPage
; - 3.4 在往
AutoreleasePage(hotPage)
添加Autorelease
对象时,实际执行两个步骤;a. 如果是第一次添加
Autorelease
对象, 首先往hotPage
中添加POOL_BOUNDARY
指针并返回它地址, 同时next
指针向后移动; 如果不是第一次添加则字节执行下面步骤;
b. 往hotPage
中添加Autorelease
对象指针, 同时next
指针向后移动; - 3.5 在
Autoreleasepool
代码结束时, 也就是后面大括号时, 开始执行AutoreleasepoolPage
的pop
操作时, 入参3.4.a 返回的POOL_BOUNDARY
地址, 作为标志(也就是大家说的哨兵对象地址)
然后开始从hotPage
通过while
循环开始释放Autorelease
对象, 如果hotPage
释放完毕仍然未到达标志, 则通过parent
指针向上查找, 继续释放; 释放的同时next--
跟着移动和重新设置hotPage
;
释放到达POOL_BOUNDARY
标志位时结束, 同时释放childPage
的空间;
备 :抽时间整理流程图;
补充 Autoreleasepool 的底层结构和源码大致逻辑
1.Autoreleasepool的底层结构
通过clang
语句将下面代码转转后
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
appDelegateClassName = NSStringFromClass([AppDelegate class]);
Model *model = [[Model alloc] init];
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
可以得到Autoreleasepool
其底层结构如下,
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
appDelegateClassName = NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("AppDelegate"), sel_registerName("class")));
Model *model = ((Model *(*)(id, SEL))(void *)objc_msgSend)((id)((Model *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Model"), sel_registerName("alloc")), sel_registerName("init"));
}
return UIApplicationMain(argc, argv, __null, appDelegateClassName);
===>
#autoreleasepool 转化为了__AtAutoreleasePool, 直接在.cpp 中搜索可以得知
struct __AtAutoreleasePool {
///构造函数, 在结构体创建的时候调用
__AtAutoreleasePool() {
atautoreleasepoolobj = objc_autoreleasePoolPush();
}
///析构函数, 在结构体销毁的时候调用;
~__AtAutoreleasePool() {
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
void * atautoreleasepoolobj;
};
===>
#去 objc4-781版本源码中查询objc_autoreleasePoolPush的结构如下,
# 注意它是返回一个指针地址, 这个地址是POOL_BOUNDARY 的地址;
# 当在执行 pop 操作时入参这个地址, 表示释放到这个位置为止
void *
objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
===>
#AutoreleasePoolPage的结构如下(继承自AutoreleasePoolPageData)
class AutoreleasePoolPage;
struct AutoreleasePoolPageData
{
///验证AutoreleasePoolPageData 的完整性
magic_t const magic;
///指向当前AutoreleasePoolPageData栈顶的可用空间的指针
__unsafe_unretained id *next;
///当前线程
pthread_t const thread;
///双向链表结构, 指向前面AutoreleasePoolPageData
AutoreleasePoolPage * const parent;
///双向链表结构, 指向后面AutoreleasePoolPageData
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t hiwat;
///构造函数, 注意 child 默认传入的是空, parent指向前面的 page;
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)
{
}
};
2.Autoreleasepoolpage的的 push 逻辑
我们接着上面的源码继续看Autoreleasepoolpage
的 push
过程
首先看下一个重要的参数 POOL_BOUNDARY
他的定义如下
# define POOL_BOUNDARY nil
我们知道Autoreleasepoolpage
中的next
指向当前栈顶的可用地址; 下面以如下代码来讲解下POOL_BOUNDARY
和next
的协调作用
@autoreleasepool {
for (int i = 0; i < 5; i++) {
Model *model = [[Model alloc] init];
}
}
假设刚开始的时候POOL_BOUNDARY = nil
而next
指向当前栈顶的可用位置地址, 当我们向
Autoreleasepoolpage
添加变量时, 这时POOL_BOUNDARY
变为原先 next
位置, 同时next
向栈顶移动;
#下面看下Autoreleasepoolpage的 push函数
static inline void *push() {
id *dest;
if (slowpath(DebugPoolAllocation)) {
// Each autorelease pool starts on a new pool page.
/*
特殊情况: 调试模式下每一个 autoreleapool 都会创建新的 page;
判断条件为DebugPoolAllocation, 请点击查看其成立条件;
正常使用autoreleapool不会走此流程, 正常情况下即使是多个 autoreleapool嵌套,
他们也公用 page, 通过各自 的 POOL_BOUNDARY 来作为边界;
*/
dest = autoreleaseNewPage(POOL_BOUNDARY);
} else {
///正常情况下autoreleapool 的push逻辑
dest = autoreleaseFast(POOL_BOUNDARY);
}
ASSERT(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
return dest;
}
===>
# 看下autoreleaseFast(POOL_BOUNDARY)的执行逻辑;
static inline id *autoreleaseFast(id obj) {
/*
注意这个名字 hotPage, hot说明是正在使用的 page, 例如, 如果有多个AutoreleasePoolPage
那么一定是前面的AutoreleasePoolPage已经存储满了, 所以使用最后面的AutoreleasePoolPage;
这个空间尚未使用满的AutoreleasePoolPage称为hotPage; 注意它的值可能是 nil,
因为如果AutoreleasePool 中并没有autoreleas 对象时,
并不会创建AutoreleasePoolPage, 这时 hotPage = nil;
*/
AutoreleasePoolPage *page = hotPage();
///如果当前page 存在且没有使用完则执行add操作
if (page && !page->full()) {
return page->add(obj);
} else if (page) {
///如果当前 page 已经使用完毕. 则执行autoreleaseFullPage操作
return autoreleaseFullPage(obj, page);
} else {
///当前 page 不存在, 执行autoreleaseNoPage; 特殊情况不深入探究
return autoreleaseNoPage(obj);
}
}
===>
# 下面看下 没有AutoreleasePoolPage时的添加逻辑:
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());
///注意这个标志, 是否需要 PUSH 进去额外的 POOL_BOUNDARY 对象
bool pushExtraBoundary = false;
/*
第一次执行 AutoreleasePoolPage::push 操作时, 此时haveEmptyPoolPlaceholder() == false, 所以条件不满足;
从第二次开始, 这个条件就满足了, 会将pushExtraBoundary = true;
*/
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;
}
///特殊情况, 向空的 pool 中添加元素, 直接返回 nil
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;
}
/*
注意这个判断条件: 当第一次AutoreleasePoolPage::push时入参POOL_BOUNDARY满足,
调用setEmptyPoolPlaceholder(), 然后haveEmptyPoolPlaceholder() == true;
如果嵌套AutoreleasePool时, 再次调用AutoreleasePoolPage::push时, 如果没有实际
添加autorelease 对象, 则此条件仍然满足, 因为入参仍然是 POOL_BOUNDARY;
只有实际添加autorelease对象入栈时此条件才不满足;
*/
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.
///返回的一个占位 pool, 实际上此时还没有开辟AutoreleasePoolPage;
return setEmptyPoolPlaceholder();
}
// We are pushing an object or a non-placeholder'd pool.
// Install the first page.
///实际上第一次添加autorelease对象到AutoreleasePool 时正式创建AutoreleasePoolPage;
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
setHotPage(page);
// Push a boundary on behalf of the previously-placeholder'd pool.
///因为pushExtraBoundary == true, 所以实际上第一次添加autorelease对象 时, 是先添加POOL_BOUNDARY指针;
if (pushExtraBoundary) {
page->add(POOL_BOUNDARY);
}
// Push the requested object or pool.
///正式添加autorelease对象
return page->add(obj);
}
===>
#实际的 add 操作
id *add(id obj) {
ASSERT(!full());
unprotect();
///首先将 next 指针记录下来
id *ret = next; // faster than `return next-1` because of aliasing
/*
这行语句分解开等价于 *next = obj; *next ++;
因为根据运算符的结合性, 从右向左结合, 先执行=运算符;
执行后把POOL_BOUNDAY 或者 autorelease 对象放入 next 原本位置;
然后 next 的指向向后移动;
*/
*next++ = obj;
protect();
return ret;
}
===>
/*
如果 hotPage 满了的时候 需要向后移动查找尚未;
调用autoreleaseFullPage(obj, page); 注意入参, 除了 obj(POOL_BOUNDARY)
还有一个参数是 page, 作用是需要把 page 连接起来
*/
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);
///如果 child 指针指向的 page 存在则设置为 hotPage, 否则创建新的设置为 hotPage
do {
if (page->child) page = page->child;
else page = new AutoreleasePoolPage(page);
} while (page->full());
///设置当前HotPage
setHotPage(page);
///当 page 创建成功后, 再执行新的添加操作;
return page->add(obj);
}
不考虑占位 autoreleasepool
时的添加过程
3.Autoreleasepoolpage的的 pop 逻辑
static inline void
pop(void *token)
{
AutoreleasePoolPage *page;
id *stop;
/*
根据AutoreleasePoolPage的 push 我们知道它返回的是结构体 的地址;
首先判断是否最后一个是不是空的占位 pool, 如果是占位 pool 则执行;
如果多个AutoreleasePool嵌套(或者一个 pool), 最内层没有实际Autorelease的对象,
那么它一定是一个占位 pool, 实际上没有开辟AutoreleasePoolPage;
*/
if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
// Popping the top-level placeholder pool.
page = hotPage();
///如果没有 hotPage , 说明没有AutoreleasePoolPage, 只有占位 pool;只需要将占位pool 置 nil 即可;
if (!page) {
// Pool was never used. Clear the placeholder.
//看这句话翻译, pool 没有使用(就是没有添加Autorelease对象), 清理占位 pool;
return setHotPage(nil);
}
// Pool was used. Pop its contents normally.
// Pool pages remain allocated for re-use as usual.
/*
注意官方注释: pool 被使用了, 那就正常的 pop;将 page 指针指向 coldpage;
关于 coldpage 的定义, 请自行去看下就行, 实际上就是指向栈底的那个 AutoreleasePoolPage;
然后将 token 指针指实际的 page(去掉占位 pool ) 的 begin();
*/
page = coldPage();
token = page->begin();
} else {
/*
走到这里说明所有的 page 都是被使用状态, 没有占位 pool;
调用pageForPointer()函数获取入参 page 数据信息;
*/
page = pageForPointer(token);
}
/*
设置 stop 为 token ;
如果链表尾部是实际的 page:
这个 token 的值是链表尾部 page 的POOL_BOUNDAY 的地址;
如果链表尾部是占位的 pool:
这个 token 的值是去掉占位 pool 后链表尾部 page 的POOL_BOUNDAY 的地址;
*/
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.
/*
经过上面判断, stop 应该指向入参 page 的 begin()地址;
page 指向入参的 page, 所以它的 parent 指向应该是空;
出现错误,执行 badPop(), 请自行查看, 文中不再贴出具体逻辑;
*/
return badPop(token);
}
}
///特殊情况, 调试模式(每个 pool 都会创建新的 page)不再深入探究, 请自行查看;
if (slowpath(PrintPoolHiwat || DebugPoolAllocation || DebugMissingPools)) {
return popPageDebug(token, page, stop);
}
///执行popPage 操作, 注意入参 false
return popPage<false>(token, page, stop);
}
===>
#执行 pagePop 操作, allowDebug的值是 false
template<bool allowDebug>
static void
popPage(void *token, AutoreleasePoolPage *page, id *stop)
{
if (allowDebug && PrintPoolHiwat) printHiwat();
///实际的释放操作, autorelease 对象在这里释放的
page->releaseUntil(stop);
// memory: delete empty children
///删除多余的 chiildPage;
///特殊情况/调试模式, 不深究
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);
/*
正常情况, 执行 childPage的 kill 操作
注意由于 child 默认是空, 所以, 执行到里面是一定是多个 page 连接的情况
*/
} else if (page->child) {
// hysteresis: keep one empty child if page is more than half full
///看上面的官方注释: 如果当前 page 使用率超过一半, 则保留一个空 page 备用;
if (page->lessThanHalfFull()) {
///当前 page 的使用率不足一半, 则只保留当前 page;
page->child->kill();
}
else if (page->child->child) {
///当前 page 的使用率超过一半, 则只保留当前 page+下一个 chiildPage;
page->child->child->kill();
}
}
}
===>
#真正释放 autorelease 对象的逻辑
void releaseUntil(id *stop)
{
// Not recursive: we don't want to blow out the stack
// if a thread accumulates a stupendous amount of garbage
/*
通过static inline void pop(void *token)函数的逻辑我们可以得知, 正常情况下stop应该
指向的是链表尾部 page 的 begin()位置, 也就是其 POOL_BOUNDARY 的地址;
所以就从链表尾部开始遍历, 直到 next 指向 POOL_BOUNDARY 位置结束;
*/
while (this->next != stop) {
// Restart from hotPage() every time, in case -release
// autoreleased more objects
/*
注意这句官方注释: 每次从 hotPage 开始释放, 防止重复释放autoreleased对象;
因为是栈结构, 后加入的先释放, 先加入的后释放, 从链表尾部开始释放;
*/
AutoreleasePoolPage *page = hotPage();
// fixme I think this `while` can be `if`, but I can't prove it
while (page->empty()) {
///如果当前 page 已经释放完毕, 通过 parent 指针找到前面的 page,并设置为 hotPage;
page = page->parent;
setHotPage(page);
}
page->unprotect();
/*
id obj = *--page->next;根据运算符向左结合性, 这行代码等价于
先将 next指针执行--, 然后将这个位置的autorelease对象给 obj;
因为 next 指针始终指向的是栈顶可以用的位置; 所以它的前面
存放的是POOL_BOUNDARY或者autorelease对象;
*/
id obj = *--page->next;
memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
page->protect();
///如果 obj 不是POOL_BOUNDARY指针, 那就是 autoreleased 对象, 执行objc_release操作;
if (obj != POOL_BOUNDARY) {
objc_release(obj);
}
}
///已经遍历到入参 page, 设置入参 page 为 hotPage;
setHotPage(this);
#if DEBUG
// we expect any children to be completely empty
for (AutoreleasePoolPage *page = child; page; page = page->child) {
ASSERT(page->empty());
}
#endif
}
4._objc_autoreleasePoolPrint()函数打印
我们通过extern void _objc_autoreleasePoolPrint(void);
方式可以调用私有函数来打印autoreleasepool
的信息, 如下代码
#import "ViewController1.h"
#import "Model.h"
extern void _objc_autoreleasePoolPrint(void);
@interface ViewController1 ()
@end
@implementation ViewController1
///在工程设置为此文件为MRC
- (void)viewDidLoad {
[super viewDidLoad];
@autoreleasepool {
Model *model = [[[Model alloc] init] autorelease];
Model *model2 = [[[Model alloc] init] autorelease];
@autoreleasepool {
Model *model2 = [[[Model alloc] init] autorelease];
_objc_autoreleasePoolPrint();
}
}
}
@end
可以获得如下打印结果
参考文章和下载链接
文中测试代码
objc4源码下载地址
网友评论