阅读 669

iOS -- Autorelease & AutoreleasePool

前言

内存管理一直是Objective-C 的重点,在MRC环境下,通过调用[obj autorelease]来延迟内存的释放,在现在ARC环境下,我们都知道编译器会在合适的地方插入release/autorelease内存释放语句,我们甚至可以不需要知道Autorelease就能很好的管理内存。虽然现在已经几乎用不到MRC,但是了解 Objective-C 的内存管理机制仍然是十分必要的,看看编译器帮助我们怎么来管理内存。本文仅仅是记录自己的学习笔记。

AutoreleasePool简介

1.什么是AutoreleasePool

AutoreleasePool:自动释放池是 Objective-C 开发中的一种自动内存回收管理的机制,为了替代开发人员手动管理内存,实质上是使用编译器在适当的位置插入releaseautorelease等内存释放操作。当对象调用 autorelease方法后会被放到自动释放池中延迟释放时机,当缓存池需要清除dealloc时,会向这些 Autoreleased对象做 release 释放操作。

2.对象什么时候释放(ARC规则)

一般的说法是对象会在当前作用域大括号结束时释放, 有这样一个ARC环境下简单的例子🌰:首先创建一个ZHPerson类:

//// ZHPerson.h
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface ZHPerson : NSObject

+(instancetype)object;
@end

////ZHPerson.m

#import "ZHPerson.h"
@implementation ZHPerson

-(void)dealloc
{
    NSLog(@"ZHPerson dealloc");
}
+(instancetype)object
{
    return [[ZHPerson alloc] init];
}
@end
复制代码

然后在ViewController.m导入头文件ZHPerson.h,然后在写一段这样的代码:

__weak id temp = nil;
{
    ZHPerson *person = [[ZHPerson alloc] init];
    temp = person;
}
NSLog(@"temp = %@",temp);
复制代码

解释一下这个代码:先声明了一个 __weak 变量temp,因为 __weak 变量有一个特性就是它不会影响所指向对象的生命周期,然后让变量temp指向创建的person对象,输出如下:

这里超出了person的作用域,它就被释放了,看来是正常的。

把上面的创建对象的方法,变一变写法:

__weak id temp = nil;
{
    ZHPerson *person = [ZHPerson object];
    temp = person;
}
NSLog(@"temp = %@",temp);
复制代码

输出如下:

这里person对象超出了其作用域还是存在的,被延迟释放了,也就是说其内部调用了autorelease方法。

小总结

查询得知:以 alloc, copy, ,mutableCopynew这些方法会被默认标记为 __attribute((ns_returns_retained)) ,以这些方法创建的对象,编译器在会在调用方法外围要加上内存管理代码retain/release,所以其在作用域结束的时候就会释放,而不以这些关键字开头的方法,会被默认标记为__attribute((ns_returns_not_retained)),编译器会在方法内部自动加上autorelease方法,这时创建的对象就会被注册到自动释放池中,同时其释放会延迟,等到自动释放池销毁的时候才释放。

3.AutoreleasePool的显示创建

1.MRC下的创建

//1.生成一个NSAutoreleasePool对象
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
//2.创建对象
id object = [[NSObject alloc] init];
//3.对象调用autorelease方法
[object autorelease];
//4.废弃NSAutoreleasePool对象,会对释放池中的object发送release消息
[pool drain];
复制代码

2.ARC下的创建

@autoreleasepool {
    //LLVM会在内部插入autorelease方法
    id object = [[NSObject alloc] init];
    }
复制代码

AutoreleasePool 的作用前面有提到过,每当一个对象调用 autorelease方法时,实际上是将该对象放入当前 AutoreleasePool 中,当前AutoreleasePool 释放时,会对添加进该 AutoreleasePool 中的对象逐一调用 release 方法。在ARC环境下,并不需要特别的去关注Autoreleasepool的使用,因为系统已经做了处理。

AutoreleasePool探索学习

为了看一下AutoreleasePool到底做了什么,先来创建一个main.m文件(Xcode -> File -> New Project -> macOS -> Command Line Tool -> main.m);

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        NSLog(@"Hello, World!");
    }
    return 0;
}
复制代码

