iOS-内存管理6-autorelease
一. 转成C++代码
我们都知道,在MRC中,当对象调用autorelease后,这个对象会在它所在的自动释放池结束后调用release方法,如下代码:
int main(int argc, const char * argv[]) {
@autoreleasepool {
MJPerson *person = [[[MJPerson alloc] init] autorelease];
}
return 0;
}
person指针指向的对象会在{}结束后调用release方法,但是它底层是怎么实现的呢?
将上面代码转成C++代码,如下:
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;
}
上面代码,相信应该很容易理解,剔除没用的,如下:
/* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
......
}
上面定义了一个__autoreleasepool局部变量,搜索__AtAutoreleasePool定义,发现是个结构体,如下:
//C++的结构体和类很像,结构体中也可以定义函数,你可以认为它就是个类
struct __AtAutoreleasePool {//构造函数,在创建结构体的时候调用
__AtAutoreleasePool() {
atautoreleasepoolobj = objc_autoreleasePoolPush();
}//析构函数,在结构体销毁的时候调用
~__AtAutoreleasePool() {
objc_autoreleasePoolPop(atautoreleasepoolobj);
}void * atautoreleasepoolobj;
};
根据上面代码,所以文章开头的代码其实就是这三行:
//构造函数
atautoreleasepoolobj = objc_autoreleasePoolPush();
//对象调用了autorelease
MJPerson *person = [[[MJPerson alloc] init] autorelease];
//析构函数
objc_autoreleasePoolPop(atautoreleasepoolobj);
现在我们知道了,autoreleasepool会在刚开始调用Push,结束调用Pop,想要知道这两个函数内部做了什么还要进去看看。
在objc4里面搜索这两个函数:
void *
objc_autoreleasePoolPush(void)
{
//调用C++类的push()方法
return AutoreleasePoolPage::push();
}void
objc_autoreleasePoolPop(void *ctxt)
{
//调用C++类的pop()方法
AutoreleasePoolPage::pop(ctxt);
}
可以发现,分别是调用AutoreleasePoolPage类的push()和pop()方法。
小总结:
- 自动释放池的主要底层数据结构是:__AtAutoreleasePool结构体、AutoreleasePoolPage类
- 调用了autorelease的对象最终都是通过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对象占用4096字节内存,除了用来存放它内部的成员变量,剩下的空间用来存放调用了autorelease方法的对象的地址
- 所有的AutoreleasePoolPage对象通过双向链表的形式连接在一起
就是A链表可以访问B链表,B链表也可以访问A链表。
为什么要设计成双向链表?
因为一个AutoreleasePoolPage对象只占用4096字节内存,如果存满了,就会创建一个新的AutoreleasePoolPage对象,然后这些AutoreleasePoolPage对象之间通过通过双向链表的形式连接在一起,如下图:
文章图片
双向链表.png
- 0x2000 - 0x1000 = 0x1000,转成10进制就是4096字节,一个AutoreleasePoolPage对象占用4096字节。
- 0x1038 - 0x1000 = 0x0038,转成10进制就是56字节,AutoreleasePoolPage对象内部的七个成员,每个成员占用8字节,所以一共占用56字节。
- 4096 - 56 = 4040,所以剩下的4040字节用来存放调用了autorelease方法的对象的地址,如果这个AutoreleasePoolPage对象里面不够存了,就会创建一个新的AutoreleasePoolPage对象。
id * begin() {
return (id *) ((uint8_t *)this+sizeof(*this));
}id * end() {
return (id *) ((uint8_t *)this+SIZE);
}
对于begin(),就是this指针(就是自己的地址,如上面的0x1000)加上它自己有多大(就是他内部的七个成员变量的大小:56),返回的是一个地址,这个地址就是从什么地方开始存储调用了autorelease方法的对象的地址。
同理,对于end(),找到SIZE源码:
static size_t const SIZE = PAGE_MAX_SIZE;
#define PAGE_MAX_SIZEPAGE_SIZE
#define PAGE_SIZEI386_PGBYTES
#define I386_PGBYTES4096
可以发现,SIZE就是4096字节,所以对于end(),就是自己地址加上4096,就得到结束的地方的地址。
parent指针指向上一个AutoreleasePoolPage对象的地址值,child指针指向下一个AutoreleasePoolPage对象的地址值。双向链表就是通过parent指针和child指针联系在一起的。
三. push()和pop() 那么push()和pop()函数里面究竟做了什么呢?
1. push() 调用push()方法会将一个POOL_BOUNDARY入栈,并且返回其存放的内存地址,那么这句话是什么意思呢?
在objc4搜索POOL_BOUNDARY:
define POOL_BOUNDARY nil //就相当于0
看下图:
文章图片
POOL_BOUNDARY.png 就是将0这个值存放到0x1038的位置,然后把0x1038这个地址值返回,如下代码返回的就是0x1038。
atautoreleasepoolobj = objc_autoreleasePoolPush();
// atautoreleasepoolobj = 0x1038
接下来就开始执行代码:
MJPerson *person = [[[MJPerson alloc] init] autorelease];
当发现有一个对象调用了autorelease,就把这个对象的地址值接着0x1038往下存,当发现这个AutoreleasePoolPage对象不够存的时候,就会创建一个新的,然后用它的child指针指向这个新的AutoreleasePoolPage对象,然后用新的AutoreleasePoolPage对象存储,如下,当autoreleasepool里面有1000个对象的时候:
int main(int argc, const char * argv[]) {
@autoreleasepool {
for (int i = 0;
i < 1000;
i++) {
MJPerson *person = [[[MJPerson alloc] init] autorelease];
}
}
那么这1000个对象的地址在AutoreleasePoolPage对象里面存储的结构为:
文章图片
1000个对象 当代码都执行完后,就会调用objc_autoreleasePoolPop()函数
2. pop() 【iOS-内存管理6-autorelease】调用pop()方法时传入一个POOL_BOUNDARY的内存地址(也就是上面说的0x1038),会从最后一个入栈的对象开始发送release消息,直到遇到这个POOL_BOUNDARY。
上面的next是什么呢?
id *next指向了下一个能存放autorelease对象地址的区域。
比如,一开始next指向0x1038,当调用push之后把POOL_BOUNDARY入栈,这时候next就指向0x1038下一格的位置,如果这时候有一个对象的地址存储到了AutoreleasePoolPage对象里面,那么next就指向0x1038下下一格的位置,以此类推。
四. autoreleasepool嵌套 如下代码:
01 int main(int argc, const char * argv[]) {
02@autoreleasepool { //r1 = push()
03MJPerson *p1 = [[[MJPerson alloc] init] autorelease];
04MJPerson *p2 = [[[MJPerson alloc] init] autorelease];
05
06@autoreleasepool { // r2 = push()
07MJPerson *p3 = [[[MJPerson alloc] init] autorelease];
08
09@autoreleasepool { // r3 = push()
10MJPerson *p4 = [[[MJPerson alloc] init] autorelease];
11
12} // pop(r3)
13} // pop(r2)
14} // pop(r1)
15return 0;
16 }
第02行将POOL_BOUNDARY (r1)存进去
第03、04行分别将p1、p2存进去
第06行将POOL_BOUNDARY (r2)存进去
第07行将p3存进去
第09行将POOL_BOUNDARY (r3)存进去
第10行将p4存进去
第12行拿到r3,从最后一个进栈的对象(p4)开始release,一直到r3
第13行拿到r2,从最后一个进栈的对象(p3)开始release,一直到r2
第14行拿到r1,从最后一个进栈的对象(p2、p1)开始release,一直到r1
结合下图,更容易理解:
文章图片
autoreleasepool嵌套 注意:
- 上面说的入栈并不是内存中的堆、栈那个栈,而是数据结构的那种栈。
- 我们知道栈是先进后出,比如上面的存储地址的过程,push进来和pop出去就达到了先进后出的效果。
extern void _objc_autoreleasePoolPrint(void);
_objc_autoreleasePoolPrint函数是私有的,使用extern声明这个函数,就可以直接调用了,如下:
extern void _objc_autoreleasePoolPrint(void);
int main(int argc, const char * argv[]) {
@autoreleasepool { //r1 = push()
MJPerson *p1 = [[[MJPerson alloc] init] autorelease];
MJPerson *p2 = [[[MJPerson alloc] init] autorelease];
@autoreleasepool { // r2 = push()
for (int i = 0;
i < 600;
i++) {
MJPerson *p3 = [[[MJPerson alloc] init] autorelease];
}@autoreleasepool { // r3 = push()
MJPerson *p4 = [[[MJPerson alloc] init] autorelease];
_objc_autoreleasePoolPrint();
//查看自动释放池的情况
} // pop(r3)
} // pop(r2)
} // pop(r1)
return 0;
}
打印:
objc[65684]: ##############
objc[65684]: AUTORELEASE POOLS for thread 0x1000aa5c0//对应的线程
objc[65684]: 606 releases pending.//一共存了606个
objc[65684]: [0x101803000]................PAGE (full)(cold)//第一页 cold
objc[65684]: [0x101803038]################POOL 0x101803038//POOL_BOUNDARY (r1)
objc[65684]: [0x101803040]0x100541000MJPerson
objc[65684]: [0x101803048]0x100541420MJPerson
objc[65684]: [0x101803050]################POOL 0x101803050//POOL_BOUNDARY (r2)
objc[65684]: [0x101803058]0x100540e10MJPerson
objc[65684]: [0x101803ff0]0x10053bd10MJPerson
......省略
objc[65684]: [0x101802ef0]0x10053bd20MJPerson
objc[65684]: [0x101803ff8]0x10053bd20MJPerson
objc[65684]: [0x100806000]................PAGE(hot)//创建一个新的AutoreleasePoolPage对象第二页 hot
objc[65684]: [0x100806038]0x10053bd30MJPerson
objc[65684]: [0x100806040]0x10053bd40MJPerson
......省略
objc[65684]: [0x101803048]0x100541420MJPerson
objc[65684]: [0x100806348]0x10053c350MJPerson
objc[65684]: [0x100806350]################POOL 0x100806350//POOL_BOUNDARY (r3)
objc[65684]: [0x100806358]0x10053c360MJPerson
objc[65684]: ##############
一共存了606个,其中603个对象,3个POOL_BOUNDARY。从上面打印也可以看出,当AutoreleasePoolPage对象存不下时会创建一个新的AutoreleasePoolPage对象。其中第一页是cold是冷的意思,第二页是hot是热的意思,hot页是当前页的意思,以后release的时候就会从hot页开始。
五. 查看push()、autorelease、pop()源码 1. 先看push() 现在我们看push()和pop()函数的源码应该就很容易理解了:
static inline void *push()
{
id *dest;
if (DebugPoolAllocation) {
//没有page对象就new一个,并将POOL_BOUNDARY传进去
dest = autoreleaseNewPage(POOL_BOUNDARY);
} else {
//有page对象,直接将POOL_BOUNDARY传进去
dest = autoreleaseFast(POOL_BOUNDARY);
}
assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
return dest;
}
进入autoreleaseFast函数:
static inline id *autoreleaseFast(id obj)
{
AutoreleasePoolPage *page = hotPage();
if (page && !page->full()) { //如果有page并且没有满
return page->add(obj);
//将POOL_BOUNDARY入栈
} else if (page) { //如果有page(满了)
return autoreleaseFullPage(obj, page);
} else { //如果没page
return autoreleaseNoPage(obj);
}
}
通过上面两段push()的源代码可知,如果有page就直接将POOL_BOUNDARY入栈,如果没有page,就创建page之后再将POOL_BOUNDARY入栈,验证了我们上面说的。
2. 再看autorelease 在NSObjec.mm -> autorelease -> rootAutorelease -> rootAutorelease2,进入rootAutorelease2函数:
objc_object::rootAutorelease2()
{
assert(!isTaggedPointer());
//哪一个对象调用autorelease就将哪一个对象传进去,并调用AutoreleasePoolPage的autorelease方法
return AutoreleasePoolPage::autorelease((id)this);
}
可以看出,哪一个对象调用autorelease就将哪一个对象传进去,并调用AutoreleasePoolPage的autorelease方法,进入autorelease方法:
static inline id autorelease(id obj)
{
assert(obj);
assert(!obj->isTaggedPointer());
id *dest __unused = autoreleaseFast(obj);
//将对象地址值add到AutoreleasePoolPage里面去
assert(!dest||dest == EMPTY_POOL_PLACEHOLDER||*dest == obj);
return obj;
}
可以发现,这里也调用了autoreleaseFast函数,autoreleaseFast函数的实现上面有,就是将对象地址值add到AutoreleasePoolPage里面去。
3. 再看pop()
static inline void pop(void *token)
{
AutoreleasePoolPage *page;
id *stop;
......省略page = pageForPointer(token);
stop = (id *)token;
//token就是POOL_BOUNDARY的地址值,将token赋值给stop
if (*stop != POOL_BOUNDARY) {
if (stop == page->begin()&&!page->parent) {
} else {
return badPop(token);
}
}if (PrintPoolHiwat) printHiwat();
page->releaseUntil(stop);
//释放对象,直到遇到stop为止......省略
}
省略掉其他代码,只看上面的注释。
pop()函数需要传入一个参数,这个参数就是POOL_BOUNDARY的地址值,最后调用releaseUntil释放对象,直到遇到stop为止,进入releaseUntil函数:
void releaseUntil(id *stop)
{
//使用while循环不断取出page里面的东西
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();
//将取出的东西release掉
if (obj != POOL_BOUNDARY) {
objc_release(obj);
}
}
上面代码使用while循环不断取出page里面存储的对象,然后将取出的对象release掉,和我们上面讲的一样。
六. 总结 下面代码:
@autoreleasepool {
MJPerson *person = [[[MJPerson alloc] init] autorelease];
}
底层就是下面三行:
//构造函数
atautoreleasepoolobj = objc_autoreleasePoolPush();
//对象调用了autorelease
MJPerson *person = [[[MJPerson alloc] init] autorelease];
//析构函数
objc_autoreleasePoolPop(atautoreleasepoolobj);
- 自动释放池的主要底层数据结构是:__AtAutoreleasePool结构体、AutoreleasePoolPage类,调用了autorelease的对象最终都是通过AutoreleasePoolPage对象来管理的。
- 每个AutoreleasePoolPage对象占用4096字节内存,除了用来存放它内部的成员变量,剩下的空间用来存放调用了autorelease方法的对象的地址,所有的AutoreleasePoolPage对象通过双向链表的形式连接在一起。
- 调用push()方法会将一个POOL_BOUNDARY入栈,并且返回其存放的内存地址。
- 当发现有一个对象调用了autorelease,就把这个对象的地址值接着POOL_BOUNDARY往下存,当发现这个AutoreleasePoolPage对象不够存的时候,就会创建一个新的,然后用它的child指针指向这个新的AutoreleasePoolPage对象,然后用新的AutoreleasePoolPage对象存储。
- 调用pop()方法时传入一个POOL_BOUNDARY的内存地址,会从最后一个入栈的对象开始发送release消息,直到遇到这个POOL_BOUNDARY。
① 如果有@autoreleasepool{} 创建一个新项目,修改为MRC,代码如下:
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"111");
@autoreleasepool {
MJPerson *person = [[[MJPerson alloc] init] autorelease];
}
NSLog(@"333");
}
打印:
111
-[MJPerson dealloc]
333
根据我们上面学的知识,很好理解,因为使用了autoreleasepool,所以autoreleasepool里面调用了autorelease方法的对象会在{}结束之后释放,所以才是上面打印。
② 如果没写@autoreleasepool{} 那如果没写@autoreleasepool呢?
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"111");
MJPerson *person = [[[MJPerson alloc] init] autorelease];
NSLog(@"333");
}
打印:
111
333
-[MJPerson dealloc]
你可能会想,上面的代码没autoreleasepool,但是整个程序的main函数里面不是有一个autoreleasepool吗,那上面那些代码是不是被main函数的autoreleasepool管理呢?
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
显然不是的,如果上面的代码是被main函数的autoreleasepool管理的,那么程序退出之前这个autoreleasepool是不会结束的,对象就不会被释放,但是上面打印的结果表明对象的确被释放了,说明上面那些代码不是被main函数的autoreleasepool管理的。
可能你还会想,那person对象会不会是在viewDidLoad方法调用完毕再释放的呢?
这个更好验证:
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"111");
MJPerson *person = [[[MJPerson alloc] init] autorelease];
NSLog(@"333");
NSLog(@"%s", __func__);
}- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
NSLog(@"%s", __func__);
}- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
NSLog(@"%s", __func__);
}
打印:
111
333
-[ViewController viewDidLoad]
-[ViewController viewWillAppear:]
-[MJPerson dealloc]
-[ViewController viewDidAppear:]
可以发现,是在viewWillAppear之后才释放的,可能你会越来越迷糊,那person对象究竟是在什么时候被释放呢?
其实这个问题和RunLoop有关:
打印NSLog(@"%@",[NSRunLoop mainRunLoop]),打印结果比较多,抽取我们需要的两个监听器,如下:
observers = (
"{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x10d690c9d), context = {type = mutable-small, count = 1, values = (\n\t0 : <0x7ffc78003058>\n)}}",
......省略
"{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x10d690c9d), context = {type = mutable-small, count = 1, values = (\n\t0 : <0x7ffc78003058>\n)}}"
),
RunLoop的状态如下,下面会用到
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), 1
kCFRunLoopBeforeTimers = (1UL << 1), 2
kCFRunLoopBeforeSources = (1UL << 2), 4
kCFRunLoopBeforeWaiting = (1UL << 5), 32
kCFRunLoopAfterWaiting = (1UL << 6), 64
kCFRunLoopExit = (1UL << 7), 128
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
观察打印结果里面的activities
第一个observer的activities = 0x1,就是1,说明第一个observer监听kCFRunLoopEntry状态。
第一个observer的activities = 0xa0,转成十进制是160,正好是32+128,说明第二个observer监听kCFRunLoopBeforeWaiting和kCFRunLoopExit状态。
看着MJ老师的图,我们进行总结:
RunLoop的运行逻辑.png 总结:
- iOS在主线程的Runloop中注册了2个Observer,当第1个Observer监听到了进入状态(kCFRunLoopEntry),就会调用objc_autoreleasePoolPush()
- 当第2个Observer监听到了即将休眠状态(kCFRunLoopBeforeWaiting)就会调用objc_autoreleasePoolPop()和objc_autoreleasePoolPush()
- 当第2个Observer监听到了即将退出状态(kCFRunLoopBeforeExit)就会调用objc_autoreleasePoolPop()
现在就能回答刚才的问题了,如果没写@autoreleasepool{},由于整个程序没有退出,autoreleasepool里面调用了autorelease方法的对象会在RunLoop休眠之前被释放。
- (void)viewDidLoad {
[super viewDidLoad];
// 这个Person什么时候调用release,是由RunLoop来控制的
// 它可能是在某次RunLoop循环中,RunLoop休眠之前调用了release
MJPerson *person = [[[MJPerson alloc] init] autorelease];
}
再次观察上面的打印信息:
111
333
-[ViewController viewDidLoad]
-[ViewController viewWillAppear:]
-[MJPerson dealloc]
-[ViewController viewDidAppear:]
既然person对象会在RunLoop休眠之前被释放,那么可以看出viewDidLoad和viewWillAppear处在同一次运行循环中(因为一次休眠到下一次休眠是一个循环)。
2. 面试题2 ARC中,方法里有局部对象,出了方法后会立即释放吗?
这个问题,我们猜想有两种可能:
- 如果ARC生成的代码是直接在方法完成之前给对象调用了一次[person release],那么对象就会在方法结束之后立马释放。
- 如果ARC生成的代码是直接在对象后面加autorelease,那么对象就会在RunLoop休眠之前被释放。
- (void)viewDidLoad {
[super viewDidLoad];
MJPerson *person = [[MJPerson alloc] init];
NSLog(@"%s", __func__);
// ARC中就相当于在这里生成一行 [person release];
}- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
NSLog(@"%s", __func__);
}- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
NSLog(@"%s", __func__);
}
打印:
-[ViewController viewDidLoad]
-[MJPerson dealloc]
-[ViewController viewWillAppear:]
-[ViewController viewDidAppear:]
可以发现,viewDidLoad执行完后对象立马就被释放了,说明ARC中,方法里有局部对象,出了方法后会立即释放,因为就相当于在方法的最后加一行release代码。
Demo地址:autorelease
推荐阅读
- JAVA(抽象类与接口的区别&重载与重写&内存泄漏)
- 基于微信小程序带后端ssm接口小区物业管理平台设计
- 2020-04-07vue中Axios的封装和API接口的管理
- 全过程工程咨询——时间管理(12)
- 《卓有成效的管理者》第二十二堂课(创造英雄)
- 游乐园系统,助力游乐园管理
- #山言良语#用管理思维百天减肥18斤
- 最有效的时间管理工具(赢效率手册和总结笔记)
- 干货来袭(自我管理(来几款撩人的APP))
- Java内存泄漏分析系列之二(jstack生成的Thread|Java内存泄漏分析系列之二:jstack生成的Thread Dump日志结构解析)