Swift - 使用自动释放池有风险吗? CPU 用法?

Swift - Risk in using autoreleasepool? CPU usage?

使用 Xcode Profiler,我刚刚在 JSON 解码时发现了一个并非真正必要的内存峰值。显然这是一个已知问题,我应该将调用包装在 autoreleasepool 中,这有助于:

//extension..
var jsonData: Data? {
    return autoreleasepool{ try? JSONSerialization.data(withJSONObject: self, options: []) }
}

我发现了另外几大块实际上并不需要的分配,所以我也将我新学到的技巧应用到其他代码中,例如:

var protoArray = [Proto_Bit]()

for bit in data {
    
    autoreleasepool{

        if let str = bit.toJSONString() {
            if let proto = try? Proto_Bit(jsonString: str) {
                protoArray.append(proto)
            }
        }
                    
    }
    
}

现在,在我将我的代码的每条指令(或至少我认为合适的任何地方)包装在这个 autoreleasepool 东西之前,我想问一下它是否有任何相关的风险或缺点。

通过这两个包装,我能够将峰值内存消耗从 500mb 减少到 170mb。我知道 Swift 也在幕后做这些事情,可能有一些警卫,但我宁愿安全也不愿后悔。

Autorelease Pools are a mechanism which comes from Objective-C for helping automate memory management and ensure that objects and resources are released "eventually", where that "eventually" comes when the pool is drained. i.e., an autorelease pool, once created on a thread, captures (retains) all objects which are -autoreleaseed 当池处于活动状态时 — 当池被耗尽时,所有这些对象都会被释放。 (请注意,这是与 Objective-C 运行时结合使用的 Foundation 功能,并没有直接与硬件集成:它是 way 更高级别的方式。)

作为直接管理自动释放池的快捷方式(并避免直接创建 NSAutoreleasePool 实例),Objective-C 引入了 @autoreleasepool 语言关键字,它有效地创建了一个自动释放池作用域的开始,并在结束时将其排出:

@autoreleasepool /* create an autorelease pool to capture autoreleased objects */ {
    // ... do stuff ...
} /* release the autoreleasepool, and all objects that were in it */

以这种方式手动引入自动释放池可以让您更好地控制自动释放对象何时被有效清理:如果您知道一段代码创建了许多自动释放对象,而这些对象实际上并没有需要比那段代码长寿,这可能是包装在 @autoreleasepool.

中的一个很好的候选者

自动释放池早于 ARC,它以一种确定性的方式自动进行引用计数,它的引入使得自动释放池在大多数代码中变得非常不必要:如果一个对象可以确定性地保留和释放,就没有必要依赖“在某个时候”自动释放它。 (事实上​​,除了像 -retain-release 这样的常规内存管理调用本身,ARC 也不允许您直接在对象上调用 -autorelease。)

Swift,遵循 ARC 内存管理模型,也不依赖于自动释放对象——所有对象在最后一次使用后都会确定性地释放。但是:Swift 仍然需要与 Objective-C 代码互操作,值得注意的是, 不是 所有 Objective-C 代码(包括很多代码,例如, Foundation) 使用 ARC。许多内部 Apple 框架仍然使用 Objective-C 的手动内存管理,因此仍然依赖于自动释放的对象。

在 Swift 可能需要与 Objective-C 代码互操作的平台上,不需要 明确地 完成任何工作以允许自动释放的对象最终被释放:Darwin 平台上的每个 Swift 应用程序在进程的根部至少有一个隐式自动释放池,用于捕获自动释放的对象。但是,正如您所注意到的:Objective-C 对象的这种“最终”释放可能会使内存使用率保持在较高水平,直到池被耗尽。为了帮助缓解高内存使用率,Swift 有 autoreleasepool { ... }(匹配 Objective-C 的 @autoreleasepool { ... }),它允许您显式和 急切地 捕获那些自动释放的对象,并在作用域结束时释放它们。

直接回答您的问题,但顺序相反:

  1. 我可以使用 autoreleasepool 弄乱任何东西吗? 对于正确编写的代码,没有。您所做的只是帮助 Objective-C 运行时比其他方式更早地清理这些对象。重要的是要注意:对象只会被池 释放 — 如果在池释放它们后它们的保留计数仍然为正,则它们必须 仍在使用中 某处,并且 不会 被释放,直到持有该对象的其他所有者也释放它们。

    是否有可能引入自动释放池会导致发生一些以前没有的意外行为?绝对地。由于一个对象 偶然地 保持存活足够长的时间以防止意外行为的发生,错误编写的代码可能会意外地工作——并且更快地释放该对象可能会触发它。但是,这既 不太可能 (考虑到 Apple 框架之外实际手动内存管理的数量极少),也不是你可以依赖的东西:如果代码在新引入的 autoreleasepool,一开始就不正确,可能会以其他方式适得其反。

  2. autoreleasepool 是否带有 CPU 开销? 是的,与应用程序执行的实际工作相比,它可能小得微乎其微。但是,这并不意味着到处洒 autoreleasepool 会有用:

    • 鉴于 Swift 项目中自动释放对象的数量随着远离 Objective-C 的代码转换量的增加而减少,越来越少看到需要急切清理的大量自动释放对象向上。您可以到处洒 autoreleasepools,但这些池子完全有可能完全空,没有任何东西需要清理
    • autoreleasepools 不影响原生 Swift 分配:只有 Objective-C 对象可以自动释放,这意味着好的Swift 代码的一部分,autoreleasepool 完全浪费了

所以,什么时候应该使用autoreleasepool

  1. 当您使用来自 Objective-C 的代码时,
  2. 您已 测量 表明由于 autoreleased 个对象,
  3. 您还 测量 通过引入 autoreleasepool
  4. 进行了适当清理

换句话说,正是您在问题中所做的。所以,荣誉。

但是,请尽量避免到处插入 autoreleasepool 的货物崇拜:如果没有实际测量和了解可能发生的情况,它极不可能有效。

[题外话:你怎么知道 objects/code 什么时候可能来自 Objective-C?你不能,很容易。一个好的经验法则是,许多 Apple 框架仍然是在 Objective-C 中编写的,或者可能在某个层 return 一个 Objective-C 对象桥接(或不桥接)到 Swift — 所以他们可能是调查 如果你测量了一些可操作的东西的罪魁祸首。 现在 3rd 方库也不太可能包含 Objective-C,但是你也可以访问他们的源代码以进行确认。]


关于优化和 autoreleasepools 的另一个注意事项:通常,您通常不应期望构建的发布配置在自动释放对象方面的行为与调试配置不同。

不同于 ARC 代码(在 Swift 和 Objective-C 中),其中编译器可以在编译时 为代码插入内存管理优化 ,自动释放池是一个运行时特性,并且由于 any 保留将必然使对象实例保持活动状态,即使将对象单次插入自动释放池中也会使它保持活动状态,直到它被处理 在运行时。因此,即使编译器可以积极优化 Release 配置中大多数对象的保留和释放的特定位置,对于自动释放的对象也没有什么可做的。

(好吧,如果 ARC 优化器对使用该对象的所有代码、自动释放池的上下文有足够的可见性,它可以围绕自动释放对象做 一些 量的优化它属于等等,但这通常是 非常 有限的,因为对象最初 -autoreleased 的范围通常远离自动释放池所在的范围,根据定义[否则它将成为常规内存管理的候选对象]。)