然后,使用编译器clang编译main.m转化成main.cpp文件(在终端使用命令:clang -rewrite-objc main.m),滑到main.cpp文件的最后,有这样一段代码:

这个代码是把@autoreleasePool转换成一个__AtAutoreleasePool类型的局部私有变量__AtAutoreleasePool __autoreleasepool;

接着在 main.cpp文件中查询__AtAutoreleasePool,来看一下它具体的实现:

可以看到__AtAutoreleasePool是结构体类型,并且实现了两个函数:构造函数__AtAutoreleasePool()和析构函数~__AtAutoreleasePool()

也就是说在声明 __autoreleasepool 变量时,构造函数 __AtAutoreleasePool() 被调用,即执行 atautoreleasepoolobj = objc_autoreleasePoolPush(); ;当出了当前作用域时,析构函数 ~__AtAutoreleasePool()被调用,即执行 objc_autoreleasePoolPop(atautoreleasepoolobj); 那么上面的main.m中的代码可以用这种形式代替:

#import <Foundation/Foundation.h>

int main(int argc, const char * argv[]) {
   // @autoreleasepool
    {
        void *atautoreleasepoolobj = objc_autoreleasePoolPush();
        // insert code here...
        NSLog(@"Hello, World!");
        objc_autoreleasePoolPop(atautoreleasepoolobj);
    }
    return 0;
}
复制代码

接下来看一下析构函数和构造函数分别实现了什么内容?这里需要一份objc_runtime的源码(源码地址),这里使用的是objc4-756.2.tar.gz:

这里两个函数本质上就是分别调用了AutoreleasePoolPagepush方法和pop方法(这里::是C++调用方法的形式,类似于点语法)。

1.AutoreleasePoolPage

AutoreleasePoolPage是一个C++实现的类,它的具体实现代码是:

class AutoreleasePoolPage 
{ 
#   define POOL_BOUNDARY nil    //哨兵对象(可以看做是一个边界)
    static size_t const COUNT = SIZE / sizeof(id);    // 对象数量

    magic_t const magic;    //用来校验 `AutoreleasePoolPage`的结构是否完整;
    id *next;    //指向最新添加的 `autoreleased` 对象的下一个位置,初始化时指向 `begin()` ;
    pthread_t const thread;    //指向当前线程;
    AutoreleasePoolPage * const parent;    //指向父结点,第一个结点的 `parent` 值为 `nil` ;
    AutoreleasePoolPage *child;    //指向子结点,最后一个结点的 `child` 值为 `nil` ;
    uint32_t const depth;    //代表深度,从 `0` 开始,往后递增 `1`;
    uint32_t hiwat;    //代表 `high water mark` ;
//剩下代码省略......
}
复制代码

通过源码可以知道这是一个典型的双向列表结构,所以AutoreleasePool是由若干个AutoreleasePoolPage以双向链表的形式组合而成。

AutoreleasePoolPage每个对象会开辟4096字节内存(虚拟内存一页的大小),除了上面的实例变量所占空间,剩下的空间全部用来储存autorelease对象的地址,AutoreleasepoolPage 通过压栈的方式来存储每个autorelease的对象(从低地址到高地址)。其中next指针作为游标指向栈顶最新add进来的autorelease对象的下一个位置,当 next指针指向begin时,表示 AutoreleasePoolPage为空;当 next指针指向end时,表示 AutoreleasePoolPage 已满,此时会新建一个AutoreleasePoolPage对象,连接链表,后来的autorelease对象在新的AutoreleasePoolPage插入,同样的新AutoreleasePoolPagenext指针被初始化在栈底(指向begin的位置)。

2.AutoreleasePoolPage::push()

既然已经知道了autorelease的对象会通过压栈的方式插入到AutoreleasePoolPage当中,那么显然AutoreleasePoolPagepush方法就承包了AutoreleasePoolPage的创建和插入。

接着看下push方法的源码:

static inline void *push() 
{
    id *dest;
    //判断是否已经初始化AutoreleasePoolPage
    if (DebugPoolAllocation) {
        // Each autorelease pool starts on a new pool page.
        dest = autoreleaseNewPage(POOL_BOUNDARY);
    } else {
        dest = autoreleaseFast(POOL_BOUNDARY);
    }
    assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
    return dest;
}
复制代码

这里的POOL_BOUNDARY可以理解为哨兵对象,或者理解为一种边界标识,而且这个POOL_BOUNDARY值为0,是个nil

接下来,先来看一下autoreleaseFast这个方法,

static inline id *autoreleaseFast(id obj)
{
    //获取到当前page,这个hotPage是从当前线程的局部私有空间取出来的
    AutoreleasePoolPage *page = hotPage();
    
    if (page && !page->full()) {
        return page->add(obj);
    } else if (page) {
        return autoreleaseFullPage(obj, page);
    } else {
        return autoreleaseNoPage(obj);
    }
}
复制代码

我们知道链表是有空间的,所以上面👆的源码可以理解为:

(1). 当前page存在且没有满时,直接将对象添加到当前page中,即next指向的位置;

(2). 当前page存在并且已满时,创建一个新的page,并将对象添加到新创建的page 中,然后将这两个链表节点进行链接。

(3). 当前page不存在时,创建第一个page ,并将对象添加到新创建的page中。

这里重点看一下page->add(obj)这个方法,

id *add(id obj)
{
    assert(!full());
    unprotect();
    id *ret = next;  // faster than `return next-1` because of aliasing
    *next++ = obj;
    protect();
    return ret;
}
复制代码

可以看到这里返回的ret其实next指针指向的地址,由上面的push方法的源码可知,这里page->add(obj)传入的obj其实就是POOL_BOUNDARY,也就是说每一次调用push方法,都会插入一个POOL_BOUNDARY,所以objc_autoreleasePoolPush的返回值就是这个哨兵对象的地址。

3.AutoreleasePoolPage::pop(ctxt)

通过上面对构造函数objc_autoreleasePoolPush的学习,已经知道objc_autoreleasePoolPush返回的是哨兵对象的地址,那么在调用析构函数objc_autoreleasePoolPop的时候传入的也就是这个哨兵对象的地址。随着方法的一步步调用,紧接着来看下AutoreleasePoolPagepop方法的实现:

static inline void pop(void *token)
{
    AutoreleasePoolPage *page;
    id *stop;
    if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
        if (hotPage()) {
            pop(coldPage()->begin());
        } else {
            setHotPage(nil);
        }
        return;
    }
            
    page = pageForPointer(token);  //根据传入的哨兵对象的地址,获取到page中的哨兵对象之后的地址空间
    stop = (id *)token;
    if (*stop != POOL_BOUNDARY) {
        if (stop == page->begin()  &&  !page->parent) {
        } else {
            return badPop(token);
        }
    }
    if (PrintPoolHiwat) printHiwat();
            
    page->releaseUntil(stop); //对当前链表当中的对象进行release操作
    if (DebugPoolAllocation  &&  page->empty()) {
    //释放 `Autoreleased` 对象后,销毁多余的 page
        AutoreleasePoolPage *parent = page->parent;
        page->kill();
        setHotPage(parent);
    } else if (DebugMissingPools  &&  page->empty()  &&  !page->parent) {
        page->kill();
        setHotPage(nil);
    }
   else if (page->child) {
        if (page->lessThanHalfFull()) {
            page->child->kill();
        }
        else if (page->child->child) {
            page->child->child->kill();
        }
    }
}
复制代码

这里重点看一下page->releaseUntil(stop)方法:

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_BOUNDARY) {
            objc_release(obj);
        }
    }
    setHotPage(this);
}
复制代码

这里的stop同样是POOL_BOUNDARY的地址,这里分析一下这个方法:

(1). 外部循环挨个遍历autoreleased 对象,直到遍历到哨兵对象POOL_BOUNDARY

(2). 如果当前page没有 POOL_BOUNDARY,并且为空,则将hotPage设置为当前page的父节点。

(3). 给当前autoreleased对象发送release消息。

(4). 最后再次配置hotPage

4.autorelease

通过上面的分析已经知道了构造方法objc_autoreleasePoolPush会创建AutoreleasePoolPage,并插入哨兵对象POOL_BOUNDARY,析构方法objc_autoreleasePoolPop会对哨兵对象之后插入的对象发送release消息,那么在这两个方法之间,对象通过调用autorelease是怎么插入到AutoreleasePoolPage的呢?下面来看下autorelease的源码实现:

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;
}
复制代码

这里的重点还是autoreleaseFast(obj);由于这里插入对象的方法和AutoreleasePoolPage调用push方法的实现是一样的,只不过push操作插入的是一个 POOL_BOUNDARY,而autorelease操作插入的是一个具体的autoreleased对象,在此处就不做多余分析。

通过上面👆的这些分析,已经大概知道AutoreleasePool是怎样的一个构造,以及内如是如何实现自动释放的。

5.AutoreleasePool的嵌套

对于嵌套的AutoreleasePool也是同样的原理,在pop的时候总会释放对象到上次push的位置为止,也就是哨兵位置,多层的pool就是插入多个哨兵对象而已,然后根据哨兵对象来进行释放,就像剥洋葱一样一层一层的,互不影响。

那么这里有个疑问,如果在AutoreleasePool多层嵌套中是同一个对象呢,那么会怎么释放?下面通过一个小例子🌰来看一下:

@autoreleasepool {
    ZHPerson *person = [ZHPerson object];
    NSLog(@"current count %d",_objc_rootRetainCount(person));
    @autoreleasepool {
        ZHPerson *person1 = person;
        NSLog(@"current count %d",_objc_rootRetainCount(person));
        @autoreleasepool {
            ZHPerson *person2 = person;
            NSLog(@"current count %d",_objc_rootRetainCount(person));
        }
    }
}
复制代码

打印结果如下:

这里dealloc方法只调用了一次,由上面的代码可知:当前person1person2是对person的引用,如果系统会为每一次引用都自动插入一个autorelease,那么对象在执行第一个autorelease的时候,会调用objc_release(obj)来释放当前的对象,那么当调用rootRelease()的时候就会报错,因为当前对象已经被释放了,那么也就是说对于引用的对象只会被释放一次。(同一个对象不能够反复的autorelease)

NSthread、NSRunLoop、AutoReleasePool

1.NSthread和AutoReleasePool

先来看个简单的例子:

temp的位置设置一个断点,然后在控制台输入watchpoint set variable temp,
等到这个线程执行结束之后,来看一下左侧边栏的内容:
当执行到NSLog(@"thread end");这句代码,表示线程执行结束,这里,其实线程会先调用[NSthread exit],然后执行_pthread_tsd_cleanup,清除当前线程的有关资源,然后调用tls_dealloc,也就是把当前线程关联的AutoReleasePool释放掉,最后调用weak_clear_no_lock清除指针。

那么这一系列过程就说明了:在NSThread退出了之后,与NSThread对应的AutoReleasePool也会被自动清空,所以当一个线程结束的时候,就会回收♻️AutoReleasePool中自动释放的对象。

结论:

每一个线程都会维护自己的AutoReleasePool,而每一个AutoReleasePool都会对应唯一一个线程,但是线程可以对应多个AutoReleasePool

2.NSRunLoop和AutoReleasePool

对于NSThread只是一个简单的线程,如果把它换成一个常驻线程呢?

这里创建一个NSTimer,并将其常驻。利用同样的方式,watchpoint set variable temp,:

可以看到这里NStimer是被加入到子线程当中的,但是在子线程中,我们并没有去写关于AutoReleasePool的内容,我们只知道test做了autorelease操作。下面回到源码中来看:

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

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);
    }
}

id *autoreleaseNoPage(id obj)
{
   AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
   setHotPage(page);
}
//这里省略了部分代码
复制代码

所以从上面的源码我们可以得出结论:子线程在使用autorelease对象的时候,会懒加载出来一个AutoreleasePoolPage,然后将对象插入进去。

那么问题又来了,autorelease对象在什么时候释放的呢?也就说AutoreleasePoolPage在什么时候调用了pop方法?

其实在上面创建一个NSThread的时候,在调用[NSthread exit]的时候,会释放当前资源,也就是把当前线程关联的autoReleasePool释放掉,而在这里当RunLoop执行完成退出的时候,也会执行pop方法,这就说明了为什么在子线程当中,我们没有显示的调用pop,它也能释放当前AutoreleasePool的资源的原因。

3.主线程的NSRunLoop和AutoReleasePool

那么在主线程的RunLoop到底什么时候把对象进行释放回收的呢?

简单粗暴点,直接在控制台通过po [NSRunloop currentRunloop]打印主线程的RunLoop:

这里,系统在主线程的RunLoop里注册了两个Observer,回调都是_wrapRunLoopWithAutoreleasePoolHandler,第一个Observer的状态是activities = 0x1,第二个Observer的状态是activities = 0xa0,这两种状态代表什么意思呢?

先在这里插入一点RunLoop的内容(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
};
复制代码

0x1代表的是kCFRunLoopEntry,也就是说第一个 Observer监视的事件是Entry(即将进入Loop时),其回调内会调用_objc_autoreleasePoolPush()创建一个自动释放池。其order优先级是-2147483647,优先级最高,保证创建自动释放池发生在其他所有回调之前。

0xa0对应的是kCFRunLoopBeforeWaitingkCFRunLoopExit,也就是说第二个Observer监视了两个事件:kCFRunLoopBeforeWaiting准备进入休眠,kCFRunLoopExit即将退出RunLoop。在kCFRunLoopBeforeWaiting事件时调用 _objc_autoreleasePoolPop()_objc_autoreleasePoolPush() 释放旧的自动释放池并创建新的自动释放池;在kCFRunLoopExit事件时调用_objc_autoreleasePoolPop() 来释放自动释放池,同时这个Observerorder优先级是 2147483647,优先级最低,保证其释放自动释放池的操作发生在其他所有回调之后。

所以在没有手动增加AutoreleasePool的情况下,Autorelease对象都是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池pushpop操作。

总结:

对于不同线程,应当创建自己的AutoReleasePool。如果应用长期存在,应该定期drain和创建新的AutoReleasePool,AutoReleasePoolRunLoop 与线程是一一对应的关系,AutoReleasePoolRunLoop在开始迭代时做push操作,在RunLoop休眠或者迭代结束时做pop操作。

AutoreleasePool的应用场景

通常情况下我们是不需要手动创建AutoreleasePool,但是也有一些特殊的:

  1. 编写的程序不基于UI框架,如命令行程序。

  2. 在循环中创建大量临时对象时用以降低内存占用峰值。

  3. 在主线程之外创建新的线程,在新线程开始执行处,创建自己的AutoreleasePool,否则将导致内存泄漏。

下面就来简单看下第二种情况,直接来个for循环:

for (int i = 0; i < 100000000; i ++) {
        NSString * str = [NSString stringWithFormat:@"noAutoReleasePool"];
        NSString *tempstr = str;
    }
}
复制代码

来看一下Memory的使用情况:

相反的,如果加上AutoreleasePool,来看一下:

for (int i = 0; i < 100000000; i ++) {
    @autoreleasepool {
        NSString * str = [NSString stringWithFormat:@"AutoReleasePool"];
        NSString *tempstr = str;
    }
}
复制代码

来看一下这种情况下的Memory的使用情况:

这个对比伤害就很明显了。

这个做个备注:在主函数main.m文件中的@autoreleasepool,如果在这里做个测试,使用for循环创建大量的临时对象,是否加上这个@autoreleasepoolMemory的使用情况没有特别大的影响。

总结

写到这里,对于AutoReleasePool学习内容就暂告一段了,正常情况下,我们不需要去关心AutoReleasePool的创建和释放,但是学习理解了AutoReleasePool能够使我们更加理解ARC模式下系统是怎样来管理内存的。

文中内容如有不当之处,还请指出,谢谢您!

关注下面的标签,发现更多相似文章
评论