19

我有一个具有 UITabBarController 的 MonoTouch 应用程序,每个选项卡都是 UINavigationController。其中一些包装了一个 UIViewController,它添加了一个 UITableView 和一个 UIToolbar,而另一些包装了一个 DialogViewController。

到目前为止,我还没有过多关注内存/视图管理(我主要是在模拟器中运行),但是当我开始在真实设备上进行测试时,我注意到由于内存不足而导致的一些故障(例如,应用程序被终止,我从日志中发现 DidReceiveMemoryWarning 在此之前被调用)。其他时候,我注意到应用程序响应的长时间停顿,我认为这是由于 GC 周期造成的。

到目前为止,我一直假设我推送到导航堆栈的每个 DialogViewController 都会清理它的视图和其他在我弹出它时分配的东西。但我开始意识到这可能并不容易,我需要开始调用 Dispose() 。

是否有关于如何使用 MonoTouch 和 MT.D 管理资源和内存的最佳实践?具体来说:

  • 弹出后是否需要在 DialogViewController 上调用 Dispose?如果是这样,最好在哪里执行此操作?(ViewDidUnload?DidReceiveMemoryWarning?析构函数?)
  • DVC 是否会自动处理传递给它的 RootElement 之类的对象,还是我需要担心这个?它作为渲染表格单元格(例如 StyledStringElement)的一部分加载的 UIImages 怎么样?
  • 有没有我应该调用 GC.Collect() 来更好地分隔集合的地方,以便在发生 GC 时不会受到响应的影响?
  • 分代垃圾收集器是否有助于解决交互问题,是否足够稳定,可以在生产应用程序中使用?(我相信它在 MonoDevelop 3.0.2 / MT 4.3.3 中仍然被称为“实验性”)
  • 我需要在 DidReceiveMemoryWarning 中做些什么来降低 iOS 拍摄我的应用程序的可能性?由于每个不可见的视图控制器似乎都收到了这个调用,我假设我应该清理那个视图控制器的资源......我应该做我在 ViewDidUnload 中做的同样的事情吗?
  • 我似乎没有调用我的 ViewDidUnload(即使在我收到 DidReceiveMemoryWarning 之后)。事实上,我不记得曾经在我的日志中看到过它。如果 iOS 总是在 DidReceiveMemoryWarning 之后调用我的 ViewDidUnload,我可以在 ViewDidUnload 中进行所有清理......在 ViewDidUnload 和 DidReceiveMemoryWarning 之间划分清理责任的最佳方法是什么?

对于这个问题的一般性质,我深表歉意——这似乎是一个很好的白皮书主题,但我找不到任何......

更新:为了使问题更具体:在使用 Instruments 和 Xamarin Heapshot 分析器之后,我很清楚当用户弹出导航堆栈时我正在泄漏 UIViewControllers。Rolf 提交了一个错误为此,它有两个重复,所以这不仅仅是我一个真正的问题。不幸的是,我还没有为泄露的 UIViewControllers 找到一个好的解决方法——我还没有找到一个对它们调用 Dispose() 的好地方。释放 ViewDidLoad 分配的资源的自然位置是在 ViewDidUnload 消息中,但它从未在模拟器上被调用,因此我的内存占用不断增长。在设备上,我确实看到了 DidReceiveMemoryWarning,但我不愿意使用它作为释放我的 viewcontroller 及其资源的地方,因为我不能保证 iOS 会真正卸载我的视图,因此不能保证我的 ViewDidLoad 会再次被调用要么(导致 ViewDidAppear 需要针对其底层资源被处置的情况进行防御性编码)。我'

4

1 回答 1

30

我在 MT.D 源代码和分析器中花了几天时间。虽然我仍在寻找关于实现 DidReceiveMemoryWarning 和 ViewDidUnload 的最佳设计模式的一般指导,但我确实有一些一般性的观察要分享,这可能对某人有用:

  1. MonoTouch.Dialog 表现得非常好。它在正常使用下不会泄漏任何资源。它在 DVC.Root 下保留了一个控件树,每个 Element 的 Dispose 方法都正确地 Dispose 底层 UIKit 控件。如果您已替换 DVC.Root,您甚至不必担心处置旧的 RootElement - 属性设置器会自动为您处置它。总体而言,MT.D 似乎没有遭受任何重大的内存问题。有一个例外 - 见下文。
  2. 创建您自己的自定义元素(例如 MultilineEntryElement)时,请确保重写 Dispose(bool) 方法,释放底层 UIKit 控件(例如 UITextView),并链接基类 Dispose() 方法。Miguel 的 MT.D github 项目中的源代码提供了很多很好的示例。所有元素都实现了标准的 Dispose 模式(尽管它们省略了调用 Dispose(false) 的析构函数/终结器)。
  3. 在实现自定义视图控制器时,一般不需要在 UIViewController 子类上实现 Dispose,也不需要在 TableView DataSource 或 Delegate 类上实现。当视图控制器被 GC'ed 时,它将正确地调用其引用的 Dispose。您在 DataSource 中分配的所有单元格都将被正确处理。
  4. 作为 (3) 的一个例外 - 在将我自己的子视图添加到 TableView 的单元格时,我遇到了一个令人讨厌的问题。这个子视图是我创建的一个名为“UICheckbox”的控件,它最终继承自 UIImageView,它有两个 UIImage(打开和关闭)和一个名为 Clicked 的公共事件。我只在引用 DataSource 成员的事件处理程序与此事件挂钩时遇到问题(如果事件处理程序不引用 DataSource 或控制器本身,一切都很好)。但是,当满足上述条件并关闭控制器时,显然存在一些 GC 无法弄清楚的循环,并且我放在 TableView 上的每个 UICheckbox 都被泄露(连同它的图像)。我发现解决此问题的唯一方法是向 ViewDidDisappear 添加代码以处理 ViewController 并清理其状态 IFF 它不再位于导航堆栈中的任何位置。这是hacky,但它的工作原理。
  5. 一般来说,我遵循以下模板在我的视图控制器中分配对象:

    • 在构造函数中不分配任何内容(仅用于传递状态)
    • 在 ViewDidLoad 中创建一个控制树(并在 ViewDidUnload 中处理它)。想想 XAML 中的“InitializeComponent”(如果有帮助的话)。如果 UIViewController 要将 DialogViewController 推送到导航堆栈,那么 ViewDidLoad 是创建 DVC 的好地方。
    • 在 ViewDidAppear 中初始化控件树中的值。例如,您可以在此方法中添加/删除/替换元素、部分,甚至 DVC 的根。但不要创建新的 DVC。
  6. 当用户向上导航导航堆栈时,泄漏 ViewControllers 存在一个普遍问题(我在问题的“更新”中引用了 bugzilla 链接)。这也会影响 MT.D。有一个相当简单的解决方法 - 在父视图控制器的 ViewDidAppear 中添加以下代码行:

        // HACK: touch the ViewControllers array to refresh it (in case the user popped the nav stack)
        // this is to work around a bug in monotouch (https://bugzilla.xamarin.com/show_bug.cgi?id=1889)
        // where the UINavigationController leaks UIViewControllers when the user pops the nav stack
        int count = this.NavigationController.ViewControllers.Length;
    

Rolf 很好地解释了为什么会发生这个错误以及为什么解决方法在 bugzilla 链接中有效,所以我不会重复它。

我希望有人觉得这很有用。我也希望比我聪明的人对如何处理 DidReceiveMemoryWarning 以及如何在该方法和 ViewDidUnload 之间拆分工作有一些指导。

更新:还有一些注意事项:

  • 我现在实现了 DidReceiveMemoryWarning 和 ViewDidUnload 的协议:前者总是传递给每个视图控制器,而后者只发送给当前未显示的视图控制器,并且不比导航堆栈的根更深。最后,我决定忽略 DidReceiveMemoryWarning,因为我真的没有可以缓存和转储的图像(根据 iOS 指南)。在 ViewDidUnload 中,我释放了我在 ViewDidLoad 中分配的所有资源。
  • 我的应用程序有一个 TabBar,其中每个选项卡都承载一个 UINavigationController,其中大部分都推送一个 DialogViewController。我正在处理的一个问题是在 ViewDidUnload 释放对它的引用之后泄漏 DialogViewController。我尝试在 ViewDidUnload 中处理 DVC,但 iOS 一直想要重新调用它,并且我在调用 GC 对象上的选择器时遇到了异常。我发现了原因——导航控制器在其 ViewControllers 数组中保留了 DVC。解决方案是通过在其位置创建一个长度为零的数组来释放该数组 - 在 ViewDidUnload 中:

    this.ViewControllers = new UIViewController[0];
    

旧数组现在将被 GC 处理,DVC 也将被 GC,因为没有任何东西指向它了。iOS 永远不会重新调用该对象。注意 - 无需在 DVC 上调用 Dispose。

于 2012-06-14T05:51:54.790 回答