经历过MRC时代的开发者,肯定都用过autorelease
方法,用于把对象交给AutoreleasePool
管理,在合适的时候,自动释放对象。其实所谓的自动释放对象,就是对所管理的对象调用release
方法。要想知道autorelease
方法的原理,首先就需要弄清楚AutoreleasePool
是个什么东东。
下面来看一个段MRC环境下的代码,为什么要在MRC下讨论这个问题呢?因为ARC会为我们在合适的地方自动加上autorelease
代码,并且不允许我们手动调用该方法了,为了方便研究autorelease
原理,我们还是得回到MRC。
****************** main.m *****************
#import <Foundation/Foundation.h>
#import "CLPerson.h"
int main(int argc, const char * argv[]) {
NSLog(@"pool--start");
@autoreleasepool {
CLPerson *p = [[[CLPerson alloc] init] autorelease];
}
NSLog(@"pool--end");
return 0;
}
************** CLPerson.m **************
#import "CLPerson.h"
@implementation CLPerson
- (void)dealloc
{
NSLog(@"%s", __func__);
[super dealloc];
}
@end
****************** 打印结果 *******************
2019-08-27 16:37:15.141523+0800 Interview16-autorelease[11602:772121] pool--start
2019-08-27 16:37:15.141763+0800 Interview16-autorelease[11602:772121] -[CLPerson dealloc]
2019-08-27 16:37:15.141775+0800 Interview16-autorelease[11602:772121] pool--end
概括一下看到的表面现象:CLPerson
实例对象p
是在@autoreleasepool {}
大括号结束的时候被释放的。
那么@autoreleasepool {}
到底做了什么呢?我们在命令行窗口里对main.m
文件执行如下命令
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main.cpp
在生成的中间代码main.cpp
中,找到main
函数的底层实现如下
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
MJPerson *person = ((MJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((MJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((MJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("MJPerson"), sel_registerName("alloc")), sel_registerName("init")), sel_registerName("autorelease"));
}
return 0;
}
其实如果你熟悉消息机制,上述的代码可以转化成如下形式
int main(int argc, const char * argv[]) {
/* @autoreleasepool */ {
__AtAutoreleasePool __autoreleasepool;
CLPerson *p = [[[CLPerson alloc] init] autorelease];
}
return 0;
}
我们观察可发现@autoreleasepool {}
经过编译之后发生了如下转变
这里多了个__AtAutoreleasePool
,它其实是个c++的结构体,可以在main.cpp
里搜索到它的定义如下
struct __AtAutoreleasePool {
//构造函数-->可以类比成OC的init方法,在创建时调用
__AtAutoreleasePool()
{
atautoreleasepoolobj = objc_autoreleasePoolPush();
}
//析构函数-->可以类比成OC的dealloc方法,在销毁时调用
~__AtAutoreleasePool()
{
objc_autoreleasePoolPop(atautoreleasepoolobj);
}
void * atautoreleasepoolobj;
};
如果你还不了解C++语法也无妨,它跟OC的类相似,可以有函数(方法),上面的这个结构体__AtAutoreleasePool
里面有已经有两个函数,
- 一个构造函数
__AtAutoreleasePool()
-->atautoreleasepoolobj = objc_autoreleasePoolPush();
,结构体被创建时调用,用于结构体的初始化 - 一个析构函数
~__AtAutoreleasePool()
-->objc_autoreleasePoolPop(atautoreleasepoolobj);
,结构体被销毁时调用
再回到我们的main
函数,其实它本质上就是下面这个形式
@autoreleasepool {}
的情况,那么如果有多层@autoreleasepool {}
嵌套在一起,就可以按照同样的规则来拆解objc_autoreleasePoolPush() & objc_autoreleasePoolPop()
接下来我们就来探究一下这两个函数的实现逻辑。在objc4源码的NSObject.mm
文件里可以找到它们的实现
*************** NSObject.mm (objc4) ******************
void *
objc_autoreleasePoolPush(void)
{
return AutoreleasePoolPage::push();
}
void
objc_autoreleasePoolPop(void *ctxt)
{
AutoreleasePoolPage::pop(ctxt);
可以看到,它们分别调用了C++类 AutoreleasePoolPage
的push()
和pop()
函数。要想继续深入后续函数的实现逻辑,我们需要先来看一看这个AutoreleasePoolPage
的内部结构,它的内容不少,有大量函数,但是我们首先需要理清楚它的成员变量,这些是可变化的,可操控的,所以去掉函数和一些静态常量,可以将AutoreleasePoolPage
结构简化如下
class AutoreleasePoolPage
{
magic_t const magic;
id *next;
pthread_t const thread;
AutoreleasePoolPage * const parent;
AutoreleasePoolPage *child;
uint32_t const depth;
uint32_t hiwat;
}
根据其命名,中文释义成自动释放池页,有个页的概念。我们知道自动释放池,是用来存放对象的,这个“页”就说明释放池的结构体应该有页面篇幅限制(内存空间大小)。具体多大呢?来看一下AutoreleasePoolPage
的两个函数
id * begin() {
return (id *) ((uint8_t *)this+sizeof(*this));
}
id * end() {
return (id *) ((uint8_t *)this+SIZE);
}
begin()
函数返回一个指针,指向自身最后一个成员变量之后的内存地址(相当于越过了自身所占用的内存空间)
end()
里面有一个SIZE
,我们看看它的定义
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
********************************************
#define PAGE_MAX_SIZE PAGE_SIZE
********************************************
#define PAGE_SIZE I386_PGBYTES
********************************************
#define I386_PGBYTES 4096 /* bytes per 80386 page */
可以看到,SIZE
实际上是4096
。这就是说end()
函数,得到的是一个指针,指向AutoreleasePoolPage
对象地址之后的第4096
个字节的内存地址。
通过以上掌握的信息,我们先抛出结论,然后再继续通过源码加深理解。
每个
AutoreleasePoolPage
对象占4096
个字节,其中成员变量共占用8字节
*7
=56个字节
。剩余的4040
个字节的空间就是用来存储自动释放对象的。因为一个
AutoreleasePoolPage
对象的内存是有限的,程序里面可能有很多对象会被加入自动释放池,因此可能会出现多个AutoreleasePoolPage
对象来共同存放自动释放对象。所有的AutoreleasePoolPage
对象是以双向链表的形式(数据结构)连接在一起的。
AutoreleasePoolPage
对象的各成员变量含义如下
magic_t const magic;
id *next;
指向AutoreleasePoolPage
内下一个可以用来存放自动释放对象的内存地址pthread_t const thread;
自动释放池所属的线程,说明它不能跟多个线程关联。AutoreleasePoolPage * const parent;
指向上一页释放池的指针AutoreleasePoolPage *child;
指向下一页释放池的指针uint32_t const depth;
uint32_t hiwat;
【第一次AutoreleasePoolPage::push();】
接下来,我们就正式开始研究AutoreleasePoolPage::push();
。假设我们现在是处在项目的main函数的第一个@autoreleasepool {}
开始的地方,也就是整个程序将会第一次去调用push()
函数:
# define POOL_BOUNDARY nil
static inline void *push()
{
id *dest;
if (DebugPoolAllocation) {//Debug模式下,每个autorelease pool都会创建新页
dest = autoreleaseNewPage(POOL_BOUNDARY);
} else {//标准情况下,调用autoreleaseFast()函数
dest = autoreleaseFast(POOL_BOUNDARY);
}
assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
return dest;
}
其中POOL_BOUNDARY
就是nil
的宏定义,忽略Debug模式,我们只看正常模式,那么push()
将会调用autoreleaseFast(POOL_BOUNDARY)
得到一个id *dest
并将其返回给上层函数。查看一下这个autoreleaseFast()
,看看它到底能给我们返回什么
static inline id *autoreleaseFast(id obj)
{
//拿到当前可用的AutoreleasePoolPage对象page
AutoreleasePoolPage *page = hotPage();
//(1)如果page存在&&page未满,则直接增加obj
if (page && !page->full()) {
return page->add(obj);
} else if (page) {//(2)如果满了,则调用autoreleaseFullPage(obj, page);
return autoreleaseFullPage(obj, page);
} else {//(3)如果没有页面,则调用autoreleaseNoPage(obj);
return autoreleaseNoPage(obj);
}
}
因为是整个程序第一次push操作,因此page对象还不存在,所以会按照情况(3)走,也就是autoreleaseNoPage(obj);
,实现如下
static __attribute__((noinline))
id *autoreleaseNoPage(id obj)
{
/*--"No page"
1.可以表示当前还没有任何pool被创建(pushed)
2.也可以表示已经创建了一个empty placeholder pool(空释放池占位符),只是还没添加任何内容
*/
assert(!hotPage());
//标签-->是否需要增加额外的POOL_BOUNDARY
bool pushExtraBoundary = false;
if (haveEmptyPoolPlaceholder()) {
/*
如果存在EmptyPoolPlaceholder(空占位符pool),就修改标签为true,
后面就需要依据此标签增加额外的POOL_BOUNDARY
*/
pushExtraBoundary = true;
}
/*
如果传入的obj不等于POOL_BOUNDARY(nil)并且找不到当前pool(丢失了),返回nil
*/
else if (obj != POOL_BOUNDARY && DebugMissingPools) {
_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;
}
/*
♥️♥️♥️♥️如果传入的是POOL_BOUNDARY,并且不在Debug模式,
会调用setEmptyPoolPlaceholder()设置一个EmptyPoolPlaceholder
*/
else if (obj == POOL_BOUNDARY && !DebugPoolAllocation) {
return setEmptyPoolPlaceholder();
}
// 初始化第一个AutoreleasePoolPage
AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
//将其设置成当前页(hot)
setHotPage(page);
// 根据pushExtraBoundary标签决定是否多入栈一个POOL_BOUNDARY
if (pushExtraBoundary) {
page->add(POOL_BOUNDARY);
}
// 将传入的obj入栈,通过 add()函数
return page->add(obj);
}
因为此时还没有创建过AutoreleasePoolPage
,并且也没有设置过EmptyPoolPlaceholder
,因此程序会命中代码中♥️♥️♥️♥️标记出的代码,调用setEmptyPoolPlaceholder();
,该函数实现如下
# define EMPTY_POOL_PLACEHOLDER ((id*)1)
static pthread_key_t const key = AUTORELEASE_POOL_KEY;
********************************************
static inline id* setEmptyPoolPlaceholder()
{
assert(tls_get_direct(key) == nil);
tls_set_direct(key, (void *)EMPTY_POOL_PLACEHOLDER);
return EMPTY_POOL_PLACEHOLDER;
}
可以看到实际上就是将key
与(id*)1
绑定起来,这个key
是一个静态常量,最后将这个(id*)1
作为一个空释放池池占位符返回,这样整个程序的第一个push()
函数结束,结果是生成了一个EMPTY_POOL_PLACEHOLDER (也就是(id*)1)
作为释放池占位符。
【第一次调用autorelease】
接着上面的过程,我们在push()
后,第一次对某个对象执行autorelease
方法时,看一下autorelease
的内部做了什么,先找到其源码如下
- (id)autorelease {
return ((id)self)->rootAutorelease();//🈯️从这里往下走
}
************************************************
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()
{
assert(!isTaggedPointer());
return AutoreleasePoolPage::autorelease((id)this);//🈯️从这里往下走
}
************************************************
static inline id autorelease(id obj)
{
assert(obj);
assert(!obj->isTaggedPointer());
id *dest __unused = autoreleaseFast(obj);//⚠️最终走到了这个方法
assert(!dest || dest == EMPTY_POOL_PLACEHOLDER || *dest == obj);
return obj;
}
通过逐层递进,我们看到autorelease
方法最终又来到了autoreleaseFast()
函数
static inline id *autoreleaseFast(id obj)
{
//拿到当前可用的AutoreleasePoolPage对象page
AutoreleasePoolPage *page = hotPage();
//(1)如果page存在&&page未满,则直接增加obj
if (page && !page->full()) {
return page->add(obj);
} else if (page) {//(2)如果满了,则调用autoreleaseFullPage(obj, page);
return autoreleaseFullPage(obj, page);
} else {//(3)如果没有页面,则调用autoreleaseNoPage(obj);
return autoreleaseNoPage(obj);
}
}
那么这一次,我们看看第一句代码里面hotPage();
得到的是什么
static inline AutoreleasePoolPage *hotPage()
{
AutoreleasePoolPage *result = (AutoreleasePoolPage *)
tls_get_direct(key);
//如果检查到key有绑定EMPTY_POOL_PLACEHOLDER,返回nil
if ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil;
if (result) result->fastcheck();
return result;//将当前页对象返回
}
因为我们一开始将key
与EMPTY_POOL_PLACEHOLDER
绑定过,因此这里返回空,表明当前页空,还未被创建,因此我们返回到autoreleaseFast
方法里面,将会调用autoreleaseNoPage(obj)
函数,根据我们上面对这个函数步骤的注释,这一次程序应该会走到函数的最后一部分
- 初始化第一个
AutoreleasePoolPage
- 将其设置成当前页(hot)
- 最初的
EMPTY_POOL_PLACEHOLDER
会使pushExtraBoundary
置为true
,因此这里需要为第一个AutoreleasePoolPage
先入栈一个POOL_BOUNDARY
- 最后用
add(obj)
将传入的自动释放对象obj
入栈
上面add()
函数的具体功能,其实就是将obj
的值赋值给当前AutoreleasePoolPage
的next
指针指向的内存空间,然后next
再进行++
操作,移向下一段可用内存空间,方便下一次存放自动释放对象的时候使用。如下
id *add(id obj)
{
assert(!full());
unprotect();
id *ret = next; // faster than `return next-1` because of aliasing
*next++ = obj;//先赋值,再++
protect();
return ret;
}
另外需要注意一下这里的setHotPage(page)
函数,实现如下
static inline void setHotPage(AutoreleasePoolPage *page)
{
if (page) page->fastcheck();
tls_set_direct(key, (void *)page);
}
它的作用就是把当前新创建的AutoreleasePoolPage
与key
绑定起来,日后hotPage()
函数就可以通过key
直接拿到当前页。
【再一次调用autorelease】
如果我们继续对新的对象执行autorelease
操作,同样会来到函数,但由于AutoreleasePoolPage
对象已经存在了,如果当前page
未满,会走如下函数
也就是直接通过
add(obj)
函数将obj
对象入栈
我们之前说过,一个AutoreleasePoolPage
对象能存放的自动释放对象数量是有限的,一个自动释放对象就是一个指针,占8字节,而AutoreleasePoolPage
对象可用的空间是4040个字节,也就是可以存放505个对象(指针),所以一页AutoreleasePoolPage
是有可能满页的,这个时候,autoreleaseFast
就会调用autoreleaseFullPage(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.
assert(page == hotPage());
assert(page->full() || DebugPoolAllocation);
do {//通过child指针拿到下一个没有满的page对象
if (page->child) page = page->child;
else page = new AutoreleasePoolPage(page);
} while (page->full());
setHotPage(page);//先将上面获取的page设置为当前页(hot)
return page->add(obj);//通过add函数将obj存入该page
}
其实上面就是通过AutoreleasePoolPage
对象的child
指针去寻找下一个未满的page
。AutoreleasePoolPage
对象之间是通过child
和parent
指针形成的双向链表结构,就是为了在这个时候使用的。同样,在清空释放池对象的时候,如果当前释放池完全空了,则会通过parent
指针去寻找上层的释放池。
【再一次AutoreleasePoolPage::push();】
除了系统在main
函数里加上的最初的一层@autoreleasepool {}
之外,有时候我们自己的代码里面可能会也会使用@autoreleasepool {}
,方便对一些对象进行更为灵活的内存管理。那么我们手动加的@autoreleasepool {}
肯定是嵌套在main函数@autoreleasepool {}
内部的,相当于
int main(int argc, const char * argv[]) {
@autoreleasepool {//这是系统加的第一层
@autoreleasepool {}//这是我们可能会添加的内层嵌套
}
}
现在我们再次来看一下这一次AutoreleasePoolPage::push();
会如何执行。同样程序会执行到autoreleaseFast(POOL_BOUNDARY);
POOL_BOUNDARY
会被传入autoreleaseFast
函数,并且也会通过add()
或者autoreleaseFullPage()
被添加到AutoreleasePoolPage
对象的页空间上。其实就是和普通的[obj autorelease]
的流程一样,只不过这次是obj
= POOL_BOUNDARY
,显然这是为了一个新的@autoreleasepool{}
做准备。
POOL_BOUNDARY
到底是拿来干嘛的呢?一会你就知道了。
分析完了源码,现在通过图例来展示一下
@autoreleasepool
的实现原理。
【假设】为方便展示每页AutoreleasePoolPage
只能存放3个释放对象,如下
autorelease对象什么时候回调用release方法呢?
这个问题就要搞清楚@autoreleasepool{}
的另一半AutoreleasePoolPage::pop(atautoreleasepoolobj);
做了什么。一起来看一看
releaseUntile(stop)
,这里的stop
实际上传入的就是POOL_BOUNDARY
,进入该函数
void releaseUntil(id *stop)
{
while (this->next != stop) {//🥝如果next指向POOL_BOUNDARY,跳出循环🥝
//🥝拿到当前页
AutoreleasePoolPage *page = hotPage();
//🥝🥝当前页如果为空,通过parent拿到上一个AutoreleasePoolPage对象作为当前页
while (page->empty()) {
page = page->parent;
setHotPage(page);
}
page->unprotect();
//🥝🥝🥝通过 --next 拿到当前页栈顶的对象
id obj = *--page->next;
memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
page->protect();
if (obj != POOL_BOUNDARY) {
//🥝🥝🥝🥝如果obj不是POOL_BOUNDARY,就进行[obj release]
objc_release(obj);
}
}
setHotPage(this);
}
pop()
核心步骤已经在上面函数里的注释体现出来。也就是说,当最内层的@autoreleasepool{}
作用域结束调用其对应的pop()
函数时,会从AutoreleasePoolPage
链表的当前页里面找到栈顶的对象,逐个开始释放,直到遇到POOL_BOUNDARY
就停下来,这样,就代表这一层的@autorelease{}
内所包含的所有对象都完成了release
方法调用。
当程序走到上一层的@autoreleasepool{}
作用域结束的地方,又回执行上面的流程,对其包含的对象一次调用release
方法。可以通过下图的示例来体会一下。
AutoreleasePool与RunLoop
通过上面的研究,我们知道@autoreleasepool{}
的作用,实际上就是在作用域的头和尾分别调用了objc_autoreleasePoolPush();
和objc_autoreleasePoolPop()
函数,但是在iOS项目当中,@autoreleasepool{}
的作用域是什么时候开始,什么时候结束呢?这就需要了解我们之前研究过的另一个知识点RunLoop。我们知道,除非我们手动启动子线程的RunLoop,否则程序里面只有主线程有RunLoop,这是系统默认开启的。下面我们来看一下主线程的RunLoop肚子里都有什么宝贝。
我们可以随便新建一个iOS项目,在ViewController
的viewDidLoad
方法里可以直接打印当前RunLoop对象(即主线程的RunLoop对象)
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"%@",[NSRunLoop currentRunLoop]);
}
@end
打印结果是洋洋洒洒的一大堆,如果你还不熟悉RunLoop的结构,可以参考我的Runloop的内部结构与运行原理,里面应该说的比较清楚了。我们可以在打印结果的common mode items
部分,找到两个跟autorelease
相关的observer
,如下图所示
具体如下
<CFRunLoopObserver 0x600003f3c640 [0x10a2fdae8]>
{
valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647,
callout = _wrapRunLoopWithAutoreleasePoolHandler (0x10e17ac9d),
context =
<CFArray 0x6000000353b0 [0x10a2fdae8]>
{
type = mutable-small, count = 1, values = (0 : <0x7f91ff802058>)
}
}
<CFRunLoopObserver 0x600003f3c500 [0x10a2fdae8]>
{
valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647,
callout = _wrapRunLoopWithAutoreleasePoolHandler (0x10e17ac9d),
context =
<CFArray 0x6000000353b0 [0x10a2fdae8]>
{
type = mutable-small, count = 1, values = (0 : <0x7f91ff802058> )
}
}
我们可以看到,这两个监听器分监听的状态分别是
-
activities = 0xa0
(对应十进制的160
) -
activities = 0x1
(对应十进制的1
)
这两个状态怎么解读呢?我们可以在CF框架的RunLoop源码里面找到对应的定义
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0),************十进制1---(进入loop)
kCFRunLoopBeforeTimers = (1UL << 1),****十进制2
kCFRunLoopBeforeSources = (1UL << 2),**十进制4
kCFRunLoopBeforeWaiting = (1UL << 5),***十进制32----(loop即将休眠)
kCFRunLoopAfterWaiting = (1UL << 6),*****十进制64
kCFRunLoopExit = (1UL << 7),**************十进制128----(退出loop)
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
根据RunLoop状态的枚举值可以看出,160 = 128 + 32
,也就是说
-
activities = 0xa0
=(kCFRunLoopExit | kCFRunLoopBeforeWaiting) -
activities = 0x1
=(kCFRunLoopEntry)
因此这三个状态被监听到的时候,就会调用_wrapRunLoopWithAutoreleasePoolHandler
函数。这个函数实际上是按照下图的示意运作 - 监听到kCFRunLoopEntry事件,调用
objc_autoreleasePoolPush();
- 监听到kCFRunLoopBeforeWaiting事件,调用
objc_autoreleasePoolPop()
,然后调用objc_autoreleasePoolPush();
- 监听到kCFRunLoopExit事件,调用
objc_autoreleasePoolPop()
根据上面的分析,我们可以总结,除了程序启动(对应kCFRunLoopEntry)和程序退出(对应kCFRunLoopExit)会调用一次objc_autoreleasePoolPush();
和objc_autoreleasePoolPop()
外,程序的运行过程中,每当RunLoop即将休眠,被observer
监听到kCFRunLoopBeforeWaiting状态时,会先调用一次objc_autoreleasePoolPop()
,这样就将当前的autoreleasepool
里面的对象逐个调用release
方法,相当于清空释放池子;紧接着再调用一次objc_autoreleasePoolPush();
,相当于开启一个新的释放池,等待RunLoop醒来后的下一次循环使用。
自动释放池的对象什么时候会被调用release方法呢?
RunLoop的每一圈循环过程中,调用过autorelease
方法的对象(也就是被加入AutoreleasePoolPage
的对象),会在当次循环即将进入休眠状态的时候,被调用release
方法,也可以说是被释放了。
好了,AutoreleasePool的原理以及它和RunLoop的关系就分析到这里。