汕头企业网站公司,网站建设摘要,wordpress英文改为中文,朝阳凌源网站建设来说一下最近在了解的autoreleasepool吧#xff0c;我们可能平时书写过许多脑残代码#xff0c;其有很多的缺陷但是我们可能当时学的比较浅就也不太了解#xff0c;就像下面这样的#xff1a;
for (int i 0; i 1000000; i) {NSNumber *num [NSNumber numberWithInt…来说一下最近在了解的autoreleasepool吧我们可能平时书写过许多脑残代码其有很多的缺陷但是我们可能当时学的比较浅就也不太了解就像下面这样的
for (int i 0; i 1000000; i) {NSNumber *num [NSNumber numberWithInt:i];
}一、autoreleasepool{}
我们平时创建一个main函数的代码的时候就会发现其中有一个这个东西autoreleasepool{}使用clang编译之后autoreleasepool{...}被编译成了{__AtAutoreleasePool __autoreleasepool; ... }。
这个__AtAutoreleasePool到底是什么 它其实是一个结构体在创建__AtAutoreleasePool结构体变量的时候调用了objc_autoreleasePoolPush(void)销毁的时候会调动objc_autoreleasePoolPop(void *)即其构造函数和析构函数所以我们可以看出其其实是一个C封装的自动释放池变量会将autoreleasepool{…}中{}中的内容添加到自动释放池中方便内存管理。 但是它在main这个函数中好像感觉并没有什么用因为程序结束了那么内存不也就被释放了那这里为什么要加autoreleasepool{} 技术上是可行的去掉main函数中的autoreleasepool{}并没有什么关系但是为了严谨为了使UIApplicationMin创建出来的自动释放对象有自动释放池可添加并能在自动释放池结束的时候释放对象而不是依赖操作系统的回收所以加上autoreleasepool{}可以把它理解为最外层才触发释放机制的自动释放池。 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;
};所以我们平时在ARC环境下写的autoreleasepool{}它其实就是自动释放池创建和释放的简单使用。 它会在{的时候创建自动释放池在}的时候销毁自动释放池并发出release通知让其中的变量自己进行release操作。
二、AutoreleasePoolPage
从上边的__AtAutoreleasePool我们可以看到这两种方法objc_autoreleasePoolPush和objc_autoreleasePoolPop但是这究竟是什么呢
void *objc_autoreleasePoolPush(void) {return AutoreleasePoolPage::push();
}void objc_autoreleasePoolPop(void *ctxt) {AutoreleasePoolPage::pop(ctxt);
}我们可以看出这里又引入了新的类AutoreleasePoolPage其相关源码如下
class AutoreleasePoolPage {magic_t const magic;//AutoreleasePoolPage 完整性校验id *next;//下一个存放autorelease对象的地址pthread_t const thread; //AutoreleasePoolPage 所在的线程AutoreleasePoolPage * const parent;//父节点AutoreleasePoolPage *child;//子节点uint32_t const depth;//深度,也可以理解为当前page在链表中的位置uint32_t hiwat;
}每一个自动释放池都是由一系列AutoreleasePoolPage组成的并且每一个AutoreleasePoolPage的大小都是4096字节16 进制 0x1000。
#define I386_PGBYTES 4096
#define PAGE_SIZE I386_PGBYTES所以我们从上述的源码可以看出自动释放池其实就是一个由AutoreleasePoolPage构成的双向链表其结构中的child和parent分别指向其前趋和后继。 单个AutoreleasePoolPage结构如下
其中有 56 bit 用于存储AutoreleasePoolPage的成员变量剩下的0x100816038 ~ 0x100817000都是用来存储加入到自动释放池中的对象。
该结构体的第一个成员变量是magic我们在isa中也学习过isa中是分判对象是否未完成初始化在这里也一样用来检查这个节点是否已经被初始化了。begin()和end()这两个类的实例方法帮助我们快速获取 0x100816038 ~ 0x100817000 这一范围的边界地址。next指向下一个为空的内存地址如果next指向的地址加入一个object它就会如下图所示移动到下一个为空的内存地址中就像栈顶指针一样。thread保存了当前页所在的线程。depth表示page的深度首次为0每个page的大小都是4096字节16进制0x1000每次初始化一个pagedepth都加一。POOL_SENTINEL就是哨兵对象它只是nil的别名用于分隔Autoreleasepool。POOL_BOUNDARY直译过来就是POOL的边界。它的作用是隔开page中的对象。因为并不是每次push与pop之间存进的对象都刚好占满一个page可能会不满可能会超过因此这个POOL_BOUNDARY帮助我们分隔每个autoreleasepool块之间的对象。也就是说这个page可能存储很多个autoreleasepool块的对象使用POOL_BOUNDARY来隔开每个autoreleasepool块的对象。
#define POOL_SENTINEL nil如果向上述刚初始化的page添加对象时就会添加在next的指向处next再向后移一位。
并且在每个自动释放池初始化调用objc_autoreleasePoolPush的时候都会把一个POOL_SENTINEL push到自动释放池的栈顶并且返回这个POOL_SENTINEL哨兵对象。
int main(int argc, const char * argv[]) {{//这里的 atautoreleasepoolobj 就是一个 POOL_SENTINELvoid * atautoreleasepoolobj objc_autoreleasePoolPush();// do whatever you wantobjc_autoreleasePoolPop(atautoreleasepoolobj);}return 0;
}上面的atautoreleasepoolobj就是一个POOL_SENTINEL。 而当方法objc_autoreleasePoolPop调用时就会向自动释放池中的对象发送release消息直到第一个 POOL_SENTINEL。 这也是autoreleasepool能准确释放其中对象的原因在该autoreleasepool push的时候会返回一个哨兵对象POOL_SENTINEL的地址并将它给pop那么这个pop就知道在执行pop释放的时候释放到这个哨兵对象POOL_SENTINEL处就可以停止了而这其中释放的内容就正好是自动释放池中的对象。
1.objc_autoreleasePoolPush方法
void *objc_autoreleasePoolPush(void) {return AutoreleasePoolPage::push();
}这里调用了AutoreleasePoolPage::push()方法
static inline void *push()
{id *dest;// POOL_BOUNDARY就是nil// 首先将一个哨兵对象插入到栈顶if (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;
}其中调用了autoreleaseFast方法hotPage指的是当前正在使用的AutoreleasePoolPage
static inline id *autoreleaseFast(id obj)
{AutoreleasePoolPage *page hotPage();if (page !page-full()) {//有 hotPage 并且当前 page 不满将object加入当前栈中return page-add(obj);} else if (page) {//有hotPage 但当前page已满找未满页或创建新页将object添加到新页中return autoreleaseFullPage(obj, page);} else {//无hotPage创建hotPage加入其中return autoreleaseNoPage(obj);}
}1.1 有hotPage并且当前page不满直接调用page-add(obj)将对象添加到自动释放池中。
// 这其实就是一个压栈操作将对象加入AutoreleasePoolPage然后移动栈顶指针
id *add(id obj) {id *ret next;*next obj;next;return ret;
}1.2 有hotPage但当前page已满找未满页或创建新页将object添加到新页中autoreleaseFullPage 当前page满的时候调用
static id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page) {
//一直遍历直到找到一个未满的 AutoreleasePoolPage如果找到最后还没找到就新建一个 AutoreleasePoolPagedo {if (page-child) page page-child;else page new AutoreleasePoolPage(page);} while (page-full());//将找到的或者构建的page作为hotPage然后将obj加入setHotPage(page);return page-add(obj);
}1.3 无hotPage创建hotPage加入其中 这个时候由于内存中没AutoreleasePoolPage就要从头开始构建这个自动释放池的双向链表那么当前页表作为第一张页表是没有parent指针的。并且我们在第一次创建page时其首位都是要加POOL_SENTINEL标识的方便让page知道在哪就结束了。
static id *autoreleaseNoPage(id obj) {AutoreleasePoolPage *page new AutoreleasePoolPage(nil); // 创建AutoreleasePoolPagesetHotPage(page); // 设置page为当前页if (obj ! POOL_SENTINEL) { // 加POOL_SENTINEL哨兵page-add(POOL_SENTINEL);}return page-add(obj); // 将obj加入
}autorelease方法
我们现在再想它dest autoreleaseFast(POOL_BOUNDARY);操作传递的是POOL_BOUNDARY变量并没有传对象那么到底是怎么存入进去的呢
通过调试查看汇编发现它其实调用的是objc_retainAutorelease方法之后层层调用发现调用的是autorelease方法
static inline id autorelease(id obj)
{printf(static inline id autorelease%p\n, obj);assert(obj);assert(!obj-isTaggedPointer());id *dest __unused autoreleaseFast(obj);assert(!dest || dest EMPTY_POOL_PLACEHOLDER || *dest obj);return obj;
}2.objc_autoreleasePoolPop方法
void objc_autoreleasePoolPop(void *ctxt) {AutoreleasePoolPage::pop(ctxt);
}我们一般都会在这个方法中传入一个哨兵对象POOL_SENTINEL方便释放如下图一样释放对象
其调用的pop方法如下
static inline void pop(void *token) {AutoreleasePoolPage *page pageForPointer(token);//使用 pageForPointer 获取当前 token 所在的 AutoreleasePoolPageid *stop (id *)token;page-releaseUntil(stop);//调用 releaseUntil 方法释放栈中的对象直到 stop 位置stop就是传递的参数一般为哨兵对象//调用 child 的 kill 方法系统根据当前页的不同状态kill掉不同child的页面//releaseUntil把page里的对象进行了释放但是page本身也会占据很多空间所以要通过kill()来处理释放空间if (page-child) {if (page-lessThanHalfFull()) { // 当前page小于一半满page-child-kill(); // 把当前页的孩子杀掉} else if (page-child-child) { // 否则留下一个孩子从孙子开始杀page-child-child-kill();}}
}Apple假设当前page一半都没满说明剩余的page空间已经暂时够了把多余的child page就可以全kill掉释放空间如果超过一半就认为下一页page还有存在的必要说不定添加的对象太多就能用的到所以kill掉孙子page有个儿子page就暂时够了。
token
oken是指向该pool的POOL_BOUNDARY指针token的本质就是指向哨兵对象的指针存储着每次push时插入的POOL_BOUNDARY的地址只有第一次push的时候会在page中插入一个POOL_BOUNDARY【或者page满了或者没有hotPage需要使用新的page了】并不是page的开头都一定是POOL_BOUNDARY
2.1 pageForPointer 获取 AutoreleasePoolPage
pageForPointer方法主要是通过内存地址的操作获取当前指针所在页的首地址
static AutoreleasePoolPage *pageForPointer(const void *p) {return pageForPointer((uintptr_t)p);
}static AutoreleasePoolPage *pageForPointer(uintptr_t p) {AutoreleasePoolPage *result;uintptr_t offset p % SIZE;assert(offset sizeof(AutoreleasePoolPage));result (AutoreleasePoolPage *)(p - offset);result-fastcheck(); // 检查当前的result是不是一个AutoreleasePoolPagereturn result;
}将指针与页面的大小也就是 4096 取模得到当前指针的偏移量因为所有的AutoreleasePoolPage在内存中都是对齐的
p 0x100816048
p % SIZE 0x48
result 0x100816000而最后调用的方法fastCheck()用来检查当前的result是不是一个AutoreleasePoolPage。 通过检查magic_t结构体中的某个成员是否为0xA1A1A1A1。 2.2 releaseUntil 释放对象
releaseUntil方法的实现如下
void releaseUntil(id *stop) {while (this-next ! stop) { // 不等于stop就继续popAutoreleasePoolPage *page hotPage(); // 获取当前页while (page-empty()) { // 当前页为空就找其父页并将其设置为当前页page page-parent;setHotPage(page);}page-unprotect();id obj *--page-next;memset((void*)page-next, SCRIBBLE, sizeof(*page-next)); // 将内存内容标记为SCRIBBLEpage-protect();if (obj ! POOL_SENTINEL) { // 该对象不为标识POOL_SENTINEL就释放对象objc_release(obj);}}setHotPage(this);
}它的实现还是很容易的用一个while循环持续释放 AutoreleasePoolPage中的内容直到next指向了stop。
使用memset将内存的内容设置成SCRIBBLE然后使用 objc_release释放对象。
2.3 kill()方法 到这里没有分析的方法就只剩下kill了而它会将当前页面以及子页面全部删除
void kill() {AutoreleasePoolPage *page this; // 获取当前页while (page-child) page page-child; // child存在就一直往下找直到找到一个不存在的AutoreleasePoolPage *deathptr;do {deathptr page;page page-parent;if (page) {page-unprotect();page-child nil; // 将其child指向置nil防止出现悬垂指针page-protect();}delete deathptr; // 删除} while (deathptr ! this); // 直到this处停止
}3.autorelease 方法
我们已经对自动释放池生命周期有一个比较好的了解最后需要了解的话题就是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方法的调用栈中最终都会调用上面提到的autoreleaseFast方法将当前对象加到AutoreleasePoolPage中。 这一小节中这些方法的实现都非常容易只是进行了一些参数上的检查最终还要调用autoreleaseFast方法
inline id objc_object::rootAutorelease() {if (isTaggedPointer()) return (id)this;if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;return rootAutorelease2();
}__attribute__((noinline,used)) id objc_object::rootAutorelease2() {return AutoreleasePoolPage::autorelease((id)this);
}static inline id autorelease(id obj) {id *dest __unused autoreleaseFast(obj);return obj;
}三、开头问题的解答
看到这里你是否明白了开篇所说的问题所在当一次运行循环结束之前也就是autoreleasepool释放autorelease对象之前autoreleasepool的内存一直在增加APP会出现内存峰值卡顿甚至会被系统强制关闭造成crash。
所以加上autoreleasepool保证每次循环生成的autorelease对象及时的释放才能避免上述问题
for (int i 0; i 1000000; i) {autoreleasepool {NSNumber *num [NSNumber numberWithInt:i];NSLog(%, num);}
}另外autoreleasepool还有延迟释放使对象超出函数作用域存在等用处。
四、总结
总的来说autoreleasepool就是一个双向链表链表中的每个节点是一个栈栈中保存了指向autoreleasepool的指针并且其中加入了需要自动释放池管理的对象所以在autoreleasepool中的所有对象引用计数都会1一旦出了autoreleasepool没有指针指向对象对象的引用计数就会-1ARC下xcode会为代码自动添加 autoreleasepool。
自动释放池是由AutoreleasePoolPage以双向链表的方式实现的当对象调用autorelease方法时会将对象加入AutoreleasePoolPage的栈中调用AutoreleasePoolPage::pop方法会向栈中的对象发送release消息
关于哨兵对象POOL_BOUNDARY和next指针
next指针只有一个永远指向下一个能存放autoreleasepool的地址而哨兵对象可以有很多个每个autoreleasePool都对应一个哨兵对象标示这个autoreleasePool对象从哪里开始存。
next和child next指向下一个能存放object对象的地址child是autoreleasePoolPage的参数指向下一个page。
autoreleasePoolPage与RunLoop的关系 RunLoop和AutoReleasePool是通过线程的方式一一对应的 在非手动添加Autorelease pool下Autorelease对象是在当前runloop进入休眠等待前被释放的 当一个runloop在不停的循环工作那么runloop每一次循环必定会经过BeforeWaiting(准备进入休眠)而去BeforeWaiting(准备进入休眠) 时会调用_objc_autoreleasePoolPop()和 _objc_autoreleasePoolPush()释放旧的池并创建新池那么这两个方法来销毁要释放的对象所以我们根本不需要担心Autorelease的内存管理问题。
RunLoop创建和释放自动释放池的时机 在进入RunLoop时创建一个AutoReleasePool。 在准备休眠的时候释放旧的AutoReleasePool再新建一个AutoReleasePool。 在RunLoop退出时释放AutoReleasePool。 保存autoreleasePoolPage的双向链表只有一个么也就是所有线程的autoreleasePoolPage都保存在一个链表中还是每个线程保存一个自己的链表并且链表头也就是链表的入口位置是保存在哪里呢谁来控制呢
一个线程有自己单独autoreleasePool链表也有可能没有链表。链表的hotPage存储在TLS中因为链表是双向的通过hotpage就可以找到表头和表尾不需要再单独存储表头。
需要自己手动添加autoreleasepool的情况
编写的不是基于UI框架的程序例如命令行工具通过循环方式创建大量临时对象使用非Cocoa程序创建的子线程