这是 MonoTouch GC 中的错误吗?

Posted

技术标签:

【中文标题】这是 MonoTouch GC 中的错误吗?【英文标题】:Is this a bug in MonoTouch GC? 【发布时间】:2012-10-24 22:03:03 【问题描述】:

注意:I've created a simple project——您可以看到故事板中 UIButtonCustomButton 之间的类型切换如何改变 GC 行为。

我正试图了解 MonoTouch 垃圾收集器。 该问题与the one fixed in MT 4.0 类似,但具有继承类型。

为了说明这一点,考虑两个视图控制器,父级和子级。

子视图包含一个 UIButton,它会在点击时写入控制台。 Controller 的Dispose 方法会抛出异常,所以很难错过。

这里是子视图控制器:

public override void ViewDidLoad ()

    base.ViewDidLoad ();

    sayHiButton.TouchUpInside += (sender, e) =>
        SayHi();
    


void SayHi()

    Console.WriteLine("Hi");


protected override void Dispose (bool disposing)

    throw new Exception("Hey! I've just been collected.");
    base.Dispose (disposing);

父视图控制器只是呈现子控制器并设置一个计时器来关闭它并运行 GC:

public override void ViewDidLoad ()

    base.ViewDidLoad ();

    var child = (ChildViewController)Storyboard.InstantiateViewController("ChildViewController");

    NSTimer.CreateScheduledTimer(2, () => 
        DismissViewController(false, null);
        GC.Collect();
    );

    PresentViewController(child, false, null);

如果您运行此代码,它会在从其终结器调用的ChildViewController.Dispose() 内崩溃,因为子控制器已被垃圾回收。很酷。

现在打开故事板并将按钮类型更改为CustomButton。 MonoDevelop 会生成一个简单的UIButton 子类:

[Register ("CustomButton")]
public partial class CustomButton : UIButton

    public CoolButton (IntPtr handle) : base (handle)
    
    

    void ReleaseDesignerOutlets()
    
    

以某种方式将按钮类型更改为CustomButton 足以欺骗垃圾收集器认为子控制器尚不符合收集条件。

这是怎么回事?

【问题讨论】:

我在Xamarin Forums上反映了这个帖子。 【参考方案1】:

这是 MonoTouch(垃圾收集器)不得不生活在引用计数世界 (ObjectiveC) 中的一个不幸的副作用。

需要一些信息才能了解正在发生的事情:

对于每个托管对象(从 NSObject 派生),都有一个对应的本地对象。 对于自定义托管类(派生自 UIButton 或 UIView 等框架类),托管对象必须保持活动状态,直到本机对象被释放 [1]。它的工作方式是,当本机对象的引用计数为 1 时,我们不会阻止托管实例进行垃圾收集。 只要引用计数增加到 1 以上,我们就会阻止托管实例被垃圾回收。

在您的情况下发生的是一个循环,它穿过 MonoTouch/ObjectiveC 桥,由于上述规则,GC 无法确定可以收集循环。

会发生这样的事情:

您的 ChildViewController 有一个 sayHiButton。原生 ChildViewController 将保留此按钮,因此其引用计数将为 2(托管 CustomButton 实例持有的一个引用 + 原生 ChildViewController 持有的一个引用)。 TouchUpInside 事件处理程序具有对 ChildViewController 实例的引用。

现在您看到 CustomButton 实例不会被释放,因为它的引用计数是 2。而且 ChildViewController 实例不会被释放,因为 CustomButton 的事件处理程序有对它的引用。

有几种方法可以打破循环来解决这个问题:

当您不再需要事件处理程序时,将其分离。 当您不再需要 ChildViewController 时,将其丢弃。

[1] 这是因为托管对象可能包含用户状态。对于镜像对应本机对象的托管对象(例如托管 UIView 实例),MonoTouch 知道该实例不能包含任何状态,因此只要没有托管代码引用托管实例,GC 就可以收集它。如果稍后需要托管实例,我们只需创建一个新实例。

【讨论】:

这是一个很好的解释!我希望 Xamarin 上有一些页面可以解释这些问题和解决方案模式。 @Dan Abramov,我们正在积极研究一个系统,以避免首先出现循环。同时,我们将考虑将其合并到我们的文档中。 @miguel:拜托了!我喜欢使用 MonoTouch,而且我知道你们非常努力地工作以提供最佳的移动开发体验,但遇到有漏洞的抽象是令人沮丧的并且不知道如何解决它们。 Xamarin 网站诱使您认为一切都会“正常工作”,包括 LINQ、泛型、GC 和其他东西。它在大多数情况下都有效,但当它无效时,没有太多文档,好像您不希望人们了解 GC 周期、蹦床问题和各种权衡。你的产品并不完美,这对我来说很好——不要对我隐瞒。 我发现的一个最佳实践是在ViewWillAppear 中设置您的事件处理内容,然后在ViewDidDisappear 中将其拆解。它很便宜,并且不需要您手动Dispose 视图控制器。还要小心通知订阅,我倾向于以与事件相同的方式设置和拆卸。最后做一个运行时检查,不要多次订阅事件,因为ViewWillAppear 可能会被多次调用。 @ChuckBatson 上面的链接失效了。

以上是关于这是 MonoTouch GC 中的错误吗?的主要内容,如果未能解决你的问题,请参考以下文章

MonoTouch 6.0.8 发行说明中的​​“运行时蹦床”是啥意思?

我可以在 MonoTouch 项目中使用响应式扩展吗?

我们可以在 Windows 环境下使用 MonoTouch 开发 iOS 应用吗

引起:java.lang.OutOfMemoryError:超出GC开销限制

如何使用 Monotouch 设置导航项标题的代码?

在 Monotouch 中更改“返回”NavigationItem 文本?