使用MonoTouch和MonoTouch.Dialog进行内存/资源管理

Omr*_*itt 19 memory-management xamarin.ios ios monotouch.dialog

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

到目前为止,我没有太多关注内存/视图管理(我一直在模拟器中运行),但是当我开始在真实设备上进行测试时,我注意到由于内存条件不足导致的一些故障(例如,应用程序被终止,我从我的日志中发现在此之前调用了DidReceiveMemoryWarning).其他时候我注意到应用程序响应性的长时间暂停,我假设是由于GC循环.

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

有没有关于如何使用MonoTouch和MT.D处理资源和内存的最佳实践?特别:

  • 弹出后,是否需要在DialogViewController上调用Dispose?如果是这样,哪里最好这样做?(ViewDidUnload?DidReceiveMemoryWarning?析构函数?)
  • DVC是否会自动处理传递给它的RootElement等对象,还是我需要担心这个问题?它作为呈现表格单元格的一部分加载的UIImages如何(例如StyledStringElement)?
  • 有没有我应该调用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 Profiler之后,我很清楚当用户弹出导航堆栈时我正在泄漏UIViewControllers.Rolf 为此提出了一个错误,它有两个重复,所以这不仅仅是我的真正问题.不幸的是,我没有找到泄漏的UIViewControllers的好方法 - 我还没有找到一个调用Dispose()的好地方.释放ViewDidLoad分配的资源的自然位置在ViewDidUnload消息中,但它永远不会在模拟器上调用,因此我的内存占用量不断增长.在设备上,我确实看到了DidReceiveMemoryWarning,但我不愿意使用它作为释放我的viewcontroller及其资源的地方,因为我无法保证iOS实际上会卸载我的视图,因此无法保证我的ViewDidLoad会再次被调用要么(导致ViewDidAppear需要针对其底层资源处置的情况进行防御性编码).我很想得到一些关于如何摆脱这个烂摊子的建议......

Omr*_*itt 30

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

  1. MonoTouch.Dialog表现得非常好.它在普通使用情况下不泄漏任何资源.它在DVC.Root下保存一个控制树,并且每个Element的Dispose方法正确地处理底层的UIKit控件.如果您已经更换了DVC.Root,您甚至不必担心处置旧的RootElement - 属性setter会自动为您处理它.总的来说,MT.D似乎没有任何重大的内存问题.有一个例外 - 见下文.
  2. 在创建自己的自定义元素(例如MultilineEntryElement)时,请确保覆盖Dispose(bool)方法,处理基础UIKit控件(例如UITextView),并链接基类Dispose()方法.Miguel的MT.D github项目中的源代码提供了很多很好的例子.所有Elements都实现了标准的Dispose模式(尽管它们省略了一个调用Dispose(false)的析构函数/终结器).
  3. 实现自定义视图控制器时,通常不需要在UIViewController子类上实现Dispose,也不必在TableView DataSource或Delegate类上实现.当视图控制器获得GC时,它将正确调用其引用的Dispose.您在DataSource中分配的所有单元格都将被正确处理.
  4. 作为(3)的一个例外 - 我在将自己的子视图添加到TableView的单元格时遇到了一个令人讨厌的问题.这个子视图是我创建的一个名为"UICheckbox"的控件,最终继承自UIImageView,它有两个UIImages(打开和关闭)和一个名为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;
    
    Run Code Online (Sandbox Code Playgroud)

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

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

更新:还有几点说明:

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

    this.ViewControllers = new UIViewController[0];
    
    Run Code Online (Sandbox Code Playgroud)

旧阵列现在将被GC,DVC也将如此,因为没有任何东西指向它了.并且iOS不会重新调用该对象.注意 - 无需在DVC上调用Dispose.

  • 这是一个很有价值的信息,多亏了这一切.只是给未来读者的一个注释:自iOS 6以来,系统不会调用`ViewDidUnload`. (6认同)