iOS原理 文章汇总
前面介绍了AutoreleasePool的基本概念和内存结构,本文将通过objc
源码来分析AutoreleasePool
的底层实现。
AutoreleasePool的Clang编译
使用@autoreleasePool
代码块可以创建一个自动释放池,通过Clang
编译后发现底层实现如下:
{
//创建一个AutoreleasePool对象
__AtAutoreleasePool *atautoreleasepoolobj = objc_autoreleasePoolPush();
//这里创建自动释放的对象,创建的对象会被加入到AutoreleasePool对象里
... ...
//给所有自动释放的对象发送一次release消息,并销毁AutoreleasePool对象
objc_autoreleasePoolPop(atautoreleasepoolobj)
}
因此,单个Autoreleasepool
的运行过程可以简单地理解为 objc_autoreleasePoolPush
、[对象 autorelease]
和 objc_autoreleasePoolPop
这三个过程。
push 操作
objc_autoreleasePoolPush
函数在底层是调用AutoreleasePoolPage
的push
函数。在源码中断点调试可知,push
函数的调用链为:push
-> autoreleaseFast(POOL_BOUNDARY)
-> autoreleaseNoPage(POOL_BOUNDARY)
。
push()
static inline void *push()
{
id *dest;
//这个判断不知道哪个实际场景可以触发,运行后都是false。源码注释:halt when autorelease pools are popped out of order, and allow heap debuggers to track autorelease pools
//老版本源码里没有这个判断,直接走 dest = autoreleaseFast(POOL_BOUNDARY)
if (slowpath(DebugPoolAllocation)) {
//这里基本不会进,而且全局搜索发现autoreleaseNewPage函数只在这里有调用
// 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);
//返回的dest就是后面执行pop函数的入参
return dest;
}
push
里直接调用autoreleaseFast(POOL_BOUNDARY)
函数,返回的dest
即为后面执行pop
函数的入参,本质是哨兵地址。
autoreleaseFast(POOL_BOUNDARY)
static inline id *autoreleaseFast(id obj)
{
//获取当前操作的page,hot page
AutoreleasePoolPage *page = hotPage();
//判断当前页
if (page && !page->full()) {
//如果page存在并且没有满
return page->add(obj);
} else if (page) {
//如果page存在并且已经满了
return autoreleaseFullPage(obj, page);
} else {
//如果page不存在
return autoreleaseNoPage(obj);
}
}
这个方法里先获取当前操作的page
,然后进行判断:
- 如果当前页存在,并且没有满,就调用
add
函数压栈对象。 - 如果当前也存在且已经满了,就调用
autoreleaseFullPage
函数创建新页面,并压栈对象。 - 如果当前页不存在,即自动释放池为
nil
或者为empty(空的)
,就调用autoreleaseNoPage
函数来设置空白占位符,或者创建第一页。
这里push
操作调用的autoreleaseFast
,会执行autoreleaseNoPage
函数,入参为POOL_BOUNDARY
。
autoreleaseNoPage(POOL_BOUNDARY)
static __attribute__((noinline))
id *autoreleaseNoPage(id obj)
{
//"No page"表示还没创建自动释放池,或者创建了一个空内容的释放池。
// "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;
}
//若当前obj不是哨兵,且释放池为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;
}
//若当前obj为哨兵,就设置空白占位符并返回。只有push操作时当前obj才是哨兵
else if (obj == POOL_BOUNDARY && !DebugPoolAllocation) {
//push操作会走到这里,然后返回
// 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();
}
//从这里开始,下面就是第一次autorelease操作处理的事项
// We are pushing an object or a non-placeholder'd pool.
// Install the first page.
//压栈第一个第一个对象时,需要创建第一页
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
//将第一页设置为hot page
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);
}
autoreleaseNoPage
函数在push
操作和第一次autorelease
操作时会调用。在push
操作时,会在这里调用setEmptyPoolPlaceholder
函数来设置一个空白占位符,表示已创建了一个自动释放池。
//设置空白占位符
static inline id* setEmptyPoolPlaceholder()
{
ASSERT(tls_get_direct(key) == nil);
//通过"key-value"形式将空白占位符存储在tls中,并返回空白占位符的地址
//tls为当前线程的本地存储,用于保存当前线程的一些信息
tls_set_direct(key, (void *)EMPTY_POOL_PLACEHOLDER);
return EMPTY_POOL_PLACEHOLDER;
}
//判断是否设置空白占位符
static inline bool haveEmptyPoolPlaceholder()
{
//通过key-value获取值来判断
id *tls = (id *)tls_get_direct(key);
return (tls == EMPTY_POOL_PLACEHOLDER);
}
//自动占位符,地址为 0x1
# define EMPTY_POOL_PLACEHOLDER ((id*)1)
//key值
static pthread_key_t const key = AUTORELEASE_POOL_KEY;
# define AUTORELEASE_POOL_KEY ((tls_key_t)__PTK_FRAMEWORK_OBJC_KEY3)
因此,push
操作本质上是设置一个空白占位符,地址为0x1
,并存储在当前线程的tls
中,通过这个空白占位符来判断自动释放池是否已创建。在push
过程中,既没有创建page
,也没有压栈哨兵,可以通过下面的代码来印证:
int main(int argc, const char * argv[]) {
@autoreleasepool {
//第一次打印
_objc_autoreleasePoolPrint();
//第二次打印
NSObject *obj = [[NSObject alloc] autorelease];
_objc_autoreleasePoolPrint();
}
return 0;
}
//打印结果
//第一次打印
objc[19922]: ##############
objc[19922]: AUTORELEASE POOLS for thread 0x1000dedc0
objc[19922]: 0 releases pending. //0
objc[19922]: [0x1] ................ PAGE (placeholder)
objc[19922]: [0x1] ################ POOL (placeholder)
objc[19922]: ##############
//第二次打印
objc[19922]: ##############
objc[19922]: AUTORELEASE POOLS for thread 0x1000dedc0
objc[19922]: 2 releases pending. //2:一个哨兵 + 一个对象
objc[19922]: [0x10080b000] ................ PAGE (hot) (cold)
objc[19922]: [0x10080b038] ################ POOL 0x10080b038
objc[19922]: [0x10080b040] 0x100667360 NSObject
objc[19922]: ##############
可以看到,第一次打印时,只执行了push
操作,只有空白占位符,地址为0x1
。第二次打印时,已经压栈了第一个对象,此时才创建了第一页page
,并压栈了哨兵和对象。
autorelease 操作
给对象发送autorelease
消息即可加入到自动释放池,autorelease
方法在底层是调用AutoreleasePoolPage
的autorelease
函数,入参为对象本身。
inline id
objc_object::rootAutorelease()
{
//若是TaggedPointer对象,直接返回,不处理
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);
}
//autorelease操作
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;
}
从代码中可知,若对象是TaggedPointer
类型,则不能被添加到自动释放池。autorelease
操作实际上是通过调用autoreleaseFast
函数来压栈对象,实现逻辑为(源码上面已附):
- 若
hot page
不存在,即压栈的是第一个对象,此时是空白的自动释放池,就调用autoreleaseNoPage
函数来创建第一页page
,并设为hot page
,先压栈哨兵,然后压栈对象。 - 若
hot page
存在且没满时,就调用add
函数来压栈对象。 - 若
hot page
存在且已满,就调用autoreleaseFullPage
函数来新建一页page
,再压栈对象。
接下来分析这其中几个关键函数的实现:
hotPage
//设置hot page
static inline void setHotPage(AutoreleasePoolPage *page)
{
if (page) page->fastcheck();
//通过key-value形式,将当前page存储在tls中,即为hot page
tls_set_direct(key, (void *)page);
}
//获取hot page
static inline AutoreleasePoolPage *hotPage()
{
//从当前线程的tls中通过key获取到page,即为hot page
AutoreleasePoolPage *result = (AutoreleasePoolPage *)
tls_get_direct(key);
if ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil;
if (result) result->fastcheck();
return result;
}
hot page
和上面的空白占位符一样,存储在当前线程的tls
中,通过key
来读写。
add
id *add(id obj)
{
ASSERT(!full());
unprotect();
id *ret = next; // faster than `return next-1` because of aliasing
//先从next指针指向的地址里开始存放obj的地址,为8字节
//再将next指针像高地址偏移8字节,指向的地址用于存放下一次添加的obj的地址
*next++ = obj;
protect();
return ret;
}
add(obj)
实际上是先从当前next
指针指向的地址里开始存放obj
的地址,为8
字节大小,再将next
指针像高地址偏移8
字节,新指向的地址用于存放下一次添加的obj
的地址。当压栈第一个对象时,需要先压栈哨兵。
//在第一页里先压栈哨兵
page->add(POOL_BOUNDARY);
# define POOL_BOUNDARY nil
POOL_BOUNDARY
为nil
,说明压栈哨兵其实只是将next
指针偏移8
字节,从新地址上开始存放对象地址。因此,哨兵其实是自动释放池中对象的边界,而这个边界地址就是在后面执行pop
操作时的入参。
autoreleaseFullPage
压栈对象时,若hot page
满了,就调用autoreleaseFullPage
函数来新建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);
//这个函数传进来的已经是hot page
//do-while循环获取没满的page
do {
//如果存在子页面,就用子页面替换当前page来判断
if (page->child) page = page->child;
//如果没有子页面,而当前page已满,则新建页面
else page = new AutoreleasePoolPage(page);
} while (page->full()); //判断page是否已满
//将page设置为hot page
setHotPage(page);
//压栈对象
return page->add(obj);
}
//判断page是否已满
bool full() {
//通过next的值和page的结束地址来判断
return next == end();
}
//page的end地址
id * end() {
//this为当前page的首地址,通过首地址加上4096字节,就可得出当前page的结束地址
return (id *) ((uint8_t *)this+SIZE);
}
//SIZE即为每页AutoreleasePoolPage的大小,为4096字节
static size_t const SIZE = 4096
因此,压栈对象时,若hot page
已满,通过do-while
循环来判断每一个子页面是否已满,若没满,则将当前子页面设为hot page
,若都满了,就新建一个页面并设为hot page
,再压栈对象。判断page
是否已满,是通过将page
的结束地址和next
指针对比,若相等,则表示page
已满。
pop 操作
pop
操作是向自动释放池里的每个对象发送一次release
消息,然后销毁掉释放池里的页面。objc_autoreleasePoolPop
函数在底层是调用AutoreleasePoolPage
的pop
函数,入参为哨兵地址。
static inline void
pop(void *token)
{
//传进来的token即为哨兵地址
AutoreleasePoolPage *page;
id *stop;
//如果token为空白占位符
if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
// Popping the top-level placeholder pool.
//获取hot page
page = hotPage();
if (!page) {
// Pool was never used. Clear the placeholder.
//如果page不存在,说明这个自动释放池没有压栈任何对象,直接清除占位符
//hotPage和空白占位符在tls中存储都是同一个key,所以可以清空
return setHotPage(nil);
}
// Pool was used. Pop its contents normally.
// Pool pages remain allocated for re-use as usual.
//容错处理,hot page存在,而哨兵地址异常,则需要更正
//如果页面存在,获取到第一页
page = coldPage();
//将token的值设置为第一页的哨兵地址
token = page->begin();
} else {
//获取token所在的页
page = pageForPointer(token);
}
//将token赋值给stop,则stop此时就是哨兵地址
stop = (id *)token;
//容错判断,若stop指向的不是哨兵地址
if (*stop != POOL_BOUNDARY) {
if (stop == page->begin() && !page->parent) {
//若stop指向当前页的begin(),且当前页没有父页面,说明当前也是第一页,stop指向的就是哨兵地址,不做任何处理
// 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 {
//若stop指向当前页的begin(),而当前页有父页面,说明stop指向的不是哨兵地址,则报异常并返回
// Error. For bincompat purposes this is not
// fatal in executables built with old SDKs.
return badPop(token);
}
}
//这个判断基本为false
if (slowpath(PrintPoolHiwat || DebugPoolAllocation || DebugMissingPools)) {
return popPageDebug(token, page, stop);
}
//出栈页
return popPage(token, page, stop);
}
pop
操作中传进来的token
即为哨兵地址,这里主要处理两件事:
- 如果当前是只有空白占位符的自动释放池,则清除掉空白符,然后返回,这个释放池也就被销毁了。
- 如果不是空白的自动释放池,先确保
token
为哨兵地址,然后获取到token
所在的页page
,并设置结束位stop
(也是哨兵地址),最后调用popPage
出栈页,完成pop
操作。
也就是说,如果当前是空白的自动释放池,pop
操作就会清除掉空白占位符,销毁释放池;如果不是空白的自动释放池,pop
操作是通过调用popPage
函数来出栈页,这个函数的三个入参:token
和stop
均为哨兵地址,page
为当前哨兵所在的页。如果自动释放池没有嵌套,page
就为自动释放池的第一页,如果有嵌套,每个子pool
都会有一个哨兵,page
就为当前pool
哨兵所在的最外层pool
中的页。可以在源码中打印这三个参数来印证。
//创建一个pool
@autoreleasepool {
//压栈一个对象
NSObject *obj = [[NSObject alloc] autorelease];
//打印pool的内存
_objc_autoreleasePoolPrint();
}
//在源码的pop(void *token)函数中,最后打印一下popPage函数的三个入参
pop(void *token)
{
... ...
printf(" ==== popPage函数的入参:token = %p, page = %p, stop = %p\n", token, page, stop);
return popPage(token, page, stop);
}
//打印结果
objc[64136]: ##############
objc[64136]: AUTORELEASE POOLS for thread 0x1000dedc0
objc[64136]: 2 releases pending.
objc[64136]: [0x10180a000] ................ PAGE (hot) (cold)
objc[64136]: [0x10180a038] ################ POOL 0x10180a038
objc[64136]: [0x10180a040] 0x101148a20 NSObject
objc[64136]: ##############
==== popPage函数的入参:token = 0x10180a038, page = 0x10180a000, stop = 0x10180a038
从打印结果可以印证,popPage
函数的三个入参:token
和stop
均为哨兵地址,page
为自动释放池的第一页。这个示例中,pool
没有嵌套,所以只有一个哨兵,并且处于第一页,也可以试试多嵌套几个子pool
,验证下入参情况是否和上面的结论一致。
接下来再分析pop
函数中几个关键函数的实现:
coldPage
static inline AutoreleasePoolPage *coldPage()
{
//获取到当前页
AutoreleasePoolPage *result = hotPage();
//while循环获取到自动释放池的第一页
if (result) {
while (result->parent) {
result = result->parent;
result->fastcheck();
}
}
return result;
}
这个函数其实就是通过hotPage
来获取到自动释放池的第一页。
begin
id * begin() {
//当前页的首地址 + 自身成员占用的内存(56字节),即为begin的地址
return (id *) ((uint8_t *)this+sizeof(*this));
}
页面的首地址加上自身成员占用的56
字节大小,就是begin
位置。第一页,begin
位置为哨兵的起始地址,其它页则为该页第一个对象的起始地址。
pageForPointer
static AutoreleasePoolPage *pageForPointer(const void *p)
{
//p为传进来的token,即哨兵地址
return pageForPointer((uintptr_t)p);
}
static AutoreleasePoolPage *pageForPointer(uintptr_t p)
{
AutoreleasePoolPage *result;
//SIZE是一页page的大小,为4096字节
//p % SIZE,是因为当pool嵌套时,需要先release子pool里的对象,通过这个方法可以获取到子pool距其在父pool中所在页起始位置的偏移量
uintptr_t offset = p % SIZE;
ASSERT(offset >= sizeof(AutoreleasePoolPage));
//p - offset,即可获取到子pool所在的父pool的页的起始地址
result = (AutoreleasePoolPage *)(p - offset);
result->fastcheck();
return result;
}
这个函数就是获取哨兵所在的页,当pool
嵌套时,先release
子pool
,就需要获取到子pool
在父pool
里所处的页。
popPage
template
static void
popPage(void *token, AutoreleasePoolPage *page, id *stop)
{
/**
* 出栈页,这个函数的三个入参:
* token:哨兵地址
* page:哨兵所在的页,一般为自动释放池的第一页
* stop:哨兵地址
*/
//pop操作入参的allowDebug为false
if (allowDebug && PrintPoolHiwat) printHiwat();
//release all objs,给自动释放池中哨兵后的所有对象发送一次release消息
//执行完这一步,自动释放池里就只剩下空页面了
page->releaseUntil(stop);
// memory: delete empty children
//kill page,移除页面,至少会保留第一页
if (allowDebug && DebugPoolAllocation && page->empty()) {
// special case: delete everything during page-per-pool debugging
//debug判断,pop操作不会进这里,因为传的allowDebug为false
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
//debug判断,pop操作不会进这里,因为传的allowDebug为false
page->kill();
setHotPage(nil);
} else if (page->child) {
// hysteresis: keep one empty child if page is more than half full
//有多个页面的自动释放池,在pop操作时会进入这里
if (page->lessThanHalfFull()) {
//若当前页的对象数量比一半还少,从第一页的子页面开始kill,只保留第一页
//如果没有嵌套pool,实际上在上面执行完releaseUntil(stop)后,page都是空页面,所以pop操作基本会走这里
//如果有嵌套pool,传进来的page可能为子pool处于最外层pool的页,此时release完子pool的对象指针后,当前页保存的外层pool的对象指针数可能会大于页数量的一半,就会触发moreThanHalfFull的条件
page->child->kill();
}
else if (page->child->child) {
//当前页的对象指针数量比一半还多,且页面数量大于3,就从第三个页开始kill,保留第一页和第二页
page->child->child->kill();
}
}
}
从代码分析可知,pop
操作调用popPage
来出栈页时,主要做了两个处理:
-
release all objs
:给自动释放池中哨兵后的所有对象发送一次release
消息。这一步执行完,自动释放池里就只剩下空页面了。 -
kill page
:移除页面,只保留第一页,其他页面都会被移除。
接下来再分析popPage
函数中几个关键函数的实现:
releaseUntil
void releaseUntil(id *stop)
{
// Not recursive: we don't want to blow out the stack
// if a thread accumulates a stupendous amount of garbage
//popPage函数传进来的stop为哨兵地址,this为自动释放池的第一页
//this ->next,即第一页的next指针
//判断第一页的对象是否被release完
while (this->next != stop) {
// Restart from hotPage() every time, in case -release
// autoreleased more objects
//获取到hot page,当前操作的页面,即最后一页
AutoreleasePoolPage *page = hotPage();
// fixme I think this `while` can be `if`, but I can't prove it
//如果当前page为空,则获取到父页面,并将父页面设为hot page
//确保下一次获取的hot page为这个父页面,实现了页面从后往前的遍历
while (page->empty()) {
page = page->parent;
setHotPage(page);
}
page->unprotect();
//每次都获取当前页的最后一个对象,实现了对象从后往前遍历释放
id obj = *--page->next;
//获取完后偏移next指针,确保一直指向当前页的最后一个对象地址
memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
page->protect();
//如果对象不为nil(不是哨兵),就释放
if (obj != POOL_BOUNDARY) {
objc_release(obj);
}
}
//释放完所有的对象后,就将第一页设为hot page
setHotPage(this);
#if DEBUG
// we expect any children to be completely empty
for (AutoreleasePoolPage *page = child; page; page = page->child) {
ASSERT(page->empty());
}
#endif
}
从代码分析可知,releaseUntil
函数的处理逻辑为:从后往前遍历自动释放池里的页面,在每一页中再从后往前遍历释放对象,直到哨兵为止。释放完对象后,释放池中就只剩下空的页面,并将第一页设为hot page
。
lessThanHalfFull
//判断当前页是否还没存满一半
bool lessThanHalfFull() {
//next:当前页最后一个对像指针在页里的地址
//begin():前面分析了,为当前页的首地址 + 58字节(自身成员的大小)
//end():当前页的结束地址
return (next - begin() < (end() - begin()) / 2);
}
//当前页的结束地址
id * end() {
//当前页的首地址 + 4096字节,就为当前页的结束地址
//每页大小为4096字节
return (id *) ((uint8_t *)this+SIZE);
}
从代码分析可知,是通过当前页最后一个对象指针在页中地址的偏移量计算,来判断当前页是否存满一半。
kill
void kill()
{
// Not recursive: we don't want to blow out the stack
// if a thread accumulates a stupendous amount of garbage
//pop操作是从自动释放池的第二页开始kill
//因此这里的this是第二页,可以通过打印输出来印证
//从第二页开始,循环遍历子页面,最终获取到自动释放池的最后一页
AutoreleasePoolPage *page = this;
while (page->child) page = page->child;
//从最后一页开始往前遍历销毁,直到this(即第二页)销毁后停止
//每页的处理逻辑为:先获取到当前页面的父页面,再将父页面的子页面设为nil,即销毁了当前页面
AutoreleasePoolPage *deathptr;
do {
deathptr = page;
//获取到父页面
page = page->parent;
if (page) {
page->unprotect();
//销毁子页面
page->child = nil;
page->protect();
}
delete deathptr;
} while (deathptr != this);
}
从代码分析可知,kill
函数是从自重释放池的队后一页开始往前遍历销毁,直到销毁第二页后停止。每一次销毁页面,都是先获取到当前页面的父页面,再将父页面的子页面设为nil
,即实现了页面销毁。
因此,pop
操作时,如果不是空白的自动释放池,是先从后往前遍历自动释放池里的所有页面,将每一页里的对象从后往前逐步release
,然后再次从后往前遍历销毁这些空页面,第一页保留不销毁,最后自动释放池里就只剩下一页空的page
,并且为hot page
,这就完成了pop
操作。
- 纠正关于
pop
操作的误解
注意,之前我一直以为pop
操作最后会销毁自动释放池,但分析完源码后,才发现只有空白的自动释放池,执行pop
操作才会被销毁,而如果存有压栈对象的自动释放池,pop
操作只是release
对象和kill
页面,并没有销毁自动释放池,这点可以通过在源码工程中打印输出来印证。
//在main函数里创建自动释放池
int main(int argc, const char * argv[]) {
@autoreleasepool {
//往自动释放池里添加`505 * 2`个对象,这样就会创建3页page
NSInteger count = 505 * 2;
for(NSInteger i=0; ireleaseUntil(stop);
//在release结束后打印自动释放池的内存
_objc_autoreleasePoolPrint();
//kill page
... ....
//在kill page后再打印自动释放池的内存
_objc_autoreleasePoolPrint();
}
//在kill函数里打印this,印证是否只kill到第二页
void kill()
{
// Not recursive: we don't want to blow out the stack
// if a thread accumulates a stupendous amount of garbage
printf(" ==== kill() -> this : %p\n", this);
... ...
}
//打印结果
//这是在release结束后输出的自动释放池,可以看到里面只剩下三个空page
objc[76175]: ##############
objc[76175]: AUTORELEASE POOLS for thread 0x1000dedc0
objc[76175]: 0 releases pending.
objc[76175]: [0x102809000] ................ PAGE (hot) (cold)
objc[76175]: [0x103009000] ................ PAGE
objc[76175]: [0x10300b000] ................ PAGE
objc[76175]: ##############
//这是在kill函数里打印的this指针,可以看到this为第二页
==== kill() -> this : 0x103009000
//这是在kill page后输出的自动释放池,可以看到最后只剩下第一页
objc[76175]: ##############
objc[76175]: AUTORELEASE POOLS for thread 0x1000dedc0
objc[76175]: 0 releases pending.
objc[76175]: [0x102809000] ................ PAGE (hot) (cold)
objc[76175]: ##############
Program ended with exit code: 0
从打印结果可以印证,当自动释放池里存有压栈对象时,pop
操作只是release
对象和kill
页面,并没有销毁自动释放池,而且池中只剩下第一页。这点会在后面分析自动释放池嵌套的时候再作说明。
总结
经过对源码的深入分析,我们可以得出以下结论:
一个自动释放池的运行过程分为这三步:
push 操作
、[对象 autorelease]
以及pop 操作
。push 操作
本质上是设置一个空白占位符,地址为0x1
,并存储在当前线程的本地存储tls
中,设置了这个空白占位符就表示已经创建了一个空的自动释放池。在push
过程中,既没有创建page
,也没有压栈哨兵。-
autorelease 操作
就是压栈对象,给对象发送autorelease
消息,将对象添加到自动释放池中,在这个过程中会做如下处理:- 如果压栈的是第一个对象,此时是空白的自动释放池,就创建第一页
page
,并设为hot page
,然后先压栈哨兵,再压栈对象。 - 若
hot page
存在且没满时,直接压栈对象。 - 若
hot page
存在且已满,新建一页page
,再压栈对象。
- 如果压栈的是第一个对象,此时是空白的自动释放池,就创建第一页
-
pop 操作
出栈时,会根据自动释放池的情况分别做不同的处理:- 若是空白的自动释放池,即内部只有空白占位符,此时
pop
操作会清除占位符,销毁自动释放池。 - 若自动释放池存有压栈对象,就先
release
对象,再kill
页面。最后,自动释放池并没有被销毁,里面还存有一张空的page
,也是之前的第一页。
- 若是空白的自动释放池,即内部只有空白占位符,此时
-
对于不是空白的自动释放池,
pop 操作
过程中具体做了如下处理:-
release对象
:从后往前遍历自动释放池里的页面,在每一页中再从后往前遍历释放对象,直到哨兵为止。release
后,池中只剩下空白的页面,然后将第一页设为hot page
。 -
kill 页面
:从后往前遍历销毁这些空页面,第一页保留不销毁。
-