前言

Autorelease->苹果在内存管理方面的优化策略,旨在方便开发者更加良好的管理对象的内存空间,做到OC对象无需开发者手动进行释放操作。
其是在OC从MRC转变为ARC中间形成的产物。

前提

本文的所有内容均已下述标准下进行的实验

从最熟悉的地方开始

main函数,敲过代码的都知道,很多语言都是从main函数开始的(写个Hello,world!,哈哈哈)
OK,那么我们从main函数开始,下面贴的就是main函数在编译前和编译后(反编译出来)的对比

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 原代码
int main(int argc, const char * argv[]) {
@autoreleasepool {
testRetainCount();
}
return 0;
}
// Hopper 反编译后的代码
int _main(int arg0, int arg1) {
void *context = objc_autoreleasePoolPush();
_testRetainCount();
objc_autoreleasePoolPop(context);
return 0;
}

从中,我们可以看到@autoreleasepool{}这个东西实质上是被替换成了以前以后的两个函数

  • objc_autoreleasePoolPush()
  • objc_autoreleasePoolPop(void *)
    OK,在讲这两个函数之前,我们有必要先了解一下这么一个类AutoreleasePoolPage

AutoreleasePoolPage

AutoreleasePoolPage类的简介

AutoreleasePoolPage(自动释放池页?这个翻译好奇怪,我们这边就约定为释放页/page吧)。
那么这个东西是什么呢,它其实是用来管理需要自动释放对象的一个双向链表
OK,我们盗个图,嘻嘻来源
AutoreleasePoolPage类
简单介绍一下元素

  • next:作为一个游标,用来指定下一个会被添加进来的对象的起始地址
  • thread:当前page所在的线程(page和线程是一一对应的关系)
  • child/parent:双向链表的福字节点,没什么好说明的。
  • Size:一个page被宏定义的SIZE限制在了4096字节,所以超过4K的话,就会创建新的childpage来存储
  • depth:链表深度,根链表为0,后续持续+1

OK,看完了图,我们来看下AutoreleasePoolPage的构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 构造函数
AutoreleasePoolPage(AutoreleasePoolPage *newParent)
: magic(), next(begin()), thread(pthread_self()),
parent(newParent), child(nil),
depth(parent ? 1+parent->depth : 0),
hiwat(parent ? parent->hiwat : 0)
{
if (parent) {
parent->check();
assert(!parent->child);
parent->unprotect();
parent->child = this;
parent->protect();
}
protect();
}
// begin函数
id * begin() {
return (id *) ((uint8_t *)this+sizeof(*this));
}

Note: 在C++中,冒号后面是赋值,next(begin())就是next=begin()

最后,在内存中的表现就会如下图所示,来源
AutoreleasePoolPage内存中的表现形式
结合代码和上图,我们可以知道,新的page的next指针被指向begin的位置(实例内存之后)

hotPage()

什么是hotPage呢?
hotPage就是当前内存中最活跃的那一页,被用来存储当前线程快速存储的对象的时候使用的。
获取的方式TLS(Thread Local Storage),这块并不懂,也不想深究,简单明了的理解就是一个全局表,用key来换取value,value就是当前的活跃页。
如有兴趣了解TLS参考大神博客

对象是何时被加入到page中的呢?

实验

在开始讲对象是怎么被加入页中,我们从一个实验中来看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void * -[Person person](void * self, void * _cmd) {
var_18 = [[Person alloc] init];
var_28 = [var_18 retain];
objc_storeStrong(var_18, 0x0);
rax = [var_28 autorelease];
return rax;
}
- (Person *)person {
Person *person = [[Person alloc] init];
return person;
}
void -[Person person1](void * self, void * _cmd) {
rdi = var_18;
[[Person alloc] init];
objc_storeStrong(rdi, 0x0);
return;
}
- (void)person1 {
Person *person = [[Person alloc] init];
}

解释

对比两个函数person和person1函数,从反编译器的代码中我们可以看出来,对于没有超出作用于范围函数的对象,是不会被加入到autoReleasePool中的,但是如果会超出了函数作用于范围的值,那么编译器会帮助你主动加上autoRelease这个函数的。
那么我们来看一下autoRelease的函数调用关系

  • [self autorelease]
    • ((id)self)->rootAutorelease();
      • rootAutorelease2();
        • AutoreleasePoolPage::autorelease((id)this); // this就是oc中的self
          • autoreleaseFast(obj) // obj就是传入的self
            • page->add(obj)
              1
              2
              3
              4
              5
              6
              7
              8
              9
              10
              11
              12
              13
              14
              15
              16
              17
              18
              19
              20
              21
              22
              23
              static inline id *autoreleaseFast(id obj)
              {
              // 线程安全的,找到当前页
              // hotPage:以全局的key(static pthread_key_t const key = AUTORELEASE_POOL_KEY;)存储的一份实例,动态更新双向页链表中最后一个节点
              // 都是同一个套路
              // 1. 存在页 跳转3
              // 2. 不存在页 创建页 跳转5
              // 3. 页是否满了 跳转5
              // 4. 添加obj 跳转6
              // 5. 设置hotPage 跳转4
              // 6. 结束
              AutoreleasePoolPage *page = hotPage(); // 找到当前活跃的PoolPage
              if (page && !page->full()) {
              // 存在页, 未满 -> 添加
              return page->add(obj);
              } else if (page) {
              // 存在页, 已满 -> 新建后添加
              return autoreleaseFullPage(obj, page);
              } else {
              // 不存在页 -> 新建页后添加
              return autoreleaseNoPage(obj);
              }
              }

其实add操作就是将obj的指针加入page的next的位置,然后next位置上移

1
2
3
4
5
6
7
8
9
id *add(id obj)
{
assert(!full());
unprotect();
id *ret = next; // faster than `return next-1` because of aliasing
*next++ = obj;
protect();
return ret;
}

objc_storeStrong增加引用计数的时候使用,但是这边是成员变量,并不会引起引用计数的增加

对象又是什么时候被释放的呢?

至此,我们已经了解到了,对象是如何被加入到page中的,接下来,我们要了解,对象的释放时机

释放

OK,那么我们回到文中最开始的那两个函数
objc_autoreleasePoolPush()
objc_autoreleasePoolPop(content)
每次调用objc_autoreleasePoolPush其实都是在给hotPage的栈顶添加一个哨兵对象,值为0,那么就会变成这个样子
(继续盗图来源
哨兵对象
那么objc_autoreleasePoolPush()的返回值就是这个哨兵对象的地址,后续可以成为objc_autoreleasePoolPop的入参进行使用,于是乎就有如下结论

  • 根据传入的哨兵对象地址,找到对应的page页
  • 根据page页和哨兵地址,将在哨兵对象添加时间后的对象全部调用release函数,并重置next指针

autoRelease和runloop的关系

这块儿没有仔细的研究,抄一抄别人写的描述,来源
我们总是看到有文章说程序启动后,苹果在主线程 RunLoop 里注册了两个 Observer:
第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。
第二个 Observer 监视了两个事件: BeforeWaiting(准备进入睡眠) 和 Exit(即将退出Loop),
BeforeWaiting(准备进入睡眠)时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;
Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。
打印出MainRunLoop,可以看到MainRunLoop的 Common mode Items 中就有这两个观察者
IMAGE

杂项

保证线程访问安全?
Linux中mprotect()函数的用法

1
2
protect();
unprotect();

objc_storeStrong的研究

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void objc_storeStrong(id *location, id obj)
{
id prev = *location;
if (obj == prev) {
return;
}
objc_retain(obj);
*location = obj;
objc_release(prev);
}
// 真正的set方法,是不会直接return掉的
objc_storeStrong(0x0, rax);
// 临时变量,会直接在内部return掉
objc_storeStrong(var_8, 0x0);

参考链接

  1. 黑幕背后的Autorelease
  2. RunLoop总结:RunLoop 与GCD 、Autorelease Pool之间的关系