研究@autoreleasepool
之前,我们先来看下他的基本构成和实现。
在main.m
文件中添加如下代码:
int main(int argc, char * argv[]){
@autoreleasepool {
}
return 1;
}
clang
编译后:
int main(int argc, char * argv[]){
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
}
return 1;
}
全局搜索__AtAutoreleasePool
:
struct __AtAutoreleasePool {
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
void * atautoreleasepoolobj;
};
在main.cpp文件中有一个__AtAutoreleasePool
结构体,里面包含了构造函数和和析构函数。
下面我们根据objc源码
看下具体实现。
一、autoreleasePool数据结构
通过objc_autoreleasePoolPush
方法,我们会发现一个结构体AutoreleasePoolPage
,在这里有一个注释,很重要:
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.
AutoreleasePoolPageData
属性的基本构成:
class AutoreleasePoolPage;
struct AutoreleasePoolPageData
{
magic_t const magic; // 16
__unsafe_unretained id *next; //8
pthread_t const thread; // 8
AutoreleasePoolPage * const parent; //8
AutoreleasePoolPage *child; //8
uint32_t const depth; // 4
uint32_t hiwat; // 4
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)
{
}
};
struct magic_t {
static const uint32_t M0 = 0xA1A1A1A1;
# define M1 "AUTORELEASE!"
static const size_t M1_len = 12;
uint32_t m[4]; //4*4
……
}
-
magic
检查校验完整性的变量 -
next
指向新加入的autorelease对象下一个位置 -
thread page
当前所在的线程 -
parent
父节点 指向前一个page -
child
子节点 指向下一个page -
depth
链表的深度,节点个数 -
hiwat
high water mark 数据容纳的一个上限
其他相关属性:
-
EMPTY_POOL_PLACEHOLDER
空池占位 -
POOL_BOUNDARY
是一个边界对象 nil,之前的源代码变量名是 * POOL_SENTINEL哨兵对象,用来区别每个page即每个 AutoreleasePoolPage 边界 -
PAGE_MAX_SIZE
=4096, 为什么呢?其实就是虚拟内存每个扇区4096个字节,也就是4K对齐。
关于构造函数AutoreleasePoolPageData,第一个next是什么呢,跳转后定位到下面:
id * begin() {
return (id *) ((uint8_t *)this+sizeof(*this));
}
lldb调试,打印发现 sizeof(*this) = 56
,也就是一个AutoreleasePoolPageData的大小,这个在前面已经标注。
注意一点:结构体指针为8,结构体的大小要根据内部属性计算,static修饰的变量空间不在结构体内。
所以,page从56字节的位置开始装对象,因此一页可以装的数量:(4096-56)/8 = 505,下面来验证一下:
extern void _objc_autoreleasePoolPrint(void);
int main(int argc, const char * argv[]) {
@autoreleasepool {(int i=0; i<1+504 +505; i++) {
NSObject *objc = [[NSObject alloc] autorelease];
}
_objc_autoreleasePoolPrint();
sleep(3);//防止打印没结束,当前线程以及结束
}
return 0;
}
注意:第一个页数据504+1,这个1是边界即哨兵,比较特殊。具体可以参考_objc_autoreleasePoolPrint
实现。
二、push
static inline id *autoreleaseFast(id obj)
{
AutoreleasePoolPage *page = hotPage();
if (page && !page->full()) {
//当前page存在,且不满,则add
return page->add(obj);
} else if (page) {
//当前page存在,且满
return autoreleaseFullPage(obj, page);
} else {
return autoreleaseNoPage(obj);
}
}
添加对象:
id *add(id obj)
{
assert(!full());
unprotect();
id *ret = next; // faster than `return next-1` because of aliasing
*next++ = obj;
protect();
return ret;
}
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.
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)
{
assert(!hotPage());
bool pushExtraBoundary = false;
if (haveEmptyPoolPlaceholder()) { //是否存在未使用的空池
pushExtraBoundary = true;
} else if (obj != POOL_BOUNDARY && DebugMissingPools) {
//obj 不是哨兵对象时, 说明上一次的 push() 没有成功, 如果打开了丢失 pools 的调试
_objc_inform(...); //输出一些调试信息
objc_autoreleaseNoPool(obj); //这个函数没有找到源码
return nil;
} else if (obj == POOL_BOUNDARY && !DebugPoolAllocation) {
//如果 obj 是哨兵对象, 并且没有开启 pool 内存申请的调试
return setEmptyPoolPlaceholder(); //将本自动释放池标记为未使用的空池
}
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil); //创建一个 page 节点, 这里会调用构造函数
setHotPage(page); //将该节点设置为 hotPage
if (pushExtraBoundary) { //如果本自动释放池是一个未使用的空池
page->add(POOL_BOUNDARY); //加入哨兵对象
}
return page->add(obj); //将 obj 加入自动释放池
}
static inline bool haveEmptyPoolPlaceholder()
{
id *tls = (id *)tls_get_direct(key); //取出本线程对应的 hotPage
return (tls == EMPTY_POOL_PLACEHOLDER); //如果为空池标识符则返回 true
}
pop 源码:
static inline void pop(void *token)
{
AutoreleasePoolPage *page;
id *stop;
if (token == (void*)EMPTY_POOL_PLACEHOLDER) { //如果 token 为空池标志
if (hotPage()) { //如果有 hotPage, 即池非空
pop(coldPage()->begin()); //将整个自动释放池销毁
} else {
setHotPage(nil); //没有 hotPage, 即为空池, 设置 hotPage 为 nil
}
return;
}
page = pageForPointer(token); //根据 token 找到所在的 节点
stop = (id *)token; //token 转换给 stop
if (*stop != POOL_BOUNDARY) { //如果 stop 中存储的不是哨兵节点
if (stop == page->begin() && !page->parent) {
//存在自动释放池的第一个节点存储的第一个对象不是哨兵对象的情况, 有两种情况导致:
//1. 顶层池呗是否, 但留下了第一个节点(有待深挖)
//2. 没有自动释放池的 autorelease 对象(有待深挖)
} else {
//非自动释放池的第一个节点, stop 存储的也不是哨兵对象的情况
return badPop(token); //调用错误情况下的 badPop()
}
}
return popPage<false>(token, page, stop);
}
static void
popPage(void *token, AutoreleasePoolPage *page, id *stop){
page->releaseUntil(stop); //将自动释放池中 stop 地址之后的所有对象释放掉
if (...) {
//这一段代码都是调试用代码
} else if (page->child) { //如果 page 有 child 节点
if (page->lessThanHalfFull()) { //如果 page 已占用空间少于一半
page->child->kill(); //kill 掉 page 的 child 节点
} else if (page->child->child) { //如果 page 的占用空间已经大于一半, 并且 page 的 child 节点有 child 节点
page->child->child->kill(); //kill 掉 child 节点的 child 节点
}
}
}
void releaseUntil(id *stop)
{
// 释放当前page中的autorelease对象
while (this->next != stop) {
AutoreleasePoolPage *page = hotPage();
// 如果当前page都释放完了还没遇到POOL_BOUNDARY,就继续释放上一个page的
while (page->empty()) {
page = page->parent;
setHotPage(page);
}
page->unprotect();
// 依次取出autorelease对象
id obj = *--page->next;
memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
page->protect();
// 释放autorelease对象
if (obj != POOL_BOUNDARY) {
objc_release(obj);
}
}
setHotPage(this);
}
三、嵌套
int main(int argc, char * argv[]){
@autoreleasepool {
NSObject *obj = [[NSObject alloc] autorelease];
@autoreleasepool {
NSObject *obj = [[NSObject alloc] autorelease];
@autoreleasepool {
NSObject *obj = [[NSObject alloc] autorelease];
_objc_autoreleasePoolPrint();
}
}
}
while (1) {
}
}
打印结果:
objc[15807]: ##############
objc[15807]: AUTORELEASE POOLS for thread 0x1199565c0
objc[15807]: 7 releases pending.
objc[15807]: [0x7fa191006000] ................ PAGE (hot) (cold)
objc[15807]: [0x7fa191006038] 0x6000016040a0 NSObject
objc[15807]: [0x7fa191006040] ################ POOL 0x7fa191006040
objc[15807]: [0x7fa191006048] 0x600001608190 NSObject
objc[15807]: [0x7fa191006050] ################ POOL 0x7fa191006050
objc[15807]: [0x7fa191006058] 0x600001608140 NSObject
objc[15807]: [0x7fa191006060] ################ POOL 0x7fa191006060
objc[15807]: [0x7fa191006068] 0x6000016081b0 NSObject
objc[15807]: ##############
push函数会把
POOL_BOUNDARY
存储到AutoreleasePoolPage
对象中,用来标记当前@autoreleasepool的边界
。一个{}
对应一个POOL_BOUNDARY
,pop时,会依次释放对象,直到遇到POOL_BOUNDARY
。
四、宏定义解释
EMPTY_POOL_PLACEHOLDER
根据注释以及代码分析, 可以大致得出这个宏定义用作 pool 中没有 add 入对象时的标记。当一个自动释放池被创建但是没有加入任何 Autorelease 对象时, 会让这个自动释放池的句柄等于 EMPTY_POOL_PLACEHOLDER, 并不为其分配内存。
注释说在顶层(即 libdispatch)进行 pool 的 push 和 pop 但并没有使用这个池子的时候, 能节省空间, OC类中有许多自带 block 的方法, 比如 UIView 的 animation 系列方法, 和 NSArray 以及 NSDictionary 的 enumerat 系列方法中, 自带的 block 会内嵌 AutoreleasePool:
[array enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
//这里的代码已经处于内嵌的 @autoreleasepool {} 中了
}];
验证:
//1
__block __weak NSArray * weakArray;
dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
@autoreleasepool {
NSArray * array = [NSArray arrayWithObjects:[NSString stringWithFormat:@"%@", self], nil];
weakArray = array;
}
});
NSLog(@"%@", weakArray);
//2
[UIView animateWithDuration:1. animations:^{
NSArray * array = [NSArray arrayWithObjects:[NSString stringWithFormat:@"%@", self], nil];
weakArray = array;
} completion:^(BOOL finished) {
NSLog(@"%@", weakArray);
}];
以上两段代码输出都为 null
, 但是如果将第一段代码中的 @autoreleasepool
删除, weakArray 将输出有值, 证明第二段代码的 block 中内嵌了 @autoreleasepool。
如果内嵌了@autoreleasepool 的代码块中没有使用到 Autorelease 对象, 却为池子分配了节点, 无疑是对内存的浪费。
EMPTY_POOL_PLACEHOLDER
即是为了解决这种情况下发生的内存浪费而存在的。
POOL_BOUNDARY
POOL_BOUNDARY
会在建立新的自动释放池时作为第一个对象加入到池中, 被称为哨兵对象
, 哨兵对象是自动释放池中非常巧妙而且重要的一环。
我们已经知道 @autoreleasepool {}
是在作用域的开始使用 push()
方法来创建自动释放池
, 在作用域结束时, 使用 pop()
方法来销毁
自动释放池。
在嵌套结构中 push() 方法不一定会创建新的 page 节点, 如果当前节点未满则会直接插入一个哨兵对象, 如果当前节点已满则创建一个新的 page 节点并且插入一个哨兵对象, push() 函数的返回值就是这个哨兵对象的地址(哨兵对象的值是 nil, 但哨兵对象的地址不为 nil), 然后在 pop() 方法调用时, 传入这个哨兵对象的地址, 对这个地址之后的 autorelease
对象发送 release 方法。
五、autoreleasepool、runloop和多线程的关系
5.1 autoreleasepool和runloop
- RunLoop和线程的一一对应的,对应的方式是以key-value的方式保存在一个全局字典中
- 主线程的RunLoop会在初始化全局字典时创建
- 子线程的RunLoop会在第一次获取的时候创建,如果不获取的话就一直不会被创建
- RunLoop会在线程销毁时销毁
系统在主线程的runloop中注册了2个observer:
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"%@", [NSRunLoop mainRunLoop]);
}
// 部分打印
observers = (
"<CFRunLoopObserver 0x600001330320 [0x7fff80617cb0]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff4808bf54),
context = <CFArray 0x600002c607e0 [0x7fff80617cb0]>{type = mutable-small, count = 1, values = (\n\t0 : <0x7f9c54802048>\n)}}",
"<CFRunLoopObserver 0x6000013303c0 [0x7fff80617cb0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff4808bf54),
context = <CFArray 0x600002c607e0 [0x7fff80617cb0]>{type = mutable-small, count = 1, values = (\n\t0 : <0x7f9c54802048>\n)}}"
)
关于CFRunLoopActivity:
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 1,进入runloop
kCFRunLoopBeforeTimers = (1UL << 1), // 2,即将处理timers
kCFRunLoopBeforeSources = (1UL << 2), // 4,即将处理sources
kCFRunLoopBeforeWaiting = (1UL << 5), // 32,即将休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 64,刚被唤醒
kCFRunLoopExit = (1UL << 7), // 128,退出runloop
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
第一个Observer
:activities为0x1
(十进制为1),它用来监听kCFRunLoopEntry
的。当监听到kCFRunLoopEntry时,会调用objc_autoreleasePoolPush
函数。
第二个Observer
:activities为0xa0
(十进制为160 = 32
+ 128
),它用来监听kCFRunLoopBeforeWaiting
和kCFRunLoopExit
。当监听到kCFRunLoopBeforeWaiting
时,会先调用objc_autoreleasePoolPop函数,再调用objc_autoreleasePoolPush函数。当监听到kCFRunLoopExit
时,会调用objc_autoreleasePoolPop函数
autorelease
对象在当前RunLoop休眠
之前释放。局部对象
在方法结束时释放,因为ARC生成的是release,而不是autorelease。
5.2 autoreleasepool和多线程
主线程我们前面已经讨论过,主线程默认为我们开启 Runloop,Runloop 会自动帮我们创建Autoreleasepool,并进行Push、Pop 等操作来进行内存管理。
在子线程你创建了 Pool 的话,产生的 Autorelease 对象就会交给 pool 去管理。如果你没有创建 Pool ,但是产生了 Autorelease 对象,就会调用 autoreleaseNoPage
方法。在这个方法中,会自动帮你创建一个 hotpage,并调用page->add(obj)
将对象添加到 AutoreleasePoolPage 的栈中,也就是说你不进行手动的内存管理,也不会内存泄漏啦!
如果没有pool管理(没有pop),在线程退出时,也会释放资源,调用tls_dealloc
方法,清空对象以及AutoreleasePoolPage。
参考:
iOS 各个线程 Autorelease 对象的内存管理
探索子线程autorelease对象的释放时机
子线程AutoRelease对象何时释放