ARC下,我们使用@autoreleasepool{}来使用一个AutoreleasePool
void main(int argc, char * argv[]) {
@autoreleasepool {
}
}
随后编译器将其改写成下面的样子(clang -rewrite-objc main.m):
报错 main.m:9:9: fatal error: 'UIKit/UIKit.h' file not found,
#import
尴尬!换个命令 xcrun -sdk iphonesimulator clang -rewrite-objc main.m(指定模拟器/SDK)
如果报错 xcodebuild[3632:229732] [MT] PluginLoading: Required plug-in compatibility UUID E0A62D1F-3C18-4D74-BFE5-A4167D643966 for plug-in at path '~/Library/Application Support/Developer/Shared/Xcode/Plug-ins/CocoaPods.xcplugin' not present in DVTPlugInCompatibilityUUIDs
则Finder 前往这个地址,显示包内存在Info.plist文件的DVTPlugInCompatibilityUUIDs数组中添加一item,值为E0A62D1F-3C18-4D74-BFE5-A4167D643966
~>
int main(int argc, char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
}
}
我们写的@autoreleasepool{}变成了 @autoreleasepool 与 {}两部分,第一部分@autorelease被注释,很明显它的作用对于编译器只是一个标示。第二部分{}
__AtAutoreleasePool 是一结构体
struct __AtAutoreleasePool {
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
void * atautoreleasepoolobj;
};
__AtAutoreleasePool __autoreleasepool; 这一句声明,多少有点C++11 中的trivial的身影(可是看到了结构体中的构造和析构函数,一脸懵逼)
构造函数与类名相同,没有返回值。析构函数与类名相同,前面加上~,没有返回值和参数,对象销毁时自动调用。
未定义构造函数,那么编译器会添加有一个无参的构造函数,而如果定义了构造函数,不会默认生成,如果你还想允许无参构造,就必须显式的声明一个
尽管有点诡异,但是构造函数和析构函数还在那摆着呢。以上代码等价于
void main(int argc, const char * argv[]) {
{
void * autoreleasepoolobj = objc_autoreleasePoolPush();
objc_autoreleasePoolPop(autoreleasepoolobj);
}
结构体会在初始化时调用 objc_autoreleasePoolPush() 方法,会在析构时(出作用域)调用 objc_autoreleasePoolPop 方法
@autoreleasepool 只是帮助我们少写了这两行代码而已,让代码看起来更美观。我们下面就要依据这两个方法,展开
void *objc_autoreleasePoolPush(void) {
return AutoreleasePoolPage::push();
}
void objc_autoreleasePoolPop(void *ctxt) {
AutoreleasePoolPage::pop(ctxt);
}
AutoreleasePoolPage 的静态方法push 和 pop,两个函数都是对AutoreleasePoolPage的简单封装
AutoreleasePoolPage is who?
AutoreleasePoolPage是一个C++实现的类
class AutoreleasePoolPage {
magic_t const magic;
id *next;
pthread_t const thread;
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t hiwat;
};
1 magic 用于对当前 AutoreleasePoolPage 完整性的校验
2 AutoreleasePool是按线程一一对应的(结构中的thread指针指向当前线程)
3 AutoreleasePool(自动释放池)并没有单独的结构,而是由若干个AutoreleasePoolPage组合。每一个 AutoreleasePoolPage 的大小都是4096 字节, 前56 bit 用于存储 AutoreleasePoolPage 的成员变量,剩下的用来存储加入到自动释放池中的对象(autorelease对象的地址)
4 上面的next指针作为游标,指向栈顶最新add进来的autorelease对象的下一个位置
next 指向了下一个为空的内存地址,如果 next 指向的地址加入一个 object,它就会如下图所示移动到下一个为空的内存地址中
5 一个AutoreleasePoolPage的空间被占满时,会新建一个AutoreleasePoolPage对象,连接链表,后来的autorelease对象在新的page加入
6 AutoreleasePoolPage 是双向链表的形式连接起来的(parent 和 child 用来构造双向链表的指针)
7 begin() 和 end() 这两个类的实例方法获取一个AutoreleasePoolPage的存储autorelease对象地址的区域边界
到了这里,你可能想要知道 POOL_SENTINEL 到底是什么,还有它为什么在栈中?
#define POOL_SENTINEL nil
POOL_SENTINEL 只是 nil 的别名,是一个哨兵对象。
static inline void *push() {
return autoreleaseFast(POOL_SENTINEL);
}
static inline id *autoreleaseFast(id obj)
{
AutoreleasePoolPage *page = hotPage();
//有 hotPage 并且 hotPage 不满
if (page && !page->full()) {
//调用 page->add(obj) 方法将对象添加至 AutoreleasePoolPage 的栈中
return page->add(obj);
} else if (page) {//有 hotPage 并且hotPage 已满
return autoreleaseFullPage(obj, page);
} else {
//无 hotPage
return autoreleaseNoPage(obj);
}
}
1 objc_autoreleasePoolPush -> AutoreleasePoolPage::push --> autoreleaseFast(POOL_SENTINEL)
因此,调用objc_autoreleasePoolPush,最终调用autoreleaseFast 方法,并传入哨兵对象 POOL_SENTINEL
2 该函数分三种情况选择不同的代码执行(hotPage 可以理解为当前正在使用的 AutoreleasePoolPage)
将哨兵对象添加到hotPage中(page->add 添加对象),对函数整理简化
id *add(id obj) {
id *ret = next;
*next = obj;
next++;
return ret;
}
这个函数其实就是一个压栈的操作,将对象加入 AutoreleasePoolPage 然后移动栈顶的指针。
autoreleaseFast(POOL_SENTINEL),函数返回的是哨兵对象的地址
然后,顺便看下autoreleaseFullPage()与autoreleaseNoPage()函数(page->child指向下一页page)
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 方法添加对象
如果不存在 hotPage,就会调用 autoreleaseNoPage 方法初始化一个 AutoreleasePoolPage,从头开始构建双向链表,也就是说,新的 AutoreleasePoolPage 是没有 parent 指针的
static id *autoreleaseNoPage(id obj) {
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
setHotPage(page);
if (obj != POOL_SENTINEL) {
//可见成员变量之上必然是哨兵对象,无论传入与否。来确保在 pop 调用的时候,不会出现异常
page->add(POOL_SENTINEL);
}
return page->add(obj);
}
综上,autoreleaseFast()将哨兵对象添加到hotpage中,并返回哨兵对象的地址
3 objc_autoreleasePoolPush之后,autorelease对象必然执行类似操作进栈,纷纷添加到page中,直到objc_autoreleasePoolPop()(参数对应push返回值,哨兵对象的地址)
static inline void pop(void *token) {
//获取哨兵对象所在page(首地址)
AutoreleasePoolPage *page = pageForPointer(token);
id *stop = (id *)token;
//释放本次push进的对象
page->releaseUntil(stop);
//根据当前页的不同状态 kill 掉不同 child 的页面
if (page->child) {
if (page->lessThanHalfFull()) {
page->child->kill();
} else if (page->child->child) {
page->child->child->kill();
}
}
}
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();
return result;
}
将指针地址与页面的大小(如4096)取模,得到指针在page中的偏移量(因为所有的 AutoreleasePoolPage 在内存中都是对齐的)减去偏移量得到page的首地址,最后调用fastCheck() 用来检查此地址是不是一个 AutoreleasePoolPage(通过检查成员magic)
回到 pop函数,然后 page->releaseUntil(stop); 哨兵对象所在页page调用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);
}
这里,需要明白一点:
可能此次push进page的对象(指针)比较多,哨兵对象所在的page已不是hotpage
但是,由autoreleaseFullPage()可以看出,hotpage必然是哨兵对象所在的page的子节点(子子节点~)
找到当前hotpage,如果已空,通过parent指针遍历双向链表,找到不空的page作为hotpage(这个就是对应上面的情况,将hotpage清空,然后找到父节点继续,直到来到哨兵对象地址)
通过page的next指针, memset将内存的内容设置成 SCRIBBLE,然后使用 objc_release 释放对象,next指针必然前移。继续循环释放对象
每次进栈,add位置依据next指针指向,之后next指向后移。进栈依据next指针,出栈也是依据next指针。出栈之后,之后next指向前移。push可能用到多页page,释放必然也可能释放多页page,因此push进栈涉及到hotpage的变换,pop出栈也涉及到hotpage的切换。
这是一个不断出栈的过程,只要哨兵对象所在页page的next指针不指向此哨兵对象,循环继续
以上,就是源码分析。我们应该知道:
1 如果存在多个自动释放池,那么它们在page之间的界限就是哨兵对象
2 自动释放池的嵌套
@autoreleasepool {//pool 1
obj1 = xxx autorelease;
@autoreleasepool {//pool 2
__autoreleasing obj2 = xxx;
@autoreleasepool {//pool 3
obj3 = alloc;
}
}
}
它们在page中的位置,如下(假设同一个page)
next指针指向
pool 3哨兵对象 nil
obj2地址
pool 2哨兵对象 nil
obj1地址
pool 1哨兵对象 nil
加入释放池,也是依据代码顺序,加入到对应的哨兵对象之后。释放池废弃时,也是先依据代码顺序,先释放最内测的释放池。当然这仅仅是以一个page为例
我们之前说过,RunLoop每次循环会创建自动释放池,这个自动释放必然是很外侧的自动释放池,而我们手动创建的相对于该自动释放池是相对内侧的释放池,该自动释放池释放时,内侧的自动释放池s必然一同释放(循环)