关于自动释放池实现机制
当我们去使用自动释放池的时候一般都是这样使用的
int main(int argc, char * argv[]) {
@autoreleasepool {
}
}
我们使用clang-rewrite-objc 文件名 将其改变为C++代码,我们就会发现代码变成了这样
下面通过extern关键字引入了两个函数objc_autoreleasePoolPush和objc_autoreleasePoolPop,我们再往下看会看到其实@autoreleasepool最终转换成的是去声明了一个变量__AtAutoreleasePool __autoreleasepool,C++中声明一个变量就会自动的去调用其的默认构造方法,所以就会去调用atautoreleasepoolobj = objc_autoreleasePoolPush();这样的一个方法,当这个变量销毁的时候也就是超出下面的{ }作用域的时候就会去调用析构函数中的objc_autoreleasePoolPop(atautoreleasepoolobj);
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;
};
#define __OFFSETOFIVAR__(TYPE, MEMBER) ((long long) &((TYPE *)0)->MEMBER)
int main(int argc, char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
}
}
下面再简单的分析下关于objc_autoreleasePoolPush和objc_autoreleasePoolPop函数,这两个函数,我们可以在下载地址进行下载,然后点击里面的NSObjtec.mm文件中进行查看
关于objc_autoreleasePoolPush函数,我们会发现下面是直接调用的是AutoreleasePoolPage::push()
void * objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
在这里我们先去看看这个AutoreleasePoolPage的类结构
class AutoreleasePoolPage
{
// EMPTY_POOL_PLACEHOLDER is stored in TLS when exactly one pool is
// pushed and it has never contained any objects. This saves memory
// when the top level (i.e. libdispatch) pushes and pops pools but
// never uses them.
/*当一个pool被push的时候EMPTY_POOL_PLACEHOLDER会被存储在TLS中,它并不包含任何对象,当我们在使用libdispatch,也就是GCD的时候进行push和pop的时候回节约内存,这里本人理解因为在runloop中使用了大量GCD的知识,而且Runloop当中又和自动释放池有紧密的关系,所以可能是这个原因*/
# define EMPTY_POOL_PLACEHOLDER ((id*)1)
# define POOL_BOUNDARY nil
static pthread_key_t const key = AUTORELEASE_POOL_KEY;
static uint8_t const SCRIBBLE = 0xA3; // 0xA3A3A3A3 after releasing
static size_t const SIZE =
#if PROTECT_AUTORELEASEPOOL
PAGE_MAX_SIZE; // must be multiple of vm page size
#else
PAGE_MAX_SIZE; // size and alignment, power of 2
#endif
static size_t const COUNT = SIZE / sizeof(id);//存放对象数量
magic_t const magic;//用来校验完整性
id *next;//页中对象的下一位索引
pthread_t const thread;//所属线程
AutoreleasePoolPage * const parent;//父页
AutoreleasePoolPage *child;//子页
uint32_t const depth;//深度
uint32_t hiwat;//这个字段是high water的缩写,这个字段据说用来计算pool中最多存放的对象个数。在每次执行pop()的时候,会更新一下这个字段
}
根据上面的结构体我们可以知道其实autoreleasepool的结构就是是一个由 AutoreleasepoolPage双向链表的结构,其中 child 指向它的子 page,parent 指向它的父 page
紧接着我们再去查看下magic_t这个结构体,因为下面会有很多关于检验完整性的函数的调用
struct magic_t {
static const uint32_t M0 = 0xA1A1A1A1;
# define M1 "AUTORELEASE!"
static const size_t M1_len = 12;
uint32_t m[4];
magic_t() {
assert(M1_len == strlen(M1));//判断M1是否是12个字符长度
assert(M1_len == 3 * sizeof(m[1]));//判断M1_len是不是12
m[0] = M0;//将0xA1A1A1A1赋值给M0
//把M1字符串地址的12个字节复制到m1地址所指向的空间中
strncpy((char *)&m[1], M1, M1_len);
}
//析构函数
~magic_t() {
m[0] = m[1] = m[2] = m[3] = 0;
}
//检测函数
bool check() const {
return (m[0] == M0 && 0 == strncmp((char *)&m[1], M1, M1_len));
}
//快速检测函数
bool fastcheck() const {
#if CHECK_AUTORELEASEPOOL
return check();
#else
return (m[0] == M0);
#endif
}
# undef M1
};
这里再简单的介绍下CHECK_AUTORELEASEPOOL,关于这个宏,源文件的注解如下所示,其实意思如果我们把CHECK_AUTORELEASEPOOL设置为1就会去调用check()函数,否则就会调用fastcheck()函数
// Set this to 1 to validate the entire autorelease pool header all the time
// (i.e. use check() instead of fastcheck() everywhere)
我们可以再去查看AutoreleasePoolPage::push()函数,这里就是通过AutoreleasePoolPage类去调用push方法,因为这里push函数是静态内联函数,所以可以直接通过类进行调用,我们再去查看其内部的实现
static inline void *push()
{
id *dest;
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;
}
上面的DebugPoolAllocation就是判断是不是在Debug模式下,在 Debug 情况下每一个自动释放池都以一个新的poolPage开始会去调用autoreleaseNewPage方法,否则的话就去调用autoreleaseFast函数
我们下面去看看autoreleaseNewPage方法,就是去判断page存不存在,如果存在就去创建一个新的page,并将对象添加到新创建的 page 中,如果当前的page不存在时,即还没有page的时候,创建第一个page,并将对象添加到新创建的page中,添加的第一个对象是POOL_BOUNDARY也就是边界对象的意思,代表 AutoreleasePoolPage 中的第一个对象
static __attribute__((noinline))
id *autoreleaseNewPage(id obj)
{
AutoreleasePoolPage *page = hotPage();
if (page) return autoreleaseFullPage(obj, page);
else return autoreleaseNoPage(obj);
}
我们先看下hotPage方法做了什么
其实在下面就是去做了取出当前的page,如果取出的page是EMPTY_POOL_PLACEHOLDER,那就说明是空的池子,就说明还没有page,直接返回nil,EMPTY_POOL_PLACEHOLDER其实就是当一个释放池没有包含任何对象,又被推入栈中,就存储在TLS(Thread_local_storage)中,叫做空池占位符
static inline AutoreleasePoolPage *hotPage()
{
AutoreleasePoolPage *result = (AutoreleasePoolPage *)
tls_get_direct(key);
if ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil;
if (result) result->fastcheck();
return result;
}
我们会发现里面有一个tls_get_direct()函数,其实就是通过这个函数去取出当前的page的,其中tls的意思就是线程局部存储(Thread Local Storage)的缩写。
这个时候我们再去看看这个方法,对于这个函数目前理解有限,觉得应该就是利用pthread_getpecific和pthread_setspecific实现同一个线程中不同函数间共享数据。关于下面的pthread_getspecific函数原型如下所示
函数原型:void *pthread_getspecific(pthread_key_t key);
功能:使用pthread_getspecific获取调用线程的键绑定,并将该绑定存储在value指向的位置中
关于下面用到的key就是在AutoreleasePoolPage类当中是有的
static inline void *tls_get_direct(tls_key_t k)
{
assert(is_valid_direct_key(k));
if (_pthread_has_direct_tsd())
{
return _pthread_getspecific_direct(k);
}
else
{
return pthread_getspecific(k);
}
}
关于tls_set_direct这个方法也贴出来,其中关于下面的pthread_setspecific的原型是int pthread_setspecific(pthread_key_t key, const void *value);其中需要注意的就是value必须是动态内存分配,否则在其他函数使用getsetpecific时会出现错误
static inline void tls_set_direct(tls_key_t k, void *value)
{
assert(is_valid_direct_key(k));
if (_pthread_has_direct_tsd())
{
_pthread_setspecific_direct(k, value);
}
else
{
pthread_setspecific(k, value);
}
}
再来看看autoreleaseFast方法
static inline id *autoreleaseFast(id obj)
{
AutoreleasePoolPage *page = hotPage();
if (page && !page->full()) {
return page->add(obj);
} else if (page) {
return autoreleaseFullPage(obj, page);
} else {
return autoreleaseNoPage(obj);
}
}
这个方法就是当前 page 存在且没有满时,直接将对象添加到当前 page 中,也就是next 指向的位置;当前page存在并且已经满的时候,创建一个新的page ,并将对象添加到新创建的page中;如果在当前page不存在的时候,也就是说还没有page的时候,会去创建第一个 page,并将对象添加到新创建的page中
下面我们再来看下add方法
id *add(id obj)
{
assert(!full());
unprotect();//解除保护
id *ret = next; // faster than `return next-1` because of aliasing
*next++ = obj;//将obj插入到page中并且重新去设置next的指向
protect();//设置保护
return ret;
}
我们主要去看unprotect()方法和protect()方法
关于下面的方法,我们主要是去看mprotect,这个函数在Linux中,mprotect()函数可以用来修改一段指定内存区域的保护属性,函数原型int mprotect(const void *start, size_t len, int prot); 这个函数的区间开始的地址start必须是一个内存页的起始地址,并且区间长度len必须是页大小的整数倍。关于这一点在AutoreleasePoolPage类当中也有提及
关于PROTECT_AUTORELEASEPOOL这个宏其实在NSObject.mm这个文件中也是有所提及的
所以其实下面就是设置从this的内存地址开始的长度为SIZE的内存区域的保护属性修改为可读可写#if PROTECT_AUTORELEASEPOOL的意思就是如果PROTECT_AUTORELEASEPOOL为真就编译下面的代码段
inline void unprotect() {
#if PROTECT_AUTORELEASEPOOL
check();
mprotect(this, SIZE, PROT_READ | PROT_WRITE);
#endif
}
我们再来看看protect()函数,其实也差不多就是去设置这个内存段为只读
inline void protect() {
#if PROTECT_AUTORELEASEPOOL
mprotect(this, SIZE, PROT_READ);
check();
#endif
}
紧接着我们再去具体的看下autoreleaseFullPage方法
其实在这个函数中刚开始就是判断参数中的page是不是当前的page,不是就报错了,以及判断当前的page有没有满或者说是不是在Debug模式下,然后如果当前page是有子page了,那么就直接设置page为它的child page,如果child page不存在的话那么我们就去新建page,最后设置page为最新的page也就是page链表上最新的结点,最后把obj给添加进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.
//判断是不是最新的page
assert(page == hotPage());
//判断page是不是满了或者是不是Debug模式下
assert(page->full() || DebugPoolAllocation);
do {
//判断page->child是不是存在,如果存在的话就让page = page->child
if (page->child) page = page->child;
//否则就去创建
else page = new AutoreleasePoolPage(page);
} while (page->full());
setHotPage(page);
return page->add(obj);
}
我们再去看看最后的setHotPage函数,我们可以看到其实也就是去调用tls_set_direct函数,也就是去保存这个page
static inline void setHotPage(AutoreleasePoolPage *page)
{
if (page) page->fastcheck();
tls_set_direct(key, (void *)page);
}
当我们首次使用@autoreleasepool的时候就会去调用autoreleaseNoPage函数,关于这个函数如果当前没有AutoreleasePoolPage的对象就会先去调用这个方法,然后在Debug模式下就会直接去创建一个AutoreleasePoolPage对象,然后去插入POOL_BOUNDARY这个边界对象。
如果是在release模式下的话就会先去调用setEmptyPoolPlaceholder(),然后当里面放入变量的时候,变量后面都会加一个autorelease,也就是说会调用autorelease方法还会去调用autoreleaseFast这个方法然后判断当前还没有page就会去调用autoreleaseNoPage方法然后会去创建新的page
static __attribute__((noinline))
id *autoreleaseNoPage(id obj)
{
// "No page" could mean no pool has been pushed
// or an empty placeholder pool has been pushed and has no contents yet
assert(!hotPage());//判断hotPage()返回的page是否为空,如果为空的话才能继续下去
bool pushExtraBoundary = false;//是否要push边界对象
//如果有占位的EMPTY_POOL_PLACEHOLDER,就去设置需要去push一个边界对象
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;
}
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",
pthread_self(), (void*)obj, object_getClassName(obj));
objc_autoreleaseNoPool(obj);
return nil;
}
//如果不是debug模式以及obj是边界对象就去设置空的占位池子也就是 EMPTY_POOL_PLACEHOLDER
else if (obj == POOL_BOUNDARY && !DebugPoolAllocation) {
// 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();
}
// We are pushing an object or a non-placeholder'd pool.
// Install the first page. 创建第一个page
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
//设置最新的page
setHotPage(page);
// Push a boundary on behalf of the previously-placeholder'd pool.
if (pushExtraBoundary) {
/*插入一个边界对象会走这里的方法肯定是之前已经设置过 setEmptyPoolPlaceholder也就是说不是在Debug模式下,并且obj不是POOL_BOUNDARY*/
page->add(POOL_BOUNDARY);
}
// Push the requested object or pool. 添加一个对象
return page->add(obj);
}
我们都知道放在自动释放池里面的对象在ARC中后面都会被加上一个autorelease,关于这个方法的实现如下所示
static inline id autorelease(id obj)
{
//判断obj这个对象存不存在
assert(obj);
//判断TaggedPointer是不是为空
assert(!obj->isTaggedPointer());
//添加这个对象
id *dest __unused = autoreleaseFast(obj);
//判断需要满足dest为空或者dest == EMPTY_POOL_PLACEHOLDER或者*dest为obj其中的一个条件
assert(!dest || dest == EMPTY_POOL_PLACEHOLDER || *dest == obj);
//返回这个对象本身
return obj;
}
这里需要注意的就是isTaggedPointer这个方法,关于这个方法的实现,其实这里判断这个变量是不是只是表面是一个对象但其实就是一个普通变量,比如说NSNumber和NSDate,普通对象是不需要去free释放内存的,所以这里肯定是需要去做一个判断,Tagged Pointer是专门用来存储小的对象的。关于这个Tagged Pointer可以去百度
objc_object::isTaggedPointer()
{
#if SUPPORT_TAGGED_POINTERS
return ((uintptr_t)this &; TAG_MASK);
#else
return false;
#endif
}
最后我们看到pop方法
这里的pop函数里面的参数调用的就是我们调用objc_autoreleasePoolPush()的返回值,这个可以在一开始就看到,然后返回值要么是EMPTY_POOL_PLACEHOLDER要么是POOL_BOUNDARY,为什么呢?
因为如果在非Debug模式下我们是会去调用autoreleaseNoPage方法里面的setEmptyPoolPlaceholder()方法,所以返回值就是EMPTY_POOL_PLACEHOLDER
在debug模式下会去调用autoreleaseNewPage如果是第一次使用自动释放池的话会去调用autoreleaseNoPage方法,然后会返回POOL_BOUNDARY所在的位置
所以在push方法中会有这样的判断
static inline void pop(void *token)
{
AutoreleasePoolPage *page;
id *stop;
//判断token是否是EMPTY_POOL_PLACEHOLDER
if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
// Popping the top-level placeholder pool.
//调用hotPage()方法判断当前的页存不存在
if (hotPage()) {
// Pool was used. Pop its contents normally.
// Pool pages remain allocated for re-use as usual.
//先调用coldPage()寻找到最上面的祖宗结点,然后调用begin方法,返回的即可以用于存放对象地址的第一个位置,然后进行pop
pop(coldPage()->begin());
} else {
//如果hotPage()为nil之后,会去调用tls_set_direct(key, (void *)page);设置关联的数据为nil
// Pool was never used. Clear the placeholder.
setHotPage(nil);
}
//返回
return;
}
//根据token寻找到页所在的地址
page = pageForPointer(token);
//获取stop如果不是POOL_BOUNDARY的话这种情况下就是在MRC下我们开辟一个线程,在线程当中使用了autorelease的时候,创建page的时候add obj的对象就不是POOL_BOUNDARY
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
}
//没有满足上面的两个条件就会报错
else {
// Error. For bincompat purposes this is not
// fatal in executables built with old SDKs.
return badPop(token);
}
}
if (PrintPoolHiwat) printHiwat();
//然后去调用page的releaseUntil方法
page->releaseUntil(stop);
// memory: delete empty children
//在Debug模式下删除所有空的page
if (DebugPoolAllocation && page->empty()) {
// special case: delete everything during page-per-pool debugging
AutoreleasePoolPage *parent = page->parent;
page->kill();
setHotPage(parent);
}
//如果是在debug模式下没有主动声明自动释放池的,并且没有父页的情况下了,就去走下面的else if然后删除所有page,
else if (DebugMissingPools && page->empty() && !page->parent) {
// special case: delete everything for pop(top)
// when debugging missing autorelease pools
page->kill();
setHotPage(nil);
}
//如果page有child page的话
else if (page->child) {
// hysteresis: keep one empty child if page is more than half full
/*如果当前page小于一半满,则把当前页的所有孩子都删掉,否则,就留下一个孩子,从孙子开始杀。正是因为这一步,在autoreleaseFullPage()方法中才会有if(page->child) page = page->child;else page = new AutoreleasePoolPage(page);*/
if (page->lessThanHalfFull()) {
page->child->kill();
}
else if (page->child->child) {
page->child->child->kill();
}
}
}
关于coldPage()函数
下面其实就是去不断的去寻找parent,parent
static inline AutoreleasePoolPage *coldPage()
{
AutoreleasePoolPage *result = hotPage();
if (result) {
while (result->parent) {
result = result->parent;
result->fastcheck();
}
}
return result;
}
关于begin方法,返回的是可以用于存放对象地址的第一个位置
id * begin() {
return (id *) ((uint8_t *)this+sizeof(*this));
}
关于pageForPointer这个函数,就是通过通过token获取了一下当前的page
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();
return result;
}
这边有一个例子直接拿来借鉴一下的
例如: p = 0x100623bc2
offset = p % 4096 = 0xbc2
result = p - offset = 0x100623000
之后我们再去看看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
//如果next!=stop的话,就要去寻找父亲页,因为是会有当前的stop是父亲页的next的情况是在pop(coldPage()->begin());这个函数里面
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
//找到next==begin()就是page存放的第一个对象位置的时候解释循环
while (page->empty()) {
//寻找父亲页
page = page->parent;
setHotPage(page);
}
//设置这块内存可读写
page->unprotect();
id obj = *--page->next;
//这里就是把next指向的sizeof(*page->next)字节用SCRIBBLE = 0xA3来代替
memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
//设置为只读
page->protect();
//不是边界对象就进行释放
if (obj != POOL_BOUNDARY) {
objc_release(obj);
}
}
//设置回当前的最新页
setHotPage(this);
#if DEBUG
// we expect any children to be completely empty
for (AutoreleasePoolPage *page = child; page; page = page->child) {
assert(page->empty());
}
#endif
}
关于empty()方法的判断
bool empty() {
return next == begin();
}
关于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的parent给了page
page = page->parent;
if (page) {
page->unprotect();
//先把指针指向设置为nil
page->child = nil;
page->protect();
}
//然后删除
delete deathptr;
} while (deathptr != this);
}