携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第7天,点击查看活动详情 。如果哪里写的不对,请大家评论批评。本篇文章整理了有几天的时间,可能有部分大家都看过,我相信自己整理的文档相当的齐全,如果还有哪里不到位的请大家评论留言,笔者在去学习与思考。
Autoreleasepool详细解析
Autoreleasepool
自动释放池,管理局部变量,在作用域结束的时候,统一对内部的变量做release操作。
Main起点
#import <Foundation/Foundation.h>
#import "Person.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person * p = [[Person alloc]init];
}
return 0;
}
在autoreleasepool的用法上,main函数可以说是第一个使用autoreleasepool{}的地方,Main函数会开启RunLoop导致程序持续运行,不会退出,也就是作用域一直存在,那么为什么在这里会有一个autoreleasepool呢?这里的对象怎么去销毁呢?今天我们来详细的研究一下。
源码
常规操作对main进行编译生成C++文件。
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m
在对底部可以找到一段这样的代码
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
Person * p = ((Person *(*)(id, SEL))(void *)objc_msgSend)((id)((Person *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("Person"), sel_registerName("alloc")), sel_registerName("init"));
}
return 0;
}
__AtAutoreleasePool
这里有一个__AtAutoreleasePool,在文件最下方可以找到对应的代码
extern "C" __declspec(dllimport) void * objc_autoreleasePoolPush(void);
extern "C" __declspec(dllimport) void objc_autoreleasePoolPop(void *);
struct __AtAutoreleasePool {
__AtAutoreleasePool() {
//构造函数,在创建结构体的时候调用
atautoreleasepoolobj = objc_autoreleasePoolPush();
}
~__AtAutoreleasePool() {
//析构函数,在结构体销毁的时候调用
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
void * atautoreleasepoolobj;
};
由此可见OC内部所有的对象都是一个结构体。
__AtAutoreleasePool()是一个构建函数,也就是在main函数中,
__AtAutoreleasePool __autoreleasepool;
会自动调用objc_autoreleasePoolPush();
在main函数中,
__AtAutoreleasePool __autoreleasepool;
在大括号{}里面,也就是局部变量,那个{}执行完成之后自动回执行析构函数,执行objc_autoreleasePoolPop()
很有意思的是构造函数生成了
atautoreleasepoolobj
对象,在析构函数的时候有销毁了atautoreleasepoolobj
对象
__AtAutoreleasePool主要的就是这个objc_autoreleasePoolPush
以及objc_autoreleasePoolPop
这里就需要去下载源码,我使用的objc4-680
,版本不同可能会存在一定的差异
void *
objc_autoreleasePoolPush(void)
{
if (UseGC) return nil;
return AutoreleasePoolPage::push();
}
void
objc_autoreleasePoolPop(void *ctxt)
{
if (UseGC) return;
AutoreleasePoolPage::pop(ctxt);
}
在这里得到一个关键的对象。AutoreleasePoolPage,也就是左右的操作都是由AutoreleasePoolPage来完成的。
也就是说,所有调用autorelease的对象都是由AutoreleasePoolPage来管理的
AutoreleasePoolPage
AutoreleasePoolPage在NSObject.mm有500+的代码,这里先提取一部分来展示
class AutoreleasePoolPage
{
#define POOL_SENTINEL nil
static pthread_key_t const key = AUTORELEASE_POOL_KEY;
static uint8_t const SCRIBBLE = 0xA3; // 0xA3A3A3A3 after releasing
static size_t const SIZE = PAGE_MAX_SIZE
static size_t const COUNT = SIZE / sizeof(id);
magic_t const magic;//用来校验 AutoreleasePoolPage 的结构是否完整;
id *next;// 当前栈最后一个对象的地址
pthread_t const thread;// 当前线程
AutoreleasePoolPage * const parent;// 指向上一个节点
AutoreleasePoolPage *child;// 指向下一个节点
uint32_t const depth;// 深度
uint32_t hiwat;
划重点:从类结构可以看出来AutoreleasePoolPage是一个双链表,为什么能看出来?看这里
AutoreleasePoolPage * const parent;// 指向上一个节点
AutoreleasePoolPage *child;// 指向下一个节点
Push
static inline void *push()
{
id *dest;
if (DebugPoolAllocation) {
// Debug模式下直接跳过,看下面真机模式
dest = autoreleaseNewPage(POOL_SENTINEL);
} else {
dest = autoreleaseFast(POOL_SENTINEL);
}
assert(*dest == POOL_SENTINEL);
return dest;
}
autoreleaseFast
static inline id *autoreleaseFast(id obj)
{
// 获取当前能够添加对象的page
AutoreleasePoolPage *page = hotPage();
if (page && !page->full()) {
return page->add(obj);
} else if (page) {
return autoreleaseFullPage(obj, page);
} else {
return autoreleaseNoPage(obj);
}
}
hotPage
获取链表中最后一个page,tls_get_direct
没理解,求大佬可以解答:
static inline AutoreleasePoolPage *hotPage()
{
AutoreleasePoolPage *result = (AutoreleasePoolPage *)
tls_get_direct(key);
if (result) result->fastcheck();
return result;
}
这里出现了几种情况,
1、当前page存在,并且page没有装满,那么可以直接add这个对象(push的时候实际上是一个POOL_SENTINEL)
2、如果page存在,已经满了,那么会创建一个新的page,autoreleaseFullPage
3、如果page不存在,表示这是第一个page,autoreleaseNoPage
划重点: 这里有一个问题,可以看到每一次push传入的参数都是POOL_SENTINEL,也就是我们理解的哨兵对象,那么需要释放的对象怎么入栈的呢?这个问题下面会给大家答案 请直接看《局部变量怎么入栈?》
怎么判断是否满了呢?
bool full() {
return next == end();
}
id * end() {
return (id *) ((uint8_t *)this+SIZE);
}
当前的指针等于page指针+Size,也就是说,当前指针指向最后一位了,后面没有空间了~~~~
autoreleaseFullPage
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);
}
这里有个while循环,一直在判断当前page已经满了。在循环里有个
if,等下看着if,大致就是获取到一个page,setHotPage从上文可以得知应该是设置当前使用的page,最后add
下面看下if
1、如果page->child意思是如果已经有了下一个链表,也就是已经有了一个新的page,把page->child赋值给page
2、else也就是没有新page,那么久去创建一个page,注意这里有个入参,当前的page?里面干啥了? 走去喽喽去
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();
}
这是个什么写法?????我学识浅薄不太懂...........看着都是AutoreleasePoolPage的参数,咱们就理解为一种写法吧,把入参分解的写法
大致的意思是创建一个新的page,把上一个节点的child指向当前的新的page,完成链表
unprotect && protect
在代码中经常发现一起使用,看起来像一个锁。
inline void unprotect() {
#if PROTECT_AUTORELEASEPOOL
check();
mprotect(this, SIZE, PROT_READ | PROT_WRITE);
#endif
}
inline void protect() {
#if PROTECT_AUTORELEASEPOOL
mprotect(this, SIZE, PROT_READ);
check();
#endif
}
看起来像是读写锁,unprotect
多了PROT_WRITE
。
autoreleaseNoPage
回到这里,
static __attribute__((noinline))
id *autoreleaseNoPage(id obj)
{
// No pool in place.
assert(!hotPage());
if (obj != POOL_SENTINEL && DebugMissingPools) {
// We are pushing an object with no pool in place,
// and no-pool debugging was requested by environment.
_objc_inform("MISSING POOLS: Object %p of class %s "
"autoreleased with no pool in place - "
"just leaking - break on "
"objc_autoreleaseNoPool() to debug",
(void*)obj, object_getClassName(obj));
objc_autoreleaseNoPool(obj);
return nil;
}
// Install the first page.
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
setHotPage(page);
// Push an autorelease pool boundary if it wasn't already requested.
if (obj != POOL_SENTINEL) {
page->add(POOL_SENTINEL);
}
// Push the requested object.
return page->add(obj);
}
我们理解了如果分配一个page,这里大致也了解了,其实就是判断一下你是否应有page,如果有直接断言了。
也就是说,第一次(没有创建过page才会走到这里),直接就是AutoreleasePoolPage入参还是nil,不需要设置父节点(因为没有父节点,他就是第一个),然后直接add,这里大概率add的也是一个哨兵对象,真实的数据理论不会到走这个方法
这里整个push就完事了
不对不对??有个问题,push的时候都是哨兵对象啊?真实的数据怎么进来的呢?
咦??是啊,我去找找
局部变量怎么入栈?
首先找到最后在哪里入栈对象的,autoreleaseFast
方法。我们知道在push的时候autoreleaseFast
入参是POOL_SENTINEL
,也就是说我们需要找到不是入参POOL_SENTINEL
的。
autorelease
只有它
static inline id autorelease(id obj)
{
assert(obj);//对象存在
assert(!obj->isTaggedPointer());//对象是优化过的指针对象
id *dest __unused = autoreleaseFast(obj);//入栈,进入到page,就是它了
assert(!dest || *dest == obj);
return obj;
}
谁来调用它呢?
我有点疯了~~
objc_object::rootAutorelease2()
__attribute__((noinline,used))
id
objc_object::rootAutorelease2()
{
assert(!isTaggedPointer());
return AutoreleasePoolPage::autorelease((id)this);
}
autorelease周期
- [NSObject autorelease]
└── id objc_object::rootAutorelease()
└── id objc_object::rootAutorelease2()
└── static id AutoreleasePoolPage::autorelease(id obj)
└── static id AutoreleasePoolPage::autoreleaseFast(id obj)
├── id *add(id obj)
├── static id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
│ ├── AutoreleasePoolPage(AutoreleasePoolPage *newParent)
│ └── id *add(id obj)
└── static id *autoreleaseNoPage(id obj)
├── AutoreleasePoolPage(AutoreleasePoolPage *newParent)
└── id *add(id obj)
总结一下,也就是当对象直接autorelease的时候,会把当前对象入栈到当前的page中。
pop
源代码挺多,一点点分析
static inline void pop(void *token)
{
AutoreleasePoolPage *page;
id *stop;
page = pageForPointer(token);
stop = (id *)token;
if (DebugPoolAllocation && *stop != POOL_SENTINEL) {
// This check is not valid with DebugPoolAllocation off
// after an autorelease with a pool page but no pool in place.
_objc_fatal("invalid or prematurely-freed autorelease pool %p; ",
token);
}
if (PrintPoolHiwat) printHiwat();
//释放page中的对象
page->releaseUntil(stop);
// memory: delete empty children
if (DebugPoolAllocation && page->empty()) {
// special case: delete everything during page-per-pool debugging
AutoreleasePoolPage *parent = page->parent;
page->kill();
setHotPage(parent);
} else if (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();
}
}
}
-
传入的实际是一个POOL_SENTINEL哨兵对象
-
我们需要找到对应的page
-
通过
pageForPointer
去找page,入参是一个token
,这个方法等会细看,token
应该是一个哨兵指针 -
releaseUntil(stop),看起来像去释放空间了
-
中间略过,可以看到最后最有的节点都
kill()
了怎么理解这个地方呢?
@autoreleasepool {}会把整个空间变成一个局部的变量,下面代码如是
{ __ AtAutoreleasePool __autoreleasepool; Person * p = [[Person alloc]init]; }
__autoreleasepool
会走构造函数,初始化了一个哨兵对象插入到了page
中,并返回了这个哨兵对象的地址,在析构时候,又传入了这个哨兵对象地址。我们可以想象一个案例举个🌰:
ABCDE几个公司做核酸,当E公司来的时候会给你一个牌子(哨兵对象),代表后面这一块都是E公司员工,突然物业让E公司去其他对方做核酸,这个队伍就要释放E公司,从后面开始释放排队成员,直到遇到E公司的牌子,我们就可以很轻松的释放掉E公司员工
pageForPointer找到当前page
static AutoreleasePoolPage *pageForPointer(uintptr_t p)
{
AutoreleasePoolPage *result;
uintptr_t offset = p % SIZE;
assert(offset >= sizeof(AutoreleasePoolPage));
result = (AutoreleasePoolPage *)(p - offset);
result->fastcheck();
return result;
}
将指针与4096取模,得到当前指针的偏移量,最后得到Page
p = 0x100816048 p % SIZE = 0x48 result = 0x100816048 - 0x48 = 0x100816000
而最后调用的方法 fastCheck()
用来检查当前的 result
是不是一个 AutoreleasePoolPage
。
通过检查 magic_t
结构体中的某个成员是否为 0xA1A1A1A1
。
releaseUntil
循环释放对象,直到stop
void releaseUntil(id *stop)
{
while (this->next != stop) {
AutoreleasePoolPage *page = hotPage();
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_SENTINEL) {
objc_release(obj);
}
}
setHotPage(this);
}
一个大的while循环,release掉当前空间的所有变量,直到遇到哨兵指针。
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);
}
最终其实就是把page的child都置成了nil
总结
当pop的时候,传入一个哨兵对象,根据哨兵对象的位置地址,计算当前所在的page,然后releaseUntil将栈中的对象释放,知道stop也就是遇到哨兵对象。最后在kill()中将链表全部删除。
与RunLoop的关系
RunLoop的几种状态
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
};
-
kCFRunLoopEntry
- 即将进入Loop,runloop 每一次进入一个 mode,就通知一次外部 kCFRunLoopEntry,之后会一直以该 mode 运行,知道当前 mode 被终止,进而切换到其他 mode,并再次通知 kCFRunLoopEntry。
-
kCFRunLoopBeforeTimers
- 即将处理 Timer
-
kCFRunLoopBeforeSources
- 即将处理 Source
-
kCFRunLoopBeforeWaiting
- 即将进入休眠,如果能够从内核队列上读出 msg 则继续运行任务,如果当前队列上没多余消息,则进入睡眠状态。
-
kCFRunLoopAfterWaiting
- 刚从休眠中唤醒,mach_msg 终于从队列里读出了 msg,可以继续执行任务了。
-
kCFRunLoopExit
- kCFRunLoopExit
关系
iOS在主线程的Runloop中注册了2个Observer
1、当第1个Observer监听到了进入状态(kCFRunLoopEntry),就会调用objc_autoreleasePoolPush()
2、当第2个Observer监听到了即将休眠状态(kCFRunLoopBeforeWaiting)就会调用objc_autoreleasePoolPop()和objc_autoreleasePoolPush()
3、当第2个Observer监听到了即将退出状态(kCFRunLoopBeforeExit)就会调用objc_autoreleasePoolPop()
面试相关
什么时候经常用到autoreleasePool
- 需要创建很多临时对象的循环,可以在循环内使用自动释放池
方法里有局部对象,出了方法后会立即释放吗
- 在ARC,在局部方法结束的时候会对局部对象做relase操作,所以局部对象会立即释放
- 如果局部对象是放在了 autoreleasePool 自动释放池,在 runloop 迭代结束的时候释放
autorelease对象在什么时机会被调用release
-
如果对象直接被autoreleasepool包住,那在autoreleasepool大括号结束的时候就release;
- (void)viewDidLoad { Preson * preson = [[[Preson alloc] init] autorelease]; } // 会在大括号结束后就释放吗? // 不够严谨啊 - (void)viewwillAppear:(bool)animated{ [super viewwillAppear:animated]; } // 实际测试发现会在viewwillAppear和viewDidLoad之后被释放? // 实际和runloop有关。
-
如果对象不是被autoreleaspool包住,释放是由runloop控制的。在
所属的runloop循环中
,runloop休眠之前调用release
main函数为什么会有autoreleasePool
官方文档有一句这样的话:翻译过来
Cocoa 总是希望代码在自动释放池块中执行,否则自动释放的对象不会被释放并且您的应用程序会泄漏内存。
所以猜测,在mian之前可能会产生一些对象,需要再程序结束的时候释放,在mian之前,RunLoop还没有开启,这些对象不会随着runloop休眠释放。
相关
developer.apple.com/library/arc…
\
/