使用自适应 popover segue 并将目的地包装在导航控制器中会导致内存泄漏

Using adaptive popover segue and wrapping the destination in a navigation controller leads to memory leaks

假设我有一个视图控制器,我在单击按钮时使用 自适应弹出框 segue 显示它。现在在某些情况下,我可能想将目标视图控制器包装在(例如)导航控制器中。因此,我将自己设置为 popoverPresentationController 的委托人,并实现了 presentationController:viewControllerForAdaptivePresentationStyle: 方法。


但我注意到一些奇怪的事情:在某些情况下,对象没有被释放。如果在前面提到的方法中,我将呈现的 viewcontroller 包装在导航控制器中:

func presentationController(controller: UIPresentationController, viewControllerForAdaptivePresentationStyle style: UIModalPresentationStyle) -> UIViewController? {
    return UINavigationController(rootViewController: controller.presentedViewController)
}

关闭时导航控制器被释放,但呈现的视图控制器保持分配状态。

相比之下,如果我通过自适应弹出窗口直接显示导航控制器,那么在关闭导航控制器和它包含的详细信息控制器时会正确释放。


为了演示目的,请参考这个测试项目(Swift):https://github.com/djbe/AdaptivePopoverSegue-Test

在导航控制器中动态包装时我们得到了什么(点击 "Popover, nav automatically added" 按钮):

--- Showing details ---
Loaded details view controller (0x7fab31632b70)
Loaded navigation controller (0x7fab32815600)
Deinit navigation controller (0x7fab32815600)

如您所见,永远不会释放详细信息视图控制器。


我查看了 presentationController:viewControllerForAdaptivePresentationStyle: 的文档,但没有具体提及所有权、强保留等... 我尝试将 Instruments 与 Allocations 工具一起使用,但是这个(简单的)案例中涉及的问题太多 retain/releases,我无法直接找到问题所在。

有没有人遇到过这个问题?或者您有解决办法吗?


解决方案

如下@TomSwift 所述,由于控制器和 segue 之间的循环引用,存在一个错误。解决这个问题并且仍然将目标控制器包装在导航控制器中的唯一方法是在 segue(自定义)的 init 方法中执行 wrapping

我已经在 Github 上更新了我的示例代码,以展示如何使用@Vasily 提到的解决方案来实现这一点,但仍然允许使用协议进行动态包装行为,而无需诉诸于使用 hacky 解决方法NSUserDefaults.

解决方案

您需要创建自定义 UIStoryboardSegue class 并覆盖初始化函数。

样本:

class StoryboardSegue: UIStoryboardSegue {

override init(identifier: String?, source: UIViewController, destination: UIViewController) {
    super.init(identifier: identifier, source: source, destination: NavigationController(rootViewController: destination))
}
}

Main.storyboard

结果

使用 XCode8 我注意到 DetailsViewControllerUIStoryboardSegue 之间存在循环引用。我看不出有什么方法可以彻底打破这个循环,因为它在 UIKit 内部。似乎有一个涉及 NSDictionary ivar“_externalObjectsTableForLoading”的辅助循环引用。你应该向 Apple 报告!

一个解决方案是不重复使用 segue 预加载的 DetailsViewController。如果你自己手动实例化它,你可以绕过这个问题。这是一个可能的实现(需要您在情节提要中设置恢复标识符!):

func presentationController(controller: UIPresentationController, viewControllerForAdaptivePresentationStyle style: UIModalPresentationStyle) -> UIViewController? {
    if (wrapInNavigationController) {
        let vc = controller.presentedViewController
        if let restorationIdentifier = vc.restorationIdentifier {
            return NavigationController(rootViewController: vc.storyboard!.instantiateViewControllerWithIdentifier(restorationIdentifier))
        }
    }
    return controller.presentedViewController
}