Objective-C Autorelease Pool 的实现原理

本文转自:http://www.jianshu.com/p/d6687291e486

内存管理一直是学习Objectie-C的重点和难点之一,尽管现在已经是ARC时代了,但是了解Objective-C的内存管理机制仍然是十分必要的。其中,弄清楚autorelease的原理更是重中之重,只有理解了autorelease的原理,我们才算是真正了解了Objective-C的内存管理机制。

autoreleased对象什么时候释放

autoreleased 本质上就是延迟调用release

AutoreleasePoolPage

-[NSAutoreleasePool release]方法最终是通过调用AutoreleasePoolPage:pop(void*)函数来负责对autoreleasepool中的autoreleased对象执行release操作的。

那这里的AutoreleasePoolPage到底是什么东西呢?其实,autoreleasepool是没有单独的内存结构的,它是通过以AutoreleasePoolPage为结点的双向链表来实现的。我们打开runtime的源码工程,在NSObject.mm文件的第438-932行可以找到autoreleasepool的实现源码。通过阅读源码,我们可以知道:


NSObject.mm源码备注图

每一个线程的autoreleasepool其实就是一个指针的堆栈

每一个指针代表一个需要release的对象或者POOL_SENTINEL是一个autorelease pool 边界.(此处翻译如有问题可留言讨论你认为对的翻译.)

一个pool token就是这个pool所对应的POOL_SENTINEL的内存地址。当这个pool被popped,所有的内存地址在pool token之后的对象都会被release;

Thread-local storage (线程局部存储)指向hot page,即最新添加的autoreleased对象所在的那个page.



1.magic_t用来校验AutoreleasePoolPage的结构是否完整;

2.next指向最新添加的autoreleased对象的下一个位置,初始化时指向begin();

3.thread指向当前线程;

4.parent指向父结点,第一个结点的parent值为nil;

5.child指向子结点,最后一个结点的child值为nil;

6.depth代表深度,从0开始,往后递增1;

7.hiwat代表high water mark;

另外,当next == begin()时,表示AutoreleasePoolPage为空;当next==end()时,表示AutoreleasePoolPage已满。

Autorelease Pool Blocks

我们使用 clang-rewrite-objc命令将下面的Objective-C代码重写成C++代码:



将会得到以下输出结果



不得不说,苹果对@autoreleasepool {}的实现真的是非常巧妙,真正可以称得上是代码的艺术。苹果通过声明一个__AtAutoreleasePool类型的局部变量__autoreleasepool来实现@autoreleasepool {}。当声明__autoreleasepool变量时,构造函数__AtAutoreleasePool()被调用,即执行atautoreleasepoolobj = objc_autoreleasePoolPush();;当出了当前作用域时,析构函数~__AtAutoreleasePool()被调用,即执行objc_autoreleasePoolPop(atautoreleasepoolobj);。也就是说@autoreleasepool {}的实现代码可以进一步简化如下:



因此,单个autoreleasepool的运行过程可以简单地理解为objc_autoreleasePoolPush()、[对像 autorelease]和objc_autoreleasePoolPop(void*)三个过程。

push操作

上面提到的objc_autoreleasePoolPush()函数本质上就是调用的 AutoreleasePoolPage 的 push 函数。

void*

objc_autoreleasePoolPush(void)

{

       if(UseGC)returnnil;

       returnAutoreleasePoolPage::push();

}

因此,我们接下来看看 AutoreleasePoolPage 的 push 函数的作用和执行过程。一个 push 操作其实就是创建一个新的 autoreleasepool ,对应 AutoreleasePoolPage 的具体实现就是往 AutoreleasePoolPage 中的next位置插入一个 POOL_SENTINEL ,并且返回插入的 POOL_SENTINEL 的内存地址。这个地址也就是我们前面提到的 pool token ,在执行 pop 操作的时候作为函数的入参。

staticinlinevoid*push()

{

       id*dest=autoreleaseFast(POOL_SENTINEL);

        assert(*dest==POOL_SENTINEL);

        return dest;

}

push 函数通过调用autoreleaseFast函数来执行具体的插入操作。

staticinlineid*autoreleaseFast(idobj)

{

AutoreleasePoolPage*page=hotPage();

if(page&&!page->full()) {

returnpage->add(obj);

}elseif(page) {

returnautoreleaseFullPage(obj,page);

}else{

returnautoreleaseNoPage(obj);

}

}

autoreleaseFast函数在执行一个具体的插入操作时,分别对三种情况进行了不同的处理:

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

2.当前 page 存在且已满时,创建一个新的 page ,并将对象添加到新创建的 page 中;

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

每调用一次 push 操作就会创建一个新的 autoreleasepool ,即往 AutoreleasePoolPage 中插入一个 POOL_SENTINEL ,并且返回插入的 POOL_SENTINEL 的内存地址。

autorelease 操作

通过NSObject.mm源文件,我们可以找到-autorelease方法的实现

- (id)autorelease{

      return((id)self)->rootAutorelease();

}

通过查看((id)self)->rootAutorelease()的方法调用,我们发现最终调用的就是 AutoreleasePoolPage 的autorelease函数。

__attribute__((noinline,used))

id

objc_object::rootAutorelease2()

{

assert(!isTaggedPointer());

returnAutoreleasePoolPage::autorelease((id)this);

}

AutoreleasePoolPage 的autorelease函数的实现对我们来说就比较容量理解了,它跟 push 操作的实现非常相似。只不过 push 操作插入的是一个 POOL_SENTINEL ,而 autorelease 操作插入的是一个具体的 autoreleased 对象。

staticinlineidautorelease(idobj)

{

assert(obj);

assert(!obj->isTaggedPointer());

id*dest__unused=autoreleaseFast(obj);

assert(!dest||*dest==obj);

returnobj;

}

pop 操作

同理,前面提到的objc_autoreleasePoolPop(void *)函数本质上也是调用的 AutoreleasePoolPage 的pop函数。

void

objc_autoreleasePoolPop(void*ctxt)

{

if(UseGC)return;

// fixme rdar://9167170

if(!ctxt)return;

AutoreleasePoolPage::pop(ctxt);

}

pop 函数的入参就是 push 函数的返回值,也就是 POOL_SENTINEL 的内存地址,即 pool token 。当执行 pop 操作时,内存地址在 pool token 之后的所有 autoreleased 对象都会被 release 。直到 pool token 所在 page 的next指向 pool token 为止。

下面是某个线程的 autoreleasepool 堆栈的内存结构图,在这个 autoreleasepool 堆栈中总共有两个 POOL_SENTINEL ,即有两个 autoreleasepool 。该堆栈由三个 AutoreleasePoolPage 结点组成,第一个 AutoreleasePoolPage 结点为coldPage(),最后一个 AutoreleasePoolPage 结点为hotPage()。其中,前两个结点已经满了,最后一个结点中保存了最新添加的 autoreleased 对象objr3的内存地址。



此时,如果执行pop(token1)操作,那么该 autoreleasepool 堆栈的内存结构将会变成如下图所示:






NSThread、NSRunLoop和NSAutoreleasePool

根据苹果官方文档中对NSRunLoop的描述,我们可以知道每一个线程,包括主线程,都会拥有一个专属的NSRunLoop对象,并且会在有需要的时候自动创建。(注:NSRunLoop的本质是一个消息机制的处理模式)

苹果官方文档中对NSRunLoop的描述


The NSRunLoop class declares the programmatic interface to objects that manage input sources. An NSRunLoop object processes input for sources such as mouse and keyboard events from the window system, NSPort objects, and NSConnection objects. An NSRunLoop object also processes NSTimer events.

NSRunLoop类声明的编程接口对象管理输入源.一个NSRunLoop对象进程的输入源像一个窗口系统输入鼠标和键盘,NSPort 对象,和NSConnection对象。NSTimre事件发生时伴随着一个NSRunLoop对象被创建。

Your application cannot either create or explicitly manage NSRunLoop objects. Each NSThread object, including the application’s main thread, has an NSRunLoop object automatically created for it as needed. If you need to access the current thread’s run loop, you do so with the class method currentRunLoop.

您的应用不能创建或显示管理NSRunLoop对象。每一个线程,包括主线程,都会拥有一个专属的NSRunLoop对象,并且会在有需要的时候自动创建。


WARNING

警告⚠️

The NSRunLoop class is generally not considered to be thread-safe and its methods should only be called within the context of the current thread. You should never try to call the methods of an NSRunLoop object running in a different thread, as doing so might cause unexpected results.

NSRunLoop类一般不被认为是线程安全的,其方法应该只被为当前线程的上下文中。你不应该试图去访问NSRunLoop对象在不同的线程中,这样做可能会导致意想不到的结果.


同样的,根据苹果官方文档中对NSAutoreleasePool的描述,我们可知,在主线程的NSRunLoop对象(在系统级别的其它线程中应该也是如此,比如通过dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0)) 的每个event loop开始前,系统会自动创建一个autoreleasepool,并在event loop结束时drain。

 苹果官方文档中对NSAutoreleasePool的描述

The NSAutoreleasePool class is used to support Cocoa is reference-counted memory management system.An autorelease pool stores objects that are sent a release message when the pool itself is drained.

NSAutoreleasePool 类是用于支持Cocoa采用引用计数的内存管理系统. 发送一个release时 autorelease pool池将会被排干.

另外,NSAutoreleasepool 中提到,每一个线程都会维护自己的autoreleasepool堆栈。换句话说autoreleasepool是与线程紧密相关的,每一个autoreleasepool只对应一个线程。

Each thread(including the main thread) maintains its own stack of NSAutoreleasePool objects.

每个线程都只对应它自己的NSAutoreleasepool对象(包括主线程)

弄清楚NSThread、NSRunLoop和NSAutoreleasePool三者的关系可以帮助我们从整体上了解Objective-C的内存管理机制,清楚系统在背后到底为我们做了什么,理解整个运行机制等。

总结

我们到这里,相信应该对Object-C的内存管理机制有了更进一步的认识。通常情况下,我们不需要手动添加autoreleasepool 的,使用线程自动维护的autoreleasepool就好了。根据苹果官方文档中对Using Autorelease Pool Blocks的描述,我们知道在下面三种情况下是需要我们手动添加autoreleasepool的:

1.If you are writing a program that is not based on a UI framework, such as a command-line tool.

如果你编写的程序不是基于UI框架的,比如说命令行工具.

2.If you write a loop that creates many temporary objects.

You may use an autorelease pool block inside the loop to dispose of those objects before the next iteration. Using an autorelease pool block in the loop helps to reduce the maximum memory footprint of the application.

如果你编写的循环中创建了大量的临时对象

3.If you spawn a secondary thread.

You must create your own autorelease pool block as soon as the thread begins executing; otherwise, your application will leak objects. (See Autorelease Pool Blocks and Threads for details.)

如果你创建了一个辅助线程.

(有问题可留言.没事多看下英文文档 一手的资料总是比较全面的,别人的文档都是自己吸收后写出来的对于自己是有帮助的,但也不可能面面具到.)



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值