iOS - AutoreleasePool - 基础篇

[toc]

参考

AutoreleasePool - base

https://www.cnblogs.com/XXxiaotaiyang/p/5118737.html

https://www.jianshu.com/p/dfec601d84da

https://www.jianshu.com/p/b0c19505a5a4

https://www.jianshu.com/p/50bdd8438857

https://blog.csdn.net/mr_xiaojie/article/details/52953807

https://www.jianshu.com/p/32265cbb2a26

http://blog.sunnyxx.com/2014/10/15/behind-autorelease/

简介

AutoreleasePool 是OC中的一种内存自动回收机制, 它可以 延迟 加入 AutoreleasePool 中的变量的 release 时机。

在正常情况下, 创建的变量会在超出其作用域的时候 release, 但是如果将变量加入 AutoreleasePool , 那么 release 将延迟执行。

autorelease 是 ARC 进行引用计数管理的一个机制。

autorelease 本质是把 release 延迟到 autoreleasepool drain 的时候, 延迟内存的释放。

autorelease 和作用域没有任何关系。

NSAutoreleasePool 是什么?
NSAutoreleasePool 实际上是个对象引用计数自动处理器, 是一个继承于NSObject的类。
OC对象, 全部继承自NSObject, 使用引用计数的方法来管理对象的存活, 众所周知, 当引用计数为0时, 对象就被销毁了。

操作非常简单, 当对象被创建时, 引用计数被设成1。可以给对象发送retain消息, 让对象对自己的引用计数加1。
而当对象接受到release消息时, 对象就会对自己的引用计数进行减1, 当引用计数到了0, 对象就会调用自己的dealloc处理。

当对象被加入到 AutoreleasePool 中, 会对其对象retain一次, 当 AutoreleasePool 结束时, 会对其所有对象发送一次release消息。

AutoreleasePool 可以同时有多个, 它的组织是个栈, 总是存在一个栈顶pool, 也就是当前pool;

每创建一个pool, 就往栈里压一个, 改变当前pool为新建的pool; 每次给pool发送drain消息, 就弹出栈顶的pool, 改当前pool为栈里的下一个pool。

设计场景

A retain 了一个对象, A有职责 release 它, 但是现在不能release, 因为A知道后续代码可能想要 retain 它。

定义一个函数, 返回对象A, 方法内部 retain 了A, 意味着我们需要 release A, 来维持A引用计数的平衡;

但由于A是函数返回值, 我们要确保函数调用方拿到的对象A是没被回收的, 所以不能在函数返回前 release。

也就是说, 我们需要 release 返回值, 但又不能在函数返回前。

有两个选择, 要么延后 release, 要么函数调用方帮我们 release。

这分别对应着解决函数返回值引用计数问题的两种方式:

① autorelease, 即延迟release, 函数返回前不进行 release, 先把返回值暂存在 autoreleasepool 中一段时间。
这段时间内, 调用方如果需要, 可以 retain 这个返回值, 等到 autorelease pool 干(drain)的时候, 再去 release 这个对象, 平衡对象引用计数, 适用于除 alloc、copy、new、mutableCopy 之外的函数返回值。

② 函数调用方负责 release 函数返回值, 函数和函数调用方配合维护引用计数, 适用于 alloc、copy、new、mutableCopy 之类的函数。

向对象发送 autorelease 消息, 会发生什么?

将该对象加入到当前 AutoreleasePoolPage 的 栈顶 next 指针 指向的位置。

autoreleased 对象的释放时机? ★

加入到 autoreleasepool 中的对象, 是什么时候被释放的?

未手加 AutoreleasePool 的情况下, Autoreleased 对象是在 当前的 runloop 迭代结束时释放的, 而它能够释放的原因是, 系统在每个 runloop 迭代中都加入了自动释放池Push和Pop。

autoreleasepool 销毁时, 会对 autoreleasepool 里面的所有对象做一次 release 操作。

autoreleasepool 销毁时, 在调用栈中可以发现, 系统调用了 [NSAutoreleasePool release] 方法, 这个方法最终通过调用 AutoreleasePoolPage::pop(void *) 函数来负责对 autoreleasepool 中的 autoreleased 对象执行 release 操作。

虽然 autoreleased 对象并不都是在代码块结束后就释放。但是他们有一个共同特性: 必定是在 @autoreleasepool 被销毁时释放。
所以要清楚 autoreleased 对象什么时候被释放, 只需要搞清楚 @autoreleasepool 什么时候被销毁即可。

在 ARC 下, 在线程中的临时对象, 是在当前线程的 Runloop 进入休眠 或者 退出loop 或者 退出线程时被执行release的。

main() 的 pool
// main() 添加的 autoreleasepool, 在程序退出时才会销毁
// 项目中调用了 autorelease 的对象, 并不是被 main() 函数里添加的 autoreleasepool 管理的
int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}
手动添加的 pool
// MRC 下才能主动调用autorelease
@implementation Person
- (void)dealloc {
    NSLog(@"%s", __func__);
}
@end
    
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"1");
    // 手动添加的pool, 对象 release 时机就是 `}` 结束 ★
    @autoreleasepool {
        Person *person = [[[Person alloc] init] autorelease];
    } 
    NSLog(@"2");
}
@end

输出
1
-[Person dealloc]
3
runloop 管理的 pool ★
// MRC 
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 这个 person 什么时候调用 release, 是由 RunLoop 来控制的
    // 它可能是在某次RunLoop循环中, RunLoop休眠之前调用了release
    // 会在他所处的那一次 runloop 休眠之前, 被调用release ★
    Person *person = [[[Person 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__);
}
@end

输出:

-[ViewController viewDidLoad]
-[ViewController viewWillAppear:]
-[Person dealloc]
-[ViewController viewDidAppear:]

可见, viewDidLoad 和 viewWillAppear 是处在同一次 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
};

- (void)viewDidLoad {
    [super viewDidLoad];
    // 打印出当前线程runloop的信息
    NSLog(@"%@", NSRunLoop.currentRunLoop);
}

// 输出可见, UITrackingRunLoopMode 和 kCFRunLoopDefaultMode 所注册的observer是完全一样的, 说明observer是注册到runloop上的, 所有mode共用
// 从输出内容中找到 与 AutoreleasePool 相关的 CFRunLoopObserver

// activities = 0x01 // kCFRunLoopEntry
<CFRunLoopObserver 0x60000118c140 [0x7fff8062d610]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff48c84b28), context = <CFArray 0x600002ecc390 [0x7fff8062d610]>{type = mutable-small, count = 1, values = (\n\t0 : <0x7fa6ca00a048>\n)}}

// activities = 0xa0 // kCFRunLoopBeforeWaiting | kCFRunLoopExit (160 = 32 + 128)
<CFRunLoopObserver 0x60000118c1e0 [0x7fff8062d610]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x7fff48c84b28), context = <CFArray 0x600002ecc390 [0x7fff8062d610]>{type = mutable-small, count = 1, values = (\n\t0 : <0x7fa6ca00a048>\n)}}
结论 ★

iOS 在主线程的 Runloop 中注册了2个 AutoreleasePool 相关的 Observer, 其回调都是 _wrapRunLoopWithAutoreleasePoolHandler

  • 第1个 observer

  • 监听了 kCFRunLoopEntry事件,进入时会调用objc_autoreleasePoolPush()

  • 第2个 observer

    • 监听了 kCFRunLoopBeforeWaiting 事件, 休眠之前会先后调用 objc_autoreleasePoolPop()objc_autoreleasePoolPush()

      所以休眠之前会将在这之前加入 pool 的对象, 统一调用一次 release

    • 监听了 kCFRunLoopExit 事件, 退出时会调用 objc_autoreleasePoolPop()

这保证了push() 和 pop() 是成对出现的

<u>这个结论对于其他线程, 应该也是适用的。</u>

autoreleasepool 释放时机?

MRC 下显式的调用 drain 方法。
② 在 runloop 开始时, 都会隐式创建一个 autoreleasepool, 并会在 runloop 退出时, 把前面创建的 autoreleasepool drain。

释放顺序

主线程中既有系统创建的 @autoreleasepool, 也有开发者手动创建的 @autoreleasepool。那么他的释放顺序是怎样的呢?

因为 @autoreleasepool 是以的形式存储的, 按照先进后出的规则, 释放栈中每个@autoreleasepool。

系统创建的 pool 是在 Runloop 一开始创建的, 所以它必然是栈底的;

手动创建的 pool 是在 Runloop 运行中创建的, 所以在系统的 pool 上面;

按照栈的规则, @autoreleasepool 是先释放自行创建的 pool, 再释放系统创建的。

与 线程 和 runloop 的关系:

有没有想过我们直接调用 autorelease 方法就可以把释放对象的任务交给 Autoreleasepool 对象, Autoreleasepool 对象从哪里来?

Autoreleasepool 对象又会在何时调用 [pool drain] 方法?

每一个线程, 包括主线程, 都会拥有一个专属的 NSRunLoop 对象, 并且会在有需要的时候自动创建。

线程 与 Runloop 是一对一关系, 主线程中会自动创建 Runloop, 而子线程需要手动获取。
子线程的 runloop 需要自己手动获取, 如果子线程的 runloop 没有任何事件, runloop会马上退出。

在每个 loop 开始前, 系统会自动创建一个 autoreleasepool , 并在 loop 结束时 drain 。

NSAutoreleasePool 中还提到, 每一个线程都会维护自己的 autoreleasepool 堆栈。

换句话说 autoreleasepool 是与线程紧密相关的, 每一个 autoreleasepool 只对应一个线程。(一对多???)

系统创建的 pool 和 RunLoop 的关系

见<>

在主线程执行的代码, 通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着, 所以不会出现内存泄漏, 开发者也不必显式创建 Pool 了。

也就是说 AutoreleasePool 创建是在一个RunLoop事件开始之前(push), AutoreleasePool释放是在一个RunLoop事件即将结束之前(pop)。

AutoreleasePool 里的 Autorelease 对象的加入是在RunLoop事件中,

AutoreleasePool 里的 Autorelease 对象的释放是在 AutoreleasePool 释放时。

手动创建的 @autoreleasepool

@autoreleasepool { // 这个 `{` 开始, 创建自动释放池, 内部的对象自动加入autorelease

} // 这个 `}` 开始, 自动释放池被销毁。

子线程上的 autoreleasepool? ★

子线程默认不会开启 Runloop, 对象调用 autorelease 如何处理? 不手动处理会内存泄漏吗?

① 若当前线程已经创建了 Pool , Autoreleased 对象就会交给 pool 去管理。

② 若当前线程没有pool, 代码调用顺序为: autorelease -> autoreleaseFast -> autoreleaseNoPage

autoreleaseNoPage 方法中, 会创建一个hotPage, 然后调用 page->add(obj) 将对象添加到 AutoreleasePoolPage 的栈中。

也就是说即使当前线程没有pool (没有开启runloop), 对象调用autorelease时, 也会 new 一个 AutoreleasepoolPage 出来管理autorelease对象, 不用担心内存泄漏。

<u>也就是说, 不一定要runloop开启, 才能创建pool</u>

这个是 OS X 10.9+ 和 iOS 7+ 才加入的特性。并且苹果没有对应的官方文档阐述此事, 但是你可以通过源码了解。

子线程的 AutoreleasepoolPage 和主线程的 page 有关联吗?

没有, 释放池和 线程是一一对应的

实现原理 ★

见<autoreleasepool 源码分析>


  1. 先建立一个 autorelease pool;

  2. 对象从这个 autorelease pool 里面生成;

  3. 对象生成之后调用 autorelease 函数, 这个函数的作用仅仅是在 autorelease pool 中做个标记, 让 pool 记得将来 release 一下这个对象;

  4. 当 pool 要把池中的对象 release 时, pool本身也需要rerlease, 此时 pool 会把每一个标记为 autorelease 的对象 release 一次;

但, 如果某个对象此时 retain count 大于1, 这个对象还是没有被销毁;

被标为autorelease的对象, 并不是等程序结束时才release, 如:

在 viewDidLoad 中创建一个 autorelease 对象, 在 viewDidLoad 走完, 一次消息循环完毕, 这个 autorelease pool 中的对象就会被 release;

button 的点击事件, 点击代码执行完会release一下autorelease类型的变量;

上面这个例子应该这样写:

ClassName *myName = [[[ClassName alloc] init] autorelease]; // 标记为autorelease

[classA setName:myName]; // retain count == 2 (如果myName的属性是retain的话)

[myName release]; // retain count == 1, 注意, 在ClassA的dealloc中不能release name, 否则release pool时会release这个retain count为0的对象, 这是不对的。

记住一点:

如果这个对象是你alloc或者new出来的, 你就需要调用release。如果使用autorelease, 那么仅在发生过retain的时候release一次(让retain count始终为1)。

Autoreleasepool 对象从哪里来?

对于每一个Runloop运行循环, 系统会隐式创建一个 Autoreleasepool 对象,

+ (instancetype)student; 中执行autorelease的操作, 就会将student对象添加到这个系统隐式创建的Autoreleasepool 中

Autoreleasepool 对象又会在何时调用 [pool drain] 方法?

当Runloop执行完一系列动作没有更多事情要它做时, 它会进入休眠状态, 避免一直占用大量系统资源, 或者Runloop要退出时, 会触发执行_objc_autoreleasePoolPop()方法, 相当于让 Autoreleasepool 对象执行一次 drain 方法,

Autoreleasepool 对象会对自动释放池中所有的对象依次执行依次release操作


为什么对象在被释放前, 打印出来的retainCount为1而不为0?

当对象最后一次执行release时, 系统知道马上就要回收内存了, 就没有必要再将retainCount减1了, 因为不管减不减1, 该对象都肯定会被回收, 而对象被回收后, 它的所有的内存区域, 包括retainCount值也变得没有意义。不将这个值从1变成0, 可以减少一次内存的操作, 加速对象的回收。

每个线程只有一个 autoreleasepool 吗?

可以手动添加 局部释放池

一个线程有几个 autoreleasepool栈?

autoreleasepool栈 的说法应该是 autoreleasepoolPage, 一个autoreleasepool 可以有多个page

enumerateObjectsUsingBlock

使用容器的block版本的枚举器时, 内部会自动添加一个AutoreleasePool:

[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
    // 这里被一个局部 @autoreleasepool 包围着
}];

当然, 在普通for循环和for in循环中没有, 所以, 还是新版的block版本枚举器更加方便。for循环中遍历产生大量autorelease变量时, 就需要手加局部AutoreleasePool咯。

当一个 runloop 在不停的循环工作, 那么runloop每一次循环必定会经过BeforeWaiting(准备进入休眠):而去BeforeWaiting(准备进入休眠) 时调用 _objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧池并创建新池, 那么这两个方法来销毁要释放的对象, 所以我们根本不需要担心Autorelease的内存管理问题, 这就是ARC背后的“高人”。

当对象调用 autorelease 方法时, 会将对象加入 AutoreleasePoolPage 的栈中

调用 AutoreleasePoolPage::pop 方法会向栈中的对象发送 release 消息

什么对象会入池? ★

被加入 autoreleasepool 的对象, 怎样进行内存管理?

对象被加入到离该对象最近的 autoreleasepool 中, 只有当这个 pool drain 时, pool 中的 autoreleased 对象才会被 release ( 当retainCount = 0 时对象才被释放)。

未被加入 autoreleasepool 的对象, 怎样进行内存管理?

未被加入 autoreleasepool 的对象, 都是由系统管理, 根据引用计数来控制释放的时机, 在适当的位置 release。

什么样的对象会交给自动释放池管理? ★
  • MRC下, 对象调用 autorelease 方法;

  • ARC下, 给对象添加 __autoreleasing 修饰符;

  • 非自己创建的对象: 一般是系统提供的类方法 / 工厂方法, 创建的对象如 [NSArray array];

  • 函数返回值: 对象作为方法的返回值时, 也会被附加上 __autoreleasing 修饰符; (其实这就是非自己创建的对象)

什么是自己创建的对象?

自己创建的对象: 使用 alloc、new、copy、mutablecopy 及其驼峰变形 allocObject、newObject、copyObject、mutablecopyObject 的方法创建的对象;

自己创建的对象, 遵循 "谁创建, 谁释放, 谁引用, 谁管理" 的内存管理原则, 在用完之后直接release

拿NSArray来举例, 什么情况会入池?
  • 使用 [[NSArray alloc] init]; 创建的对象, 不会入池。

  • 使用 [[[NSArray alloc] init] autorelease]; (MRC下)调用了autorelease, 入池。

  • 使用 [NSArray array]; 即调用 Foundation 的类方法/工厂方法创建出来的对象, 入池。


id 的指针或对象的指针, 在没有显式指定修饰符时, 会被默认加上 __autoreleasing 修饰符, 注册到 autoreleasepool 中。

__weak对象

__weak 修饰的对象, 为了保证在引用时不被废弃, 会注册到 autoreleasepool 中。这是苹果之前的实现

现在 ARC 不会 autorelease 弱引用对象, 而是直接 release

__weak 保证了弱指针使用期间, 状态的一致性。LLVM8的最新实现是, 不推迟release到autoreleasepool, 而是直接release。

https://stackoverflow.com/questions/40993809/why-weak-object-will-be-added-to-autorelease-pool

In conclusion, this design of __weak ensure that during the usage of weak pointer, its state is consistent. The new implmenetation of __weak of Apple LLVM version 8.0.0 (clang-800.0.42.1) do not postponed the release to autoreleasepool, but use objc_release directly.


方法里有局部对象, 出了方法作用域会立刻释放吗?

答: 会立刻释放

// ARC
@implementation Person
- (void)dealloc {
    NSLog(@"%s", __func__);
}
@end
    
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    Person *person = [[Person alloc] init];
}

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    NSLog(@"%s", __func__);
}
@end

输出:
-[ViewController viewDidLoad]
-[Person dealloc]
-[ViewController viewWillAppear:]
    
可见 viewDidLoad 执行完, 局部变量就 dealloc 了;
猜测 ARC 是在局部变量作用域结束之前, 插入了一句 release
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,524评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,869评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,813评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,210评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,085评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,117评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,533评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,219评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,487评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,582评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,362评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,218评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,589评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,899评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,176评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,503评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,707评论 2 335