AutoreleasePoolPage的引入
什么都没写,仅仅一个hello,world和一个autoreleasePool
可以看到调用了PoolPush和PoolPop两个函数,对于看一下两个函数的实现
void *
objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
void
objc_autoreleasePoolPop(void *ctxt)
{
AutoreleasePoolPage::pop(ctxt);
}
可以看到都调用了AutoreleasePoolPage 函数 看一下AutoreleasePoolPage 这个函数的实现
AutoreleasePoolPage的实现
AutoreleasePoolPage的大小是4096字节
# 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;
static size_t const SIZE =
#if PROTECT_AUTORELEASEPOOL
PAGE_MAX_SIZE;
#else
PAGE_MAX_SIZE;
#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;
一个AutoreleasePoolPage占4096个字节,扣除56个字节存储上面的信息外,其余的都用来存储加入到page中的对象地址
- 因为存储了两个指针,parent和child,所以AutoreleasePoolPage的实现方式是双向链表
- 该结构体的第一个成员变量是magic,我们在isa中也学习过,isa中是分判对象是否未完成初始化,在这里也一样,用来检查这个节点是否已经被初始化了。
- thread保存了当前页所在的线程
- depth表示page的深度,首次为0,每个page的大小都是4096字节(16进制0x1000),每次初始化一个page,depth都加一
自动释放池中的栈
假如我们的一个AutoreleasePoolPage被初始化在内存的0x100816000~0x100817000中,它在内存中的结构如下 其中有 56 bit 用于存储 AutoreleasePoolPage 的成员变量,剩下的 0x100816038 ~ 0x100817000 都是用来存储加入到自动释放池中的对象。
next指向了下一个为空的内存地址。如果next指向的地址加入一个object,他就会移动到下一个为空的内存地址中 hiwat和depth并不影响自动释放池的实现,也不在关键方法的调用栈中。
POOL_SENTINEL(哨兵对象)
除了我们熟悉的上面的结构体成员变量,还有一个哨兵对象,哨兵对象到底是什么,为什么它在栈中?
POOL_SENTINEL 只是 nil 的别名
#define POOL_BOUNDARY nil (我们现在的哨兵对象叫POOL_SENTINEL,为了方便还是延续上面的叫法)
在每个自动释放池初始化调用objc_autoreleasePoolPush 的时候,都会把一个POOL_SENTINEL push到自动释放池的栈顶,并且返回这个哨兵对象
而当方法objc_autoreleasePoolPop调用时,就会想自动释放池中的对象发送release消息,直到第一个POOL_SENTINEL
说到这我们就要来看看objc_autoreleasePoolPush方法了
objc_autoreleasePoolPush方法
以及了解了哨兵对象,再回到最开始的起点objc_autoreleasePoolPush 方法
void *
objc_autoreleasePoolPush(void) {
return AutoreleasePoolPage::push();
}
我们进入push看一下push的实现
static inline void *push()
{
id *dest;
if (DebugPoolAllocation) {
dest = autoreleaseNewPage(POOL_BOUNDARY);
} else {
dest = autoreleaseFast(POOL_BOUNDARY);
}
assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
return dest;
}
一个简单的判断是否为调试模式,调试模式下新建一个链表节点,并将一个哨兵对象添加到链表栈中
正常情况下调用autoreleaseFast 方法,这个方法的调用仅仅只有一个参数,哨兵对象 也就是说push调用的时候autoreleaseFast参数只有哨兵对象
那么我们“将对象加入自动释放池”这一步操作怎么进行的呢? 其实就不是push这个方法了
我们可以写一下将一个对象加入自动释放池的操作
int main(int argc, const char * argv[]) {
id __strong obj0 = [[NSObject alloc] init];
id __autoreleasing obj2 = obj0;
return 0;
}
就是我们强行让对象入池 看一下此时的汇编 objc_retainAutorelease
中间寻找的过程就不展示了,最终就调用了下面的方法
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;
}
这个方法会帮我们穿进去一个参数,是对象
看一下Fast这个方法
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);
}
}
hotPage就是当前正在使用的AutoreleasePoolPage
- 有hotPage并且当前page不满
-
- 调用page->add(obj)方法将对象添加至AutoreleasePoolPage的栈中(这个obj就是哨兵)
- 有hotPage并且当前的page已满
-
- 调用
autoreleaseFullPage 初始化一个新的页 -
- 调用page->add(obj)方法将对象添加至AutoreleasePoolPage的栈中
- 无hotPage
-
- 调用
autoreleaseNoPage 新建一个hotPage,并将对象添加到新创建的page中
看一下对应的几个方法
page->add 添加对象
去掉不需要我们理解的部分:
id *add(id obj) {
id *ret = next;
*next++ = obj;
return ret;
}
这其实就是一个压栈操作,将对象加入AutoreleasePoolPage,然后移动栈顶指针
autoreleaseFullPage当前hotPage已满
static id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page) {
do {
if (page->child) page = page->child;
else page = new AutoreleasePoolPage(page);
} while (page->full());
setHotPage(page);
return page->add(obj);
}
它会从传入的page开始遍历整个双向链表,直到
- 查找到一个未满的AutoreleasePoolPage
- 使用构造器传入Parent创建一个新的AutoreleasePoolPage
在查找到一个可以使用的AutoreleasePoolPage之后,会将该页面标记为hotPage,然后调用page->add方法添加对象
autoreleaseNoPage无hotPage
static id *autoreleaseNoPage(id obj) {
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
setHotPage(page);
if (obj != POOL_SENTINEL) {
page->add(POOL_SENTINEL);
}
return page->add(obj);
}
既然当前内存中不存在AutoreleasePoolPage,就要从头开始构建这个自动释放池的双线链表,即新的AutoreleasePoolPage是没有parent指针的
初始化之后,将当前也标记为hotPage,然后先向这个page中添加一个哨兵对象,来确保在pop调用的时候不会发生异常,最后,将obj添加到自动释放池中(就是加到这个page中)。
objc_autoreleasePoolPop方法
void
objc_autoreleasePoolPop(void *ctxt) {
AutoreleasePoolPage::pop(ctxt);
}
看起来传入任何一个指针都是可以的,但是在整个工程并没有发现传入其他对象的例子。不过在这个方法中传入其他指针也是可行的,会将自动释放池释放到相应的位置
看一下pop的逻辑
static inline void pop(void *token) {
AutoreleasePoolPage *page = pageForPointer(token);
id *stop = (id *)token;
page->releaseUntil(stop);
if (page->child) {
if (page->lessThanHalfFull()) {
page->child->kill();
} else if (page->child->child) {
page->child->child->kill();
}
}
}
可以看到pop是一个静态方法,其总共做了三件事
- 使用
pageForPointer 获取当前token所在的AutoreleasePoolPage - 调用
releaseUntil 方法释放栈中的对象、直到stop,stop就是传递的参数,一般为哨兵对象 - 调用child的
kill 方法 -
- 系统根据当前页的不同状态kill掉不同child的页面
releaseUntil把page里的对象进行了释放,但是page本身也会占据很多空间,要通过kill()来处理
如果当前page小于一半满,则把当前页的所有孩子都杀掉,否则,留下一个孩子,从孙子开始杀。正是因为这一步,在autoreleaseFullPage()方法中才会有这两步:[为啥???]
if (page->lessThanHalfFull()) {
page->child->kill();
}
else if (page->child->child) {
page->child->child->kill();
}
kill掉child是为了节省内存; 但是当占用一半时,很快需要创建新的page,所以保留一个child,牺牲内存来提升性能。
token
自动释放池的实现: 官方注释
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. 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. 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.
- 一个线程的自动释放池是一个指针堆栈
- 每个指针要么指向被释放的对象,要么是作为自动释放池边界的POOL_BOUNDARY
- token是指向该pool的POOL_BOUNDARY的指针。
- 当池pop时,任何比哨兵更hotter的物体都会被释放?
- pool被分成一个双向指针构成的pages,pages在必要的时候被添加和删除
- 线程本地存储的指针指向hot page,其中存储新的自动释放对象
- token的本质就是指向哨兵对象的指针,存储着每次push时插入的POOL_BOUNDARY的地址
- 只有第一次push的时候会在page中插入一个POOL_BOUNDARY【或者page满了,或者没有hotPage需要使用新的page了】,并不是page的开头都一定是POOL_BOUNDARY
releaseUntil
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循环持续释放,一直到next指向stop【token】
问题
- 保存autoreleasePoolPage的双向链表只有一个么?也就是所有线程的autoreleasePoolPage都保存在一个链表中,还是每个线程保存一个自己的链表?并且链表头也就是链表的入口位置是保存在哪里呢?谁来控制呢?
一个线程有自己单独autoreleasePool链表,也有可能没有链表。链表的hotPage存储在TLS中,因为链表是双向的,通过hotpage就可以找到表头和表尾,不需要再单独存储表头
- autoreleasePoolPage与RunLoop的关系
RunLoop和AutoReleasePool是通过线程的方式一一对应的 对于每一个RunLoop,系统会隐式的创建一个Autorelease pool ,这样所有的release pool会构成会构成一个类似栈式结构,在每一个RunLoop结束时,当前栈顶的Autorelease Pool会被销毁,pool里的每个对象会被release。
|