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()方法。

小总结:

  1. 自动释放池的主要底层数据结构是:__AtAutoreleasePool结构体、AutoreleasePoolPage类
  2. 调用了autorelease的对象最终都是通过AutoreleasePoolPage对象来管理的

二. 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;
......
}

那么这些成员有什么用?

先看结论:

  1. 每个AutoreleasePoolPage对象占用4096字节内存,除了用来存放它内部的成员变量,剩下的空间用来存放调用了autorelease方法的对象的地址
  2. 所有的AutoreleasePoolPage对象通过双向链表的形式连接在一起

什么是双向链表?
就是A链表可以访问B链表,B链表也可以访问A链表。

为什么要设计成双向链表?
因为一个AutoreleasePoolPage对象只占用4096字节内存,如果存满了,就会创建一个新的AutoreleasePoolPage对象,然后这些AutoreleasePoolPage对象之间通过通过双向链表的形式连接在一起,如下图:

iOS-内存管理6-autorelease_第1张图片
双向链表.png
  1. 0x2000 - 0x1000 = 0x1000,转成10进制就是4096字节,一个AutoreleasePoolPage对象占用4096字节。
  2. 0x1038 - 0x1000 = 0x0038,转成10进制就是56字节,AutoreleasePoolPage对象内部的七个成员,每个成员占用8字节,所以一共占用56字节。
  3. 4096 - 56 = 4040,所以剩下的4040字节用来存放调用了autorelease方法的对象的地址,如果这个AutoreleasePoolPage对象里面不够存了,就会创建一个新的AutoreleasePoolPage对象。

上面的begin()函数是什么?同样在NSObject.mm里面找到源码:

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_SIZE           PAGE_SIZE
#define PAGE_SIZE               I386_PGBYTES
#define I386_PGBYTES            4096          

可以发现,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

看下图:

iOS-内存管理6-autorelease_第2张图片
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对象里面存储的结构为:

iOS-内存管理6-autorelease_第3张图片
1000个对象

当代码都执行完后,就会调用objc_autoreleasePoolPop()函数

2. pop()

调用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()
03         MJPerson *p1 = [[[MJPerson alloc] init] autorelease];
04         MJPerson *p2 = [[[MJPerson alloc] init] autorelease];
05
06         @autoreleasepool { // r2 = push()
07             MJPerson *p3 = [[[MJPerson alloc] init] autorelease];
08
09             @autoreleasepool { // r3 = push()
10                 MJPerson *p4 = [[[MJPerson alloc] init] autorelease];
11
12             } // pop(r3)
13         } // pop(r2)
14     } // pop(r1)
15     return 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

结合下图,更容易理解:

iOS-内存管理6-autorelease_第4张图片
autoreleasepool嵌套

注意:

  1. 上面说的入栈并不是内存中的堆、栈那个栈,而是数据结构的那种栈。
  2. 我们知道栈是先进后出,比如上面的存储地址的过程,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]       0x100541000  MJPerson
objc[65684]: [0x101803048]       0x100541420  MJPerson
objc[65684]: [0x101803050]  ################  POOL 0x101803050     //POOL_BOUNDARY (r2)
objc[65684]: [0x101803058]       0x100540e10  MJPerson
objc[65684]: [0x101803ff0]       0x10053bd10  MJPerson
......省略
objc[65684]: [0x101802ef0]       0x10053bd20  MJPerson
objc[65684]: [0x101803ff8]       0x10053bd20  MJPerson
objc[65684]: [0x100806000]  ................  PAGE  (hot)          //创建一个新的AutoreleasePoolPage对象  第二页 hot
objc[65684]: [0x100806038]       0x10053bd30  MJPerson
objc[65684]: [0x100806040]       0x10053bd40  MJPerson
......省略
objc[65684]: [0x101803048]       0x100541420  MJPerson
objc[65684]: [0x100806348]       0x10053c350  MJPerson
objc[65684]: [0x100806350]  ################  POOL 0x100806350     //POOL_BOUNDARY (r3)
objc[65684]: [0x100806358]       0x10053c360  MJPerson
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);
  1. 自动释放池的主要底层数据结构是:__AtAutoreleasePool结构体、AutoreleasePoolPage类,调用了autorelease的对象最终都是通过AutoreleasePoolPage对象来管理的。
  2. 每个AutoreleasePoolPage对象占用4096字节内存,除了用来存放它内部的成员变量,剩下的空间用来存放调用了autorelease方法的对象的地址,所有的AutoreleasePoolPage对象通过双向链表的形式连接在一起。
  3. 调用push()方法会将一个POOL_BOUNDARY入栈,并且返回其存放的内存地址。
  4. 当发现有一个对象调用了autorelease,就把这个对象的地址值接着POOL_BOUNDARY往下存,当发现这个AutoreleasePoolPage对象不够存的时候,就会创建一个新的,然后用它的child指针指向这个新的AutoreleasePoolPage对象,然后用新的AutoreleasePoolPage对象存储。
  5. 调用pop()方法时传入一个POOL_BOUNDARY的内存地址,会从最后一个入栈的对象开始发送release消息,直到遇到这个POOL_BOUNDARY。

七. RunLoop与autorelease

1. 面试题1

调用autorelease的对象在什么时机会被调用release?

① 如果有@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

总结:

  1. iOS在主线程的Runloop中注册了2个Observer,当第1个Observer监听到了进入状态(kCFRunLoopEntry),就会调用objc_autoreleasePoolPush()
  2. 当第2个Observer监听到了即将休眠状态(kCFRunLoopBeforeWaiting)就会调用objc_autoreleasePoolPop()和objc_autoreleasePoolPush()
  3. 当第2个Observer监听到了即将退出状态(kCFRunLoopBeforeExit)就会调用objc_autoreleasePoolPop()

这样,整个RunLoop运行循环中push和pop就能完全对得上。

现在就能回答刚才的问题了,如果没写@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中,方法里有局部对象,出了方法后会立即释放吗?

这个问题,我们猜想有两种可能:

  1. 如果ARC生成的代码是直接在方法完成之前给对象调用了一次[person release],那么对象就会在方法结束之后立马释放。
  2. 如果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

你可能感兴趣的:(iOS-内存管理6-autorelease)