2022-07-01 05:49:48 来源 : 软件开发网
1、概述
2、底层探索
2.1、打印自动释放池结构
【资料图】
2.2、objc_autoreleasePoolPush
2.2.1、AutoreleasePoolPage
2.2.2、AutoreleasePoolPageData
2.2.3、push(对象压栈)
2.2.4、池页容量
2.3、objc_autoreleasePoolPop
2.3.1、pop(对象出栈)
2.3.2、popPage
2.3.3、releaseUntil
2.3.4、kill
3、嵌套使用
总结
1、概述OC中的一种内存自动回收机制
,它可以将加入AutoreleasePool
中的变量release
的时机延迟
当创建一个对象,在正常情况下,变量会在超出其作用域时
立即 release,如果将其加入到自动释放池中,这个对象并不会立即释放,而会等到 runloop 休眠 / 超出autoreleasepool作用域之后
进行释放
准备简单代码
#import int main(int argc, const char * argv[]) { @autoreleasepool { NSLog(@"Hello, World!"); } return 0;}
转换成.cpp文件:
clang -rewrite-objc main.m -o main.cpp
int main(int argc, const char * argv[]) { /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; NSLog((NSString *)&__NSConstantStringImpl__var_folders_jl_d06jlfkj2ws74_5g45kms07m0000gn_T_main_da0d58_mi_0); } return 0;}
autoreleasepool变成了__AtAutoreleasePool
类型声明的代码
struct __AtAutoreleasePool { __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();} ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);} void * atautoreleasepoolobj;};
前边了解过这种写法是在结构体 构造时调用objc_autoreleasePoolPush
函数,在 结构体退出作用域析构时调用objc_autoreleasePoolPop
函数,这两个函数也是下边研究的重点(在 main函数中 autoreleasepool处设置断点查看汇编也可以看到这两个函数的符号调用)
测试项目,关闭ARC
模式
手动添加一个对象到自动释放池,并打印自动释放池结构
// 导入 _objc_autoreleasePoolPrint 函数,用于打印自动释放池的结构extern void _objc_autoreleasePoolPrint(void);int main(int argc, const char * argv[]) { @autoreleasepool { NSObject *objc = [[[NSObject alloc] init] autorelease]; _objc_autoreleasePoolPrint(); } return 0;}
##############AUTORELEASE POOLS for thread 0x1000ebe002 releases pending.[0x10700b000] ................ PAGE (hot) (cold)[0x10700b038] ################ POOL 0x10700b038[0x10700b040] 0x100705f60 NSObject##############
_objc_autoreleasePoolPrint
调用 AutoreleasePoolPage::printAll()(通过AutoreleasePoolPage的命名空间调用printAll()
);按照自动释放池的结构,通过双向链表遍历page
,依次读取 page中的内容并进行打印
打印了当前自动释放池所属线程,与 2个需要释放的对象:哨兵对象:POOL
和 手动加入自动释放池的对象objc
当前的Page
信息,占56字节
,因为只有一页,即是冷页面,也是热页面
void *objc_autoreleasePoolPush(void){ // 调用 AutoreleasePoolPage 命名空间下的 push 函数 return AutoreleasePoolPage::push();}
2.2.1、AutoreleasePoolPageAutoreleasePoolPage
的定义,能看到这样一段注释
/*********************************************************************** 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. 每个指针要么是一个要释放的对象,要么是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. 池令牌是指向该池的POOL_BOUNDARY的指针。当池被弹出,每个比哨兵热的对象都被释放 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. 线程本地存储指向热页,其中存储新自动释放的对象**********************************************************************/
AutoreleasePoolPage继承于AutoreleasePoolPageData
(有用的内容基本都在 AutoreleasePoolPageData结构体中)
class AutoreleasePoolPage : private AutoreleasePoolPageData{friend struct thread_data_t;public:static size_t const SIZE =#if PROTECT_AUTORELEASEPOOLPAGE_MAX_SIZE; // must be multiple of vm page size#elsePAGE_MIN_SIZE; // size and alignment, power of 2#endifprivate:static pthread_key_t const key = AUTORELEASE_POOL_KEY;static uint8_t const SCRIBBLE = 0xA3; // 0xA3A3A3A3 after releasingstatic size_t const COUNT = SIZE / sizeof(id); static size_t const MAX_FAULTS = 2; ...}
2.2.2、AutoreleasePoolPageDataclass AutoreleasePoolPage;struct AutoreleasePoolPageData{#if SUPPORT_AUTORELEASEPOOL_DEDUP_PTRS struct AutoreleasePoolEntry { uintptr_t ptr: 48; uintptr_t count: 16; static const uintptr_t maxCount = 65535; // 2^16 - 1 }; static_assert((AutoreleasePoolEntry){ .ptr = MACH_VM_MAX_ADDRESS }.ptr == MACH_VM_MAX_ADDRESS, "MACH_VM_MAX_ADDRESS doesn"t fit into AutoreleasePoolEntry::ptr!");#endifmagic_t const magic;__unsafe_unretained id *next;pthread_t const thread;AutoreleasePoolPage * const parent;AutoreleasePoolPage *child;uint32_t const depth;uint32_t hiwat;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){}};
结构体中,包含以下成员变量:(根据下边的成员大小得出 一个page占56字节)
magic
:用来校验 AutoreleasePoolPage的结构是否完整(16字节)next
:指向最新添加的autoreleased
对象的下一个位置,初始化时执行begin()
:获取对象压栈的起始位置(8字节)thread
:指向当前线程(8字节)parent
:指向父节点,第一个节点的parent
值为nil
(8字节)child
:指向子节点,最后一个节点的child
值为nil
(8字节)depth
:代表深度,从0
开始,往后递增1
(4字节)hiwat
:代表high water mark
最大入栈数量标记(4字节)2.2.3、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;}
DebugPoolAllocation
:当自动释放池按顺序弹出时停止,并允许堆调试器跟踪自动释放池
不存在,调用autoreleaseNewPage
函数,从一个新的池页开始创建
否则,调用autoreleaseFast
函数,将哨兵对象压栈
autoreleaseFast
若存在 page,且未存满,调用add
函数
若存在 page,但已存满,调用autoreleaseFullPage
函数
遍历链表,找到最后一个空白的子页面
对其进行创建新页
设置为热页面
添加对象
不存在 page,调用autoreleaseNoPage
函数调用 AutoreleasePoolPage构造函数,创建新页通过父类 AutoreleasePoolPageData进行初始化
begin
:获取对象压栈的起始位置(sizeof(*this)
:大小取决于自身结构体中的成员变量、返回对象可压栈的真正开始地址,在成员变量以下)
objc_thread_self
:通过tls
获取当前线程
链接双向链表
设置为热页面
pushExtraBoundary
为YES
,哨兵对象压栈
对象压栈
2.2.4、池页容量int main(int argc, const char * argv[]) { @autoreleasepool { for (int i = 0; i < 505; i++) { NSObject *objc = [[[NSObject alloc] init] autorelease]; } _objc_autoreleasePoolPrint(); } return 0;}-------------------------objc[1804]: ##############objc[1804]: AUTORELEASE POOLS for thread 0x1000ebe00objc[1804]: 506 releases pending.objc[1804]: [0x10200c000] ................ PAGE (full) (cold)objc[1804]: [0x10200c038] ################ POOL 0x10200c038objc[1804]: [0x10200c040] 0x100638420 NSObjectobjc[1804]: [0x10200c048] 0x100637a40 NSObjectobjc[1804]: [0x10200c050] 0x100636970 NSObject...objc[1804]: [0x100809000] ................ PAGE (hot) objc[1804]: [0x100809038] 0x10063a0b0 NSObjectobjc[1804]: ##############
505个 NSObject对象循环加入自动释放池,当存储 504个对象时,池页已满,第 505个对象创建新池页存储
一页的容量:504 * 8 = 4032
,加上56字节
成员变量和8字节
哨兵对象,共计4096
字节
每一页都存在56字节
的成员变量
一个自动释放池,只会压栈一个哨兵对象
2.3、objc_autoreleasePoolPopvoidobjc_autoreleasePoolPop(void *ctxt){ AutoreleasePoolPage::pop(ctxt);}
2.3.1、pop(对象出栈)static inline voidpop(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. //不存在热页面,将标记设置为nil return setHotPage(nil); } // Pool was used. Pop its contents normally. // Pool pages remain allocated for re-use as usual. //存在热页面,通过双向链表循环向上找到最冷页面 page = coldPage(); //将token设置为起始位置 token = page->begin(); } else { //获取token所在的页 page = pageForPointer(token); } //赋值给stop 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 //最冷页面的起始可能不是POOL_BOUNDARY: //1. 弹出顶级池,保留冷页面 //2. 对象在没有池的情况下被自动释放 } 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(token, page, stop);}
2.3.2、popPagestatic voidpopPage(void *token, AutoreleasePoolPage *page, id *stop){ if (allowDebug && PrintPoolHiwat) printHiwat(); //当前页中对象出栈,到stop位置停止 page->releaseUntil(stop); // memory: delete empty children 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 //特殊情况:删除所有的pop //销毁当前页面 page->kill(); //将热页面标记设置为nil 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(); } }}
2.3.3、releaseUntilvoid releaseUntil(id *stop) { // Not recursive: we don"t want to blow out the stack // if a thread accumulates a stupendous amount of garbage //向下遍历,到stop停止 while (this->next != stop) { // Restart from hotPage() every time, in case -release // autoreleased more objects //获取热页面 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();#if SUPPORT_AUTORELEASEPOOL_DEDUP_PTRS AutoreleasePoolEntry* entry = (AutoreleasePoolEntry*) --page->next; // create an obj with the zeroed out top byte and release that id obj = (id)entry->ptr; int count = (int)entry->count; // grab these before memset#else //内存平移,获取对象 id obj = *--page->next;#endif memset((void*)page->next, SCRIBBLE, sizeof(*page->next)); page->protect(); //当前对象不是哨兵对象 if (obj != POOL_BOUNDARY) {#if SUPPORT_AUTORELEASEPOOL_DEDUP_PTRS // release count+1 times since it is count of the additional // autoreleases beyond the first one for (int i = 0; i < count + 1; i++) { objc_release(obj); }#else //将其释放 objc_release(obj);#endif } } //将当前页面标记为热页面 setHotPage(this);#if DEBUG // we expect any children to be completely empty for (AutoreleasePoolPage *page = child; page; page = page->child) { ASSERT(page->empty()); }#endif}
2.3.4、killvoid 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) { //将子页面设置为nil page->unprotect(); page->child = nil; page->protect(); } //销毁子页面 delete deathptr; //遍历销毁到this为止 } while (deathptr != this);}
3、嵌套使用int main(int argc, const char * argv[]) { @autoreleasepool { NSObject *objc = [[[NSObject alloc] init] autorelease]; @autoreleasepool { NSObject *objc = [[[NSObject alloc] init] autorelease]; } _objc_autoreleasePoolPrint(); } return 0;}-------------------------objc[2511]: ##############objc[2511]: AUTORELEASE POOLS for thread 0x1000ebe00objc[2511]: 4 releases pending.objc[2511]: [0x10680d000] ................ PAGE (hot) (cold)objc[2511]: [0x10680d038] ################ POOL 0x10680d038objc[2511]: [0x10680d040] 0x101370c40 NSObjectobjc[2511]: [0x10680d048] ################ POOL 0x10680d048objc[2511]: [0x10680d050] 0x101365fb0 NSObjectobjc[2511]: ##############
线程的自动释放池是一个指针堆栈,当嵌套使用时,添加好各自堆栈的哨兵对象;出栈时,先释放内部,再释放外部
总结结构:
自动释放池的压栈和出栈,通过结构体的构造函数和析构函数触发
压栈:调用objc_autoreleasePoolPush
函数
出栈:调用objc_autoreleasePoolPop
函数
特点:
自动释放池是一个存储指针的栈结构
指针要么是一个要释放的对象,要么是POOL_BOUNDARY
自动释放池边界,俗称:哨兵对象
哨兵对象的作用:当自动释放池将对象进行pop
操作时,需要知道边界在哪里,否则会破坏别人的内存空间。而哨兵对象,就是边界标识
自动释放池的栈空间被分成一个 双链接结构的页面列表,可添加和删除页面
双向链表的特别,一个页中同时存在父节点和子节点。可向前找到父页面,也可向后找到子页面
线程本地存储指向热页,其中存储新自动释放的对象
栈原则,先进后出,可以理解为最后一个页面就是热页。里面的对象最后被push
,最先被pop
容量:
池页大小为4096字节
,每一页都包含56字节
的成员变量,但一个自动释放池中,只会压栈一个哨兵对象,占8字节
原理:
自动释放池的本质是__AtAutoreleasePool
结构体,包含构造函数和析构函数结构体声明,触发构造函数,调用objc_autoreleasePoolPush
函数,本质是对象压栈的push
方法当结构体出作用域空间,触发析构函数,调用objc_autoreleasePoolPop
函数,本质是对象出栈的pop
方法对象压栈如果存在page
,并且没有存满,调用add
函数使用*next++
进行内存平移将对象压栈
如果存在page
,但存储已满,调用autoreleaseFullPage
函数
遍历链表,找到最后一个空白的子页面
对其进行创建新页
设置为热页面
添加对象
否则,不存在page
,调用autoreleaseNoPage
函数通过父类AutoreleasePoolPageData
进行初始化
begin
:获取对象压栈的起始位置
objc_thread_self
:通过tls
获取当前线程
链接双向链表
设置为热页面
pushExtraBoundary
为YES
,哨兵对象压栈
对象压栈
对象出栈调用popPage
函数,传入stop
为哨兵对象的位置当前页中对象出栈,到stop
位置停止
调用kill
函数,销毁当前页面
嵌套使用:
线程的自动释放池是一个指针堆栈,当嵌套使用时,添加好各自堆栈的哨兵对象。出栈时,先释放内部,再释放外部
ARC
模式:
ARC
模式,使用alloc
、new
、copy
、mutableCopy
前缀开头的方法进行对象创建,不会加入到自动释放池;它们的空间开辟由开发者申请,释放也由开发者进行管理
与线程的关系:
每个线程(包括主线程)维护自己的对象堆栈。随着新池的创建,它们被添加到堆栈的顶部。当池被释放时,它们会从堆栈中移除
autoreleased
对象被放置在当前线程的顶部自动释放池中;当一个线程终止时,它会自动清空所有与其关联的自动释放池
与 Runloop的关系:
主程序在事件循环的每个循环开始时在主线程上创建一个自动释放池
并在结束时将其排空,从而释放在处理事件时生成的任何自动释放对象
使用:
for循环中大量创建对象时,使用 autorelease可以有效控制内存的快速增长(原因是释放没有创建快,如果不加 autorelease最终内存也会降下来,但可以 减少内存峰值)
for (int i = 0; i<100000000; i++) { @autoreleasepool { NSLog(@"%d",i); __autoreleasing LZPerson *p =[LZPerson new]; }}
到此这篇关于iOS底层探索之自动释放池的文章就介绍到这了,更多相关iOS自动释放池内容请搜索软件开发网以前的文章或继续浏览下面的相关文章希望大家以后多多支持软件开发网!