这是MonoTouch GC中的一个错误吗?

33

注意: 我制作了一个简单的项目,您可以查看在故事板中在UIButtonCustomButton之间切换类型如何改变GC行为。

我试图理解MonoTouch垃圾收集器。
问题类似于MT 4.0中修复的问题,但是处理继承类型。

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

子视图包含一个单独的UIButton,点击时会向控制台输出内容。
控制器的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);
}

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

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论坛上复制了这个帖子。 - Dan Abramov
1个回答

46

这是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就可以收集它。 如果稍后需要管理实例,我们只需创建一个新实例。


3
很棒的解释!我希望有一些关于Xamarin解释这些问题和解决方案模式的页面。 - Dan Abramov
4
@Dan Abramov,我们正在积极研究一种避免出现循环依赖的系统。与此同时,我们将考虑将这个内容合并到我们的文档中。 - miguel.de.icaza
19
@miguel: 请这样做!我很喜欢使用MonoTouch,我知道你们非常努力地提供最好的移动开发体验,但是碰到不可靠的抽象问题却不知道如何解决真的很令人沮丧。Xamarin网站让你误以为一切都会“顺利进行”,包括LINQ、泛型、垃圾回收等等。大多数时候它都是有效的,但是当它出现问题时,几乎没有什么文档可以阐明如何处理GC周期、跳板问题和各种权衡。你们的产品并不完美,这对我来说是“可以”的——只是不要向我隐藏它。 - Dan Abramov
2
我发现的最佳实践是在 ViewWillAppear 中设置事件处理程序,然后在 ViewDidDisappear 中进行拆卸。这样做既便宜又不需要手动 Dispose 视图控制器。对于通知订阅也要小心,我倾向于像处理事件一样设置和拆卸它们。最后,在运行时检查不要多次订阅事件,因为 ViewWillAppear 可能会被调用多次。 - nverinaud
1
@ChuckBatson 上面的链接已经失效了。 - Krumelur
显示剩余7条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接