Nib 的内存管理

Mike Ash Friday Q&A 中文译文:Nib 的内存管理

作者 TommyWu
封面圖片: Nib 的内存管理

译文 · 原文: Friday Q&A 2012-04-13: Nib Memory Management · 作者 Mike Ash

原文:https://www.mikeash.com/pyblog/friday-qa-2012-04-13-nib-memory-management.html 发布:2012-04-13 作者:Mike Ash 译者:MiMo(mimo-v2.5-pro);代码块保留英文原样


我从休整期回归,准备好开启一段新的苹果平台底层世界之旅。今天的主题源自多位读者建议,他们希望我探讨处理内存管理与 nib 文件时的微妙细节,特别是 Mac 与 iOS 平台之间的差异。

Nib 加载概述

当加载一个 nib 文件时,会按顺序发生两个重要步骤。首先,加载器(loader)会实例化 nib 中的所有对象。其次,它会连接 nib 中指定的所有输出口(outlets)。

就内存管理而言,有两个相关领域。第一个是如何正确管理输出口。第二个是如何管理 nib 中的顶层对象(top-level objects)。nib 包含一个对象层次结构,其中每个对象由其父对象所有,但该层次结构顶层的对象则是一个特例。

Mac 端的 Nib 加载

让我们谈谈在 Mac 上 nib 加载的工作原理,就上述两个相关的内存管理领域而言。

顶级对象使用 alloc(分配内存)和 init(初始化)进行实例化(或使用特定于类的初始化方法,如 NSWindow 实例的 -initWithContentRect:styleMask:backing:defer:)。然后它们就这样被留下,最终释放它们的责任隐式地转移给了 File’s Owner(文件所有者)对象。如果你使用 NSWindowController 或 NSViewController 来加载 nib 文件,它会自动获取这些对象的所有权,并在控制器被销毁时释放它们。

要设置一个 outlet(输出口),nib 加载器首先搜索 setter 方法。如果 outlet 被命名为 foo,加载器会搜索一个名为 setFoo: 的方法。如果这样的方法存在,加载器会调用它,并将 outlet 的值作为参数传递。

如果这样的方法不存在,加载器会搜索一个与 outlet 同名的实例变量(instance variable)。如果找到了这样的实例变量,加载器会直接将其值设置为 outlet 的值,而不执行任何内存管理。

最后,如果既找不到方法也找不到实例变量,outlet 连接失败,outlet 不会被设置。

iOS Nib 加载

现在让我们来谈谈在 iOS 上,Nib 加载(Nib loading)是如何工作的。总体上它非常相似,但存在一些细微的差异。不必试图找出所有的差异,因为我会在后文中指出并分析它们。

顶层对象(Top level objects)是通过 allocinit(或特定类的初始化方法)来实例化的,然后被自动释放(autoreleased)。如果没有其他东西持有(retain)它们,这些对象将会被自动销毁。

为了设置一个 outlet(出口),Nib 加载器会调用 -setValue:forKey: 方法,传入 outlet 的值和 outlet 的名称。随后,KVC(键值编码)机制接管并搜索设置该特定键的方式。对于一个名为 foo 的 outlet,它会首先搜索名为 setFoo: 的方法。如果存在这样的方法,它就会调用该方法,并将 outlet 的值作为参数传入。

如果不存在这样的方法,KVC 机制会搜索名为 _foo_isFoofooisFoo 的实例变量(instance variable)。如果其中任何一个存在,它将把找到的第一个实例变量设置为 outlet 的值,释放实例变量中的旧值(如果有),并持有(retain)新值。

如果既没有找到方法,也没有找到实例变量(instance variable),KVC(键值编码)会调用 setValue:forUndefinedKey:。默认情况下,这会引发一个异常,并且可以重写该方法来为未知键实现自定义行为。

差异

这两个系统相似但不完全相同。这些差异源于 iOS 更为现代的设计理念。毫无例外,凡是两个系统存在差异的地方,iOS 的方式都更为合理。遗憾的是,Mac 的方式无法在不严重破坏向后兼容性的情况下被更改。这些差异包括:

  • 在 Mac 上,顶层对象(top level objects)必须被显式释放,除非你使用 NSWindowControllerNSViewController 来加载 nib 文件。而在 iOS 上,它们会被自动释放(autoreleased)。
  • 在 iOS 上,由于 KVC 的工作机制,直接设置实例变量(ivar)会保留(retain)该输出口(outlet)。在 Mac 上,则不会保留该输出口。
  • 由于在 iOS 上直接设置实例变量会导致保留,因此这类输出口必须在 dealloc 方法中释放。在 Mac 上,则可以完全忽略它们。
  • 在 iOS 上,由于 KVC 的存在,直接设置实例变量会执行比 Mac 上更彻底的搜索模式。(译注:此处描述的是较旧 Runtime 下的行为,在现代 ARC 和属性声明下,outlet 的内存管理已高度统一和自动化。)

相似点

牢记这些差异在认知上颇为费力且容易出错,尤其是在两个平台间切换时。处理不当可能导致内存泄漏或崩溃(或两者兼有)。处理这些差异的最佳方式是坚持使用两个平台行为一致的部分。幸运的是,这些部分同时也是处理 nib 文件最便捷、最优的方式。

当使用 Cocoa 控制器类(Mac 上的NSWindowControllerNSViewController,iOS 上的UIViewController)加载 nib 文件时,nib 中的顶层对象会自动为你处理,因此在这种情况下两个平台的行为变得相同。直接加载 nib 文件的情况极为罕见,如果你发现自己正在这样做,或许应该停止并转而使用这些控制器中的一个。

使用 @property 声明输出口(outlets)时,两个平台的内存管理行为是一致的,因为它们都会在存在 setter 方法时使用该 setter。属性的内存管理方式可以按需设置,但通常更推荐使用 strong 或 retain。在这种情况下,你必须在 dealloc 中释放属性值,就像处理任何其他 strong 属性一样 —— 除非你使用了自动引用计数(Automatic Reference Counting,简称 ARC)。在 iOS 中,对于子视图的输出口,使用 weak 可能是一个不错的选择,因为视图可能被卸载,而你不希望有一个 strong 引用在你不知情的情况下使它们保持存活。

便捷表示例 以下是各种情况的完整总结,以便捷的表格形式呈现:

结论 在 Mac 和 iOS 之间,Nib 的内存管理机制相似,但差异足以令人困惑。幸运的是,通过坚持两个平台行为完全一致的方面,可以轻松减轻这种困惑 —— 而这最终也会产生最佳实践。始终使用 Cocoa 控制器来加载 Nib,而不是自己直接加载。始终为你的输出口声明属性。与任何属性一样,如果你的输出口属性是 strong 类型,那么你必须在 dealloc 中释放其背后的实例变量(或让 ARC 代你完成)。

今天就到这里了。下期周五 Q & A 我们将带来另一个令人兴奋又有趣的内容。在此之前,由于周五 Q & A 依靠读者建议驱动,请发送您想了解的主题给我们。


#Original (English)

Source: https://www.mikeash.com/pyblog/friday-qa-2012-04-13-nib-memory-management.html

I’m back from my hiatus and ready with a fresh journey into the netherworld of Apple’s platforms. Today’s subject comes from several readers who suggested that I discuss the subtleties of dealing with memory management and nibs, and particularly the differences between the Mac and iOS.

Nib Loading OverviewWhen you load a nib, two important steps happen in sequence. First, the loader instantiates all of the objects in the nib. Second, it connects all of the outlets specified in the nib.

When it comes to memory management, there are two relevant areas. The first is how to properly manage outlets. The second is how to manage the top-level objects in the nib. A nib contains a hierarchy of objects, where each object is owned by its parent, but the objects at the top level of that hierarchy are a special case.

Mac Nib LoadingLet’s talk about how nib loading works on the Mac, with respect to the two relevant memory management areas.

Top level objects are instantiated using alloc and init (or a class-specific initializer like -initWithContentRect:styleMask:backing:defer: for NSWindow instances. They are then left like this, with responsibility for finally releasing them implicitly transferred to the File’s Owner object. If you use NSWindowController or NSViewController to load the nib, it automatically takes ownership of these objects and will release them when the controller is destroyed.

To set an outlet, the nib loader first searches for a setter method. If the outlet is called foo, the loader searches for a method called setFoo:. If such a method exists, the loader calls it, passing the value of the outlet as the parameter.

If no such method exists, the loader searches for an instance variable with the same name as the outlet. If it finds such an instance variable, the loader sets its value directly to the outlet value without performing any memory management.

Finally, if no method and no instance variable can be found, the outlet connection fails and the outlet is not set.

iOS Nib LoadingNow let’s talk about how nib loading works on iOS. Overall it’s very similar, but there are subtle differences. Don’t worry about trying to find all of the differences, as I’ll point them out and analyze them afterwards.

Top level objects are instantiated using alloc and init (or a class-specific initializer), and then autoreleased. In the absence of anything else retaining them, these objects will be automatically destroyed.

To set an outlet, the nib loader calls -setValue:forKey: with the outlet value and outlet name. The Key-Value Coding machinery then takes over and searches for a way to set that particular key. For an outlet called foo, it will first search for a method called setFoo:. If such a method exists, it calls that method, passing the value of the outlet as the parameter.

If no such method exists, the KVC machinery searches for an instance variable called _foo, _isFoo, foo, or isFoo. If any of those exists, it sets the first one it finds to the value of the outlet, releasing the old value in the instance variable (if any) and retaining the new value.

If no method and no instance variable are found, KVC calls setValue:forUndefinedKey:. By default, this raises an execption, and it can be overridden to implement custom behavior for unknown keys.

The DifferencesThese two systems are similar but not quite the same. The differences are due to the more modern nature of iOS. Without exception, where the two systems differ, the iOS way is more sensible. Unfortunately, the Mac way can’t be changed without severely breaking backwards compatibility. These differences are:

  • Top level objects must be explicitly released on the Mac, unless you use an NSWindowController or NSViewController to load the nib. On iOS, they are autoreleased.

  • Directly setting the ivar on iOS retains the outlet due to how KVC works. On the Mac, the outlet is not retained.

  • Because directly setting the ivar results in a retain on iOS, such outlets must be released in dealloc. On the Mac, they can just be ignored there.

  • Directly setting the ivar on iOS has a more thorough search pattern than on the Mac, due to KVC.

The SimilaritiesKeeping track of these differences is mentally taxing and error-prone, especially if you switch between the two platforms. Getting it wrong can cause a leak or a crash (or both). The best way to handle the differences is to stick to areas where the two platforms are the same. Fortunately, those areas are also the most convenient and best ways to approach nibs anyway.

When loading nibs with a Cocoa controller class (NSWindowController and NSViewController on the Mac, UIViewController on iOS), top-level objects in the nib are automatically handled for you, and thus the behavior becomes the same on both platforms in this case. It’s extremely rare to need to load a nib directly, and if you find yourself doing it, you should probably stop and use one of these controllers instead.

When using @property for outlets, memory management is consistent across both platforms, since they both use the setter if one exists. The memory management for the property can be set as you like, although strong or retain is generally preferred. In that case, you must release the property value in dealloc, just as you would with any other strong property, unless you’re using ARC. weak can be a good choice for outlets to subviews on iOS, where the views may be unloaded and you don’t want a strong reference to keep them alive behind your back.

A Convenient TableHere’s a full summary of the various situations in convenient table form:

ConclusionNib memory management is similar between Mac and iOS but just different enough to be annoyingly confusing. Fortunately, it’s easy to mitigate the confusion by sticking to areas where the two platforms behave identically, which results in best practices anyway. Always use a Cocoa controller to load nibs rather than loading the nib directly yourself. Always declare properties for your outlets. As with any property, if your outlet properties are strong, then you must release the backing instance variable in dealloc (or let ARC do it for you).

That’s it for today. Come back next time for another exciting and tittilating edition of Friday Q&A. Until then, since Friday Q&A is driven by reader suggestions, please send in your ideas for topics to cover.