33

注意:我创建了一个简单的项目——你可以看到在 storyboard 之间UIButton和里面切换类型是如何CustomButton改变 GC 行为的。

我正试图让我的头脑围绕 MonoTouch 垃圾收集器。
该问题类似于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为足以欺骗垃圾收集器认为子控制器尚不符合收集条件。

怎么会这样?

4

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

于 2012-10-24T23:00:36.897 回答