iOS底层 -- 内存管理之autorelase、autoreleasepool原理

自动释放池

  • 自动释放池的主要底层数据结构是:__AtAutoreleasePoolAutoreleasePoolPage

  • 调用了autorelease的对象最终都是通过AutoreleasePoolPage对象来管理的

  • __AtAutoreleasePool结构体

 struct __AtAutoreleasePool {
    __AtAutoreleasePool() { // 构造函数,在创建结构体的时候调用
        atautoreleasepoolobj = objc_autoreleasePoolPush();
    }

    ~__AtAutoreleasePool() { // 析构函数,在结构体销毁的时候调用
        objc_autoreleasePoolPop(atautoreleasepoolobj);
    }

    void * atautoreleasepoolobj;
 };

下面将代码进行转换

@autoreleasepool {
    Person *p4 = [[[MJPerson alloc] init] autorelease];
}

将上述代码转成C++代码

 {
    __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"));
 }

去除一些不必要的代码后变成下面这个样子

 {
    __AtAutoreleasePool __autoreleasepool;
   MJPerson *person = [[[MJPerson alloc] init] autorelease];
 }

又因为__AtAutoreleasePool是一个结构体,所以创建时会调用其构造函数__AtAutoreleasePool(),当离开其作用域后,会调用其析构函数~__AtAutoreleasePool(),所以上面的代码又可以转换成下面的代码

atautoreleasepoolobj = objc_autoreleasePoolPush();
Person *person = [[[Person alloc] init] autorelease];
objc_autoreleasePoolPop(atautoreleasepoolobj);

4.1 自动释放池
  • 自动释放池的主要底层数据结构是:__AtAutoreleasePoolAutoreleasePoolPage
  • 调用了autorelease的对象最终都是通过AutoreleasePoolPage对象来管理的

源码分析

  • clang重写@autoreleasepool
  • objc4源码:NSObject.mm

变量说明

  • magic 用来校验 AutoreleasePoolPage 的结构是否完整
  • next指向最新添加的 autoreleased 对象的下一个位置,初始化时指向 begin()
  • thread 指向当前线程
  • parent指向父结点,第一个结点的 parent 值为 nil
  • child 指向子结点,最后一个结点的 child 值为 nil
  • depth 代表深度,从 0 开始,往后递增 1
  • hiwat 代表 high water mark
4.2 AutoreleasePoolPage的结构
  • 每个AutoreleasePoolPage对象占用4096字节内存,除了用来存放它内部的成员变量,剩下的空间用来存放autorelease对象的地址
  • 所有的AutoreleasePoolPage对象通过双向链表的形式连接在一起
atautoreleasepoolobj = objc_autoreleasePoolPush();
Person *person = [[[Person alloc] init] autorelease];
objc_autoreleasePoolPop(atautoreleasepoolobj);

上图的执行步骤说明

  • 调用push方法会将一个POOL_BOUNDARY入栈,并且返回其存放的内存地址,即返回给atautoreleasepoolobj
  • 调用pop方法时传入一个POOL_BOUNDARY的内存地址,会从最后一个入栈的对象开始发送release消息,直到遇到这个POOL_BOUNDARY
  • id *next指向了下一个能存放autorelease对象地址的区域

代码例子如下

extern void _objc_autoreleasePoolPrint(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool { //  r1 = push()

        Person *p1 = [[[Person alloc] init] autorelease];
        Person *p2 = [[[Person alloc] init] autorelease];

        @autoreleasepool { // r2 = push()
            for (int i = 0; i < 5; i++) {
                Person *p3 = [[[Person alloc] init] autorelease];
            }

            @autoreleasepool { // r3 = push()
                Person *p4 = [[[Person alloc] init] autorelease];
                _objc_autoreleasePoolPrint();
            } // pop(r3)
        } // pop(r2)
    } // pop(r1)
    return 0;
}

执行结果

  1. 因为只打印了一个PAGE,所以说明他们是在同一个AutoreleasePoolPage,只是每次一个新的autoreleasepool,都会插入一个POOL_BOUNDARY
  2. 每次释放对象时,都是从后往前释放,直到遇到POOL_BOUNDARY为止。

代码例子二

int main(int argc, const char * argv[]) {
    @autoreleasepool { //  r1 = push()
        @autoreleasepool {
            MJPerson *p1 = [[[MJPerson alloc] init] autorelease];
            _objc_autoreleasePoolPrint();
        }
        return 0;
    }
}

执行结果

代码例子三

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

执行结果

4.3 Runloop和Autorelease

iOS在主线程的Runloop中注册了2个Observer

  • 第1个Observer监听了kCFRunLoopEntry事件,会调用objc_autoreleasePoolPush()

  • 第2个Observer

<1> 监听了kCFRunLoopBeforeWaiting事件,会调用objc_autoreleasePoolPop()objc_autoreleasePoolPush()
<2> 监听了kCFRunLoopBeforeExit事件,会调用objc_autoreleasePoolPop()

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

代码例子如下

  • MRC环境下
- (void)viewDidLoad {
    [super viewDidLoad];

    // 这个Person什么时候调用release,是由RunLoop来控制的
    // 它可能是在某次RunLoop循环中,RunLoop休眠之前调用了release
    MJPerson *person = [[[MJPerson alloc] init] autorelease];
    NSLog(@"%s", __func__);
}

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];

    NSLog(@"%s", __func__);
}

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];

    NSLog(@"%s", __func__);
}

运行结果如下

  1. 得出结论,autorelease并不是根据对象的作用域来决定释放时机。
  2. 实际上,autorelease释放对象的依据是Runloop,简单说,runloop就是iOS中的消息循环机制,当一个runloop结束时系统才会一次性清理掉被autorelease处理过的对象,其实本质上说是在本次runloop迭代结束时清理掉被本次迭代期间被放到autorelease pool中的对象的。至于何时runloop结束并没有固定的duration。
  3. 本次runloop迭代休眠之前调用了objc_autoreleasePoolPop()方法,然后调用release,从而释放Person对象。
4.5 方法里有局部对象, 出了方法后会立即释放吗
  • ARC环境下
- (void)viewDidLoad {
    [super viewDidLoad];

    Person *person = [[Person alloc] init];
    NSLog(@"%s", __func__);
}

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];

    NSLog(@"%s", __func__);
}

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];

    NSLog(@"%s", __func__);
}

image

通过打印结果可知,当person对象出了其作用域后就销毁,即系统会在它出作用域的时候,自动调用其release方法。

五、扩展

既然由runloop来决定对象释放时机而不是作用域,那么,在一个{}内使用循环大量创建对象就有可能带来内存上的问题,大量对象会被创建而没有及时释放,这时候就需要靠我们人工的干预autorelease的释放了。

上文有提到autorelease pool,一旦一个对象被autorelease,则该对象会被放到iOS的一个池:autorelease pool,其实这个pool本质上是一个stack,扔到pool中的对象等价于入栈。我们把需要及时释放掉的代码块放入我们生成的autorelease pool中,结束后清空这个自定义的pool,主动地让pool清空掉,从而达到及时释放内存的目的。优化代码如下

@autoreleasePool{
    //domeSomeThing;
}

什么时候用@autoreleasepool

根据 Apple的文档 ,使用场景如下:

  • 写基于命令行的的程序时,就是没有UI框架,如AppKitCocoa框架时。
  • 写循环,循环里面包含了大量临时创建的对象。(本文的例子)
  • 创建了新的线程。(非Cocoa程序创建线程时才需要)
  • 长时间在后台运行的任务。
  1. autorelease 机制基于 UI framework。因此写非UI framework的程序时,需要自己管理对象生存周期。
  2. autorelease 触发时机发生在下一次runloop的时候。因此如何在一个大的循环里不断创建autorelease对象,那么这些对象在下一次runloop回来之前将没有机会被释放,可能会耗尽内存。这种情况下,可以在循环内部显式使用@autoreleasepool {}autorelease对象释放。
  3. 自己创建的线程。Cocoa的应用都会维护自己autoreleasepool。因此,代码里spawn的线程,需要显式添加autoreleasepool。注意:如果是使用POSIX API 创建线程,而不是NSThread,那么不能使用Cocoa,因为Cocoa只能在多线程(multithreading)状态下工作。但可以使用NSThread创建一个马上销毁的线程,使得Cocoa进入multithreading状态。
什么对象会加入Autoreleasepool中
  • 使用allocnewcopymutableCopy的方法进行初始化时,由系统管理对象,在适当的位置release。
  • 使用array会自动将返回值的对象注册到Autoreleasepool
  • __weak修饰的对象,为了保证在引用时不被废弃,会注册到Autoreleasepool中。
  • id的指针对象的指针,在没有显示指定时会被注册到Autoleasepool中。

你可能感兴趣的:(iOS底层 -- 内存管理之autorelase、autoreleasepool原理)