@autoreleasepool 自动释放池
面试题:
1、临时变量什么时候释放?
2、自动释放池原理?
3、自动释放池能否嵌套使用?
研究分析
例如main 中的 @autoreleasepool 思路是什么?
clang、汇编
int main(int argc, const char * argv[]) {
@autoreleasepool {
}
return 0;
}
1、clang
终端
cd main.m 地址
clang -rewrite-objc main.m -o main.cpp
编译过程中会产生一些中间的状态。
查看得到的main.cpp,拉到最后查看
extern void _objc_autoreleasePoolPrint(void);
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
}
return 0;
}
static struct IMAGE_INFO { unsigned version; unsigned flag; } _OBJC_IMAGE_INFO = { 0, 2 };
我们源码中的 @autoreleasepool 变成了 __AtAutoreleasePool __autoreleasepool;
查看 __AtAutoreleasePool
struct __AtAutoreleasePool {
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
void * atautoreleasepoolobj;
};
这是一个结构体,里面是
构造函数 objc_autoreleasePoolPush();
析构函数 objc_autoreleasePoolPop(atautoreleasepoolobj);
下面看一下实际是怎么调用的。
写个方法
struct LGTest {
LGTest(){
printf("1123 - %s\n",__func__);
}
~LGTest(){
printf("5667 - %s\n",__func__);
}
};
@autoreleasepool{
LGTest test;
}
打印结果
1123 - LGTest
5667 - ~LGTest
Program ended with exit code: 0
可以发现,调用了函数的构造函数,然后又调用了析构函数(因为出了作用域就没有意义了)
2、 汇编查看
查看源码
objc 的源码。
void *
objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
内存分页
点进去,看到来自于一个私有的 AutoreleasePoolPageData 这么一个数据结构。
/***********************************************************************
Autorelease pool implementation
// 先进后出
A thread's autorelease pool is a stack of pointers.
Each pointer is either an object to release, or POOL_BOUNDARY which is
an autorelease pool boundary.
A pool token is a pointer to the POOL_BOUNDARY 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.
**********************************************************************/
BREAKPOINT_FUNCTION(void objc_autoreleaseNoPool(id obj));
BREAKPOINT_FUNCTION(void objc_autoreleasePoolInvalid(const void *token));
class AutoreleasePoolPage : private AutoreleasePoolPageData
{
friend struct thread_data_t;
1、具有栈的特性,先进后出。
2、每个指针要么是要释放的对象,要么是要释放的 POOL_BOUNDARY 自动释放池边界。
POOL_BOUNDARY 边界(之前的说法是哨兵对象)
边界的用途。在栈的结构体中有一些自己的成员变量,再往里面添加一些对象,加进来的对象也需要pop出去,需要根据边界来判断释放到什么时候为止。
3、双向列表
doubly-linked list
4、跟我们当前寄存的
tls 跟 线程 是有关系的。
链表结点:
struct AutoreleasePoolPageData
{
magic_t const magic; // 16 检查结构体完整性
__unsafe_unretained id *next; // 8 next指针,指向最新添加的 autoreleased 对象的下个位置,用来进行偏移的。初始化时指向begin();
pthread_t const thread; // 8 指向当前线程 用来取出自动释放池
AutoreleasePoolPage * const parent;// 8 指向父结点,第一个结点的 parent 值为nil
AutoreleasePoolPage *child; // 8 指向子结点,最后一个结点的 child 值为nil
uint32_t const depth; // 4 深度 链表链层的深度 从0开始,往后递增1
uint32_t hiwat; // 4 最大入栈数量
看一下
struct magic_t {
static const uint32_t M0 = 0xA1A1A1A1; //静态变量占用的是全局静态变量的空间,不占结构体空间
# define M1 "AUTORELEASE!"
static const size_t M1_len = 12; //静态变量
uint32_t m[4]; //m[4]; 数组有4个元素,都是uint32_t占 4*4 = 16;
magic_t() {
ASSERT(M1_len == strlen(M1));
ASSERT(M1_len == 3 * sizeof(m[1]));
m[0] = M0;
strncpy((char *)&m[1], M1, M1_len);
}
如上注释分析可以知道 magic_t const magic; 是16字节。 AutoreleasePoolPageData 的总内存大小为56
AutoreleasePoolPage * const parent;// 8 父节点
AutoreleasePoolPage *child; // 8 子节点
这就类似 - Class 类
类里面有superClass。普通的 类的初始化的时候,会先将 父类 和 元类 初始化构造完成。初始化父类之后,会将我们本类这个类添加为我们父类的类的子节点。
通过上面的代码分析,说明我们所有的 AutoreleasePool 都会有以上这些属性。
暂时不知道这些都是干什么的,先回到 return AutoreleasePoolPage::push(); 点击跳到 push 里面去
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;
}
这里就是压栈进去
分析各个参数
class AutoreleasePoolPage : private AutoreleasePoolPageData
{
......
AutoreleasePoolPage(AutoreleasePoolPage *newParent) :
AutoreleasePoolPageData(begin(),
objc_thread_self(),
newParent,
newParent ? 1+newParent->depth : 0,
newParent ? newParent->hiwat : 0)
{
AutoreleasePoolPageData(begin(), 数据类型的第一个参数传的是begin()。
看一下 begin()
id * begin() {
return (id *) ((uint8_t *)this+sizeof(*this));
}
为什么 sizeof(*this) 是 56呢?看一下上面的 AutoreleasePoolPageData 这些是固有属性大小,总共是56字节。
经过上面的代码验证,我们猜测 autoreleasePool 里面添加对象进行了分页,一页的话存了多少个对象呢。如上我们循环了505次,第一页里存了我们的504个对象,但是还有个问题,我们循环了505次,为什么会有506个对象,506个对象的话第二页存了一个那么说明第一页存了505个。难道是一页能存505个对象?
看源码
AutoreleasePoolPage size_t 就是我们 page 的大小。
objc_autoreleasePoolPush
class AutoreleasePoolPage : private AutoreleasePoolPageData
{
friend struct thread_data_t;
public:
static size_t const SIZE =
#if PROTECT_AUTORELEASEPOOL
PAGE_MAX_SIZE; // must be multiple of vm page size
#else
PAGE_MIN_SIZE; // size and alignment, power of 2
#endif
#define PAGE_MIN_SIZE PAGE_SIZE
#define PAGE_SIZE I386_PGBYTES
#define I386_PGBYTES 4096
总共的 page 大小 - 固有属性大小(56) = 可以存放对象的大小。一个对象是8字节 那么可存放对象的大小/8 = 可存放对象个数。
4096 - 56 = 4040 / 8 = 505
505 - 1个特殊的属性之外第一页能容纳504个对象。
一个 AutoreleasePoolPage 里面最多可以容纳多少个8字节对象?
每页可以容纳 505 个对象,第一页会加入一个特殊的对象 边界。也就是说第一页可以加 504个自己的对象,之后的都是505个。
验证
for (int i=0; i<505+505; i++) {
NSObject *objc = [[NSObject alloc] autorelease];
}
控制台打印出来的内容可以看到
由此可以知道每页存了505个对象,第一页加入了一个特殊的边界对象。
下面就看一下怎么添加这个边界对象的
void
_objc_autoreleasePoolPrint(void)
{
AutoreleasePoolPage::printAll();
}
static void printAll()
{
_objc_inform("##############");
_objc_inform("AUTORELEASE POOLS for thread %p", objc_thread_self());
AutoreleasePoolPage *page;
ptrdiff_t objects = 0;
for (page = coldPage(); page; page = page->child) {
objects += page->next - page->begin();
}
_objc_inform("%llu releases pending.", (unsigned long long)objects);
//是否有空的池占位符
if (haveEmptyPoolPlaceholder()) {
_objc_inform("[%p] ................ PAGE (placeholder)",
EMPTY_POOL_PLACEHOLDER);
_objc_inform("[%p] ################ POOL (placeholder)",
EMPTY_POOL_PLACEHOLDER);
}
else {
for (page = coldPage(); page; page = page->child) {
page->print();
}
}
判断是否有空的池占位符,也就是看是否是第一页,是的话就会添加边界(哨兵对象),所以第一页只能容纳我们自己的504个对象。
2、对象是怎么进入的,怎么释放的。
进栈
AutoreleasePoolPage::push();
//看一下 push()
static inline void *push()
{
id *dest;
//判断是否是debug 环境
if (slowpath(DebugPoolAllocation)) {
// Each autorelease pool starts on a new pool page.
dest = autoreleaseNewPage(POOL_BOUNDARY);
} else {
//一般是fast环境
dest = autoreleaseFast(POOL_BOUNDARY);
}
看一下 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);
}
}
添加的话先拿到 next 指向最新添加到 autoreleased 对象的下个位置。然后 *next++ = obj; 这行代码的意思是将obj 放到这个next 指向的这个地址。然后再将next 指针向下移动8字节(++)。同时是一个初始化方法。
id *add(id obj)
{
ASSERT(!full());
unprotect();
id *ret = next; // faster than `return next-1` because of aliasing
*next++ = obj;
protect();
return ret;
}
添加的obj 初始化
objc_object::rootAutorelease()
{
// 判断是否是标记
if (isTaggedPointer()) return (id)this;
if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;
return rootAutorelease2();
}
__attribute__((noinline,used))
id
objc_object::rootAutorelease2()
{
ASSERT(!isTaggedPointer());
return AutoreleasePoolPage::autorelease((id)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;
}
page 满的话开分页
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);
}
如果分页是满的就会 do while 循环,递归去找下一页,如果还是满的继续递归。直到找到没有满的,或者是刚好最后一个满的话新创建一页 然后add。
如果找到的 page 会被设置成(setHotPage(page)) PAGE (hot)聚焦页(也就是给了一个标记),其他的page 就是 PAGE (full)
static inline void setHotPage(AutoreleasePoolPage *page)
{
if (page) page->fastcheck();
tls_set_direct(key, (void *)page);
}
出栈
释放
NEVER_INLINE
void
objc_autoreleasePoolPop(void *ctxt)
{
AutoreleasePoolPage::pop(ctxt);
}
ctxt 是上下文。从最开始的 main.cpp
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
可以看到传的是一个 objc_autoreleasePoolPush(); 的返回值,也就是标记 析构 构造函数 对应的 对象。
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();
//如果是空的池就把 page->begin() 赋值给 token
token = page->begin();
} else {
page = pageForPointer(token);
}
stop = (id *)token;
//判断是否等于 nil
if (*stop != POOL_BOUNDARY) {
// 第一个结点 - 没有父结点 parent 父结点
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);
}
# define EMPTY_POOL_PLACEHOLDER ((id*)1)
EMPTY_POOL_PLACEHOLDER 是一个空的占位的,表示没有压栈任何对象。这个时候直接把 page->begin() 给 占位符
# define POOL_BOUNDARY nil
stop!=nil; 的时候
return popPage<false>(token, page, stop);
template<bool allowDebug>
static void
popPage(void *token, AutoreleasePoolPage *page, id *stop)
{
if (allowDebug && PrintPoolHiwat) printHiwat();
//直接进行 release。
page->releaseUntil(stop);
release 不断的向上递归,直到this->next == stop next(最新对象的下个位置,也就是最后一个位置)
递归找到obj (id obj = *--page->next;) 直接进行 objc_release(obj);
void releaseUntil(id *stop)
{
while (this->next != stop) {
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);
判断每一页是否递归完成 next == begin()节点
bool empty() {
return next == begin();
}
找到的obj 进行 objc_release 释放对象
__attribute__((aligned(16), flatten, noinline))
void
objc_release(id obj)
{
if (!obj) return;
if (obj->isTaggedPointer()) return;
return obj->release();
}
对象释放完之后然后一层一层的去把表杀掉,同时设置 setHotPage(nil)
page->kill();
setHotPage(parent);
static AutoreleasePoolPage *pageForPointer(const void *p)
{
return pageForPointer((uintptr_t)p);
}
3、不同线程 @autoreleasePool
@autoreleasePool 嵌套
MRC 下必须要要手动添加 autorelease
1、@autoreleasePool 与线程关联,不同的线程 @autoreleasePool 是不相同的。
2、@autoreleasePool 嵌套 - 只会创建一个 page 但是有多个(嵌套几层有几个)个边界(哨兵对象)
为什么是多个边界(哨兵对象),因为他们有不同的作用域空间,
例如:
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
// 1 + 504 + 505 + 505
NSObject *objc = [[NSObject alloc] autorelease];
NSLog(@"objc = %@",objc);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
@autoreleasepool {
NSObject *obj = [[NSObject alloc] autorelease];
NSLog(@"obj = %@",obj);
_objc_autoreleasePoolPrint();
}
});
_objc_autoreleasePoolPrint();
}
// 3*16 + 8 = 56
// 504
sleep(2);
return 0;
}
有两个边界(哨兵对象)
通过打印可以看到不同的线程 autoreleasePool 绑定是不同的
第一个绑定在 thread 0x1000d2dc0
第二个绑定在 thread 0x700005b04000
所以AutoreleasePool的释放有如下两种情况。
1、Autorelease对象是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池Push和Pop。
2、手动调用AutoreleasePool的释放方法(drain方法)来销毁AutoreleasePool
在 ARC 环境下, 方法在将要执行结束的时候, 局部变量的指针都会被置为 nil
总结
1、clang
__AtAutoreleasePool
__autoreleasepool
2、__AtAutoreleasePool
构造函数 objc_autoreleasePoolPush();
析构函数 objc_autoreleasePoolPop(atautoreleasepoolobj);
3、通过汇编
可以知道 运行的时候先进行了 objc_autoreleasePoolPush 构造
然后执行完,直接进行了 objc_autoreleasePoolPop 析构
4、push
- 栈属性 先进后出
- 存放的对象 边界、我们创建的需要释放的对象
- 是一个双向联保结构
- 自动释放池与线程是一一对应的关系
5、AutoreleasePoolPageData
固有属性 56字节
magic; //16 检查结构体完整性
next; //8 next指针,指向最新添加的 autoreleased 对象的下个位置,用来进行偏移的。初始化时指向begin();
thread; // 8 指向当前线程 用来取出自动释放池
parent; //8 指向父结点,第一个结点的 parent 值为nil
child; //8 指向子结点,最后一个结点的 child 值为nil
depth; //4 深度 链表链层的深度 从0开始,往后递增1
hiwat; // 4 最大入栈数量
6、AutoreleasePoolPage
每页可以容纳 505 个对象,第一页会加入一个特殊的对象 边界。也就是说第一页可以加 504个自己的对象,之后的都是505个。
7、入栈
通过next 指针存取对应地址,然后next 指针指向向下移动8字节
如果page 满的话新创建一页 new AutoreleasePoolPage(page);
然后 add
存入的这一页 setHotPage(page) 设置为聚焦页
8、出栈 也就是释放
objc_autoreleasePoolPop(void *ctxt) ctext 上下文,对应构造函数
- 判断是否是空池 如果是空池,把begin() 赋值给 token
- stop = (id*)token
- 判断token是否等于 nil
- 不等于nil
- 是都是第一节点,是否有父节点(如果是第一个节点又有父节点的说明有问题,直接不处理)
- 然后 badpop(token)
- 通过next 不断向上一页一页的去递归去释放,直到next = stop(begin())起始位置为止。每一页有begin(),同样通过next 去判断,当前页是否递归完。
- 对象释放之后然后重下向上一层一层的去把表杀掉,
9、与线程管理
@autoreleasePool 与线程关联,不同的线程 @autoreleasePool 是不相同的。
10、@autoreleasePool 嵌套
只会创建一个 page 但是有多个(嵌套几层有几个)个边界(哨兵对象)
为什么是多个边界(哨兵对象),因为他们有不同的作用域空间,