33

Note: I've created a simple project—you can see how switching types between UIButton and CustomButton in storyboard changes GC behavior.

I'm trying to get my head wrapped around MonoTouch garbage collector.
The issue is similar to the one fixed in MT 4.0, however with inherited types.

To illustrate it, consider two view controllers, parent and child.

Child's view contains a single UIButton that writes to console on tap.
Controller's Dispose method throws an exception so it's hard to miss.

Here goes child view controller:

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);
}

Parent view controller just presents child controller and sets a timer to dismiss it and run 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);
}

If you run this code, it predictably crashes inside ChildViewController.Dispose() called from its finalizer because child controller has been garbage collected. Cool.

Now open the storyboard and change button type to CustomButton. MonoDevelop will generate a simple UIButton subclass:

[Register ("CustomButton")]
public partial class CustomButton : UIButton
{
    public CoolButton (IntPtr handle) : base (handle)
    {
    }

    void ReleaseDesignerOutlets()
    {
    }
}

Somehow changing the button type to CustomButton is enough to trick garbage collector into thinking child controller is not yet eligible for collection.

How is that so?

Community
  • 1
  • 1
Dan Abramov
  • 264,556
  • 84
  • 409
  • 511
  • I mirrored this thread on [Xamarin Forums](http://forums.xamarin.com/discussion/224/is-this-a-bug-in-monotouch-gc#latest). – Dan Abramov Oct 24 '12 at 22:34

1 Answers1

46

This is an unfortunate side-effect of MonoTouch (who is garbage collected) having to live in a reference counted world (ObjectiveC).

There are a few pieces of information required to be able to understand what's going on:

  • For every managed object (derived from NSObject), there is a corresponding native object.
  • For custom managed classes (derived from framework classes such as UIButton or UIView), the managed object must stay alive until the native object is freed [1]. The way it works is that when a native object has a reference count of 1, we do not prevent the managed instance from getting garbage collected. As soon as the reference count increases above 1, we prevent the managed instance from getting garbage collected.

What happens in your case is a cycle, which crosses the MonoTouch/ObjectiveC bridge and due to the above rules, the GC can't determine that the cycle can be collected.

This is what happens:

  • Your ChildViewController has a sayHiButton. The native ChildViewController will retain this button, so its reference count will be 2 (one reference held by the managed CustomButton instance + one reference held by the native ChildViewController).
  • The TouchUpInside event handler has a reference to the ChildViewController instance.

Now you see that the CustomButton instance will not be freed, because its reference count is 2. And the ChildViewController instance will not be freed because the CustomButton's event handler has a reference to it.

There are a couple of ways to break the cycle to fix this:

  • Detach the event handler when you no longer need it.
  • Dispose the ChildViewController when you no longer need it.

[1] This is because a managed object may contain user state. For managed objects which are mirroring a corresponding native object (such as the managed UIView instance) MonoTouch knows that the instance can not contain any state, so as soon as no managed code has a reference to the managed instance, the GC can collect it. If a managed instance is required at a later stage, we just create a new one.

Rolf Bjarne Kvinge
  • 19,253
  • 2
  • 42
  • 86
  • 3
    That's a great explanation! I wish there was some page on Xamarin explaining these problems and solution patterns. – Dan Abramov Oct 24 '12 at 23:43
  • If you don't mind, I added an emphasis to the part I initially missed in your answer. I'm finally getting it now. – Dan Abramov Oct 24 '12 at 23:53
  • Can you explain what happens if you didn't force refcount-more-than-one but otherwise GC-eligible managed objects to live? Even if they have managed state, no one in the managed world links to them anyway? I'm not sure I fully follow yet. – Dan Abramov Oct 24 '12 at 23:57
  • 4
    @Dan Abramov, we are actively investigating a system to avoid having the cycles in the first place. Meanwhile, we will look into merging this into our docs. – miguel.de.icaza Oct 25 '12 at 12:07
  • 19
    @miguel: Please do! I enjoy using MonoTouch and I know you guys work super hard to deliver the best mobile dev experience but it's frustrating to bump into leaky abstractions **and not know how to work around them**. Xamarin website tricks you into thinking that everything is going to “just work”, including LINQ, generics, GC and stuff. It works for the most part, but when it doesn't, there is not much documentation, as though you don't want people to know about GC cycles, trampoline problems and various tradeoffs. Your product is not perfect which is *fine* with me—just don't hide it from me. – Dan Abramov Oct 25 '12 at 13:05
  • It should be possible to create a runtime tool (or add a feature to the profiler) that looks for these cycles shouldn't it? Perhaps this should go on the wishlist for Xamarin Studio. – Felix Jun 19 '13 at 22:55
  • 2
    A best practice I found is to setup your event handling stuff in `ViewWillAppear` and teardown it in `ViewDidDisappear`. It's cheap and does not require you to manually `Dispose` view controllers. Be also careful with notification subscriptions which I tend to setup and teardown the same way I do for events. And finally do a runtime check to not subscribe multiple times to events because `ViewWillAppear` may be called more than one time. – nverinaud Oct 28 '13 at 12:23
  • Is this still the way it works in the current version? – laktak Apr 28 '14 at 08:17
  • This article offers an example of how to work around: http://blog.bluetubeinteractive.com/2013/07/memory-management-pitfalls-in-xamarin-ios-pitfall-2.html – Chuck Batson Jul 11 '14 at 18:26
  • 1
    @ChuckBatson The link above is dead. – Krumelur Feb 04 '15 at 08:18
  • @Krumelur: http://bluetubeinc.com/blog/2013/7/memory-management-pitfalls-in-xamarin-ios-pitfall-2/ – testing Apr 29 '15 at 10:52
  • If one want to know more about memory management in general, he can have a look on this video: [iOS Memory Managment by Rodrigo Kumpera (Xamarin)](https://www.youtube.com/watch?v=8aDe01Mm0zA) – testing Aug 19 '15 at 10:58