Memory Management Pitfalls in Xamarin iOS - Pitfall 2

Pitfall #2 – Button Event Handlers


In the Introduction to this series, we talked about the interaction between Xamarin.iOS and the native Objective C iOS code.  We pointed out that since Objective C is reference counted, the Xamarin.iOS C# garbage collector may have trouble releasing objects in certain scenarios and needs our help.  In part 1 we reviewed parent/child relationships and possible cyclical references.  Now let’s look at a more complicated, but much more common, scenario. 

This one bears a little bit more explanation and a demonstration.

 

The Setup


Storyboard

 

We’ll start out with a very simple storyboard application with two view controllers.  The first “Continue” button triggers a push segue to load the second view controller that contains the two buttons we’ll be working with.

As you can see, the first button is a standard UIButton, fittingly labeled “Normal UIButton.”  The second button we’ve given a new class, “CustomButton,” which is defined as so: 

public partial class MyButton : UIButton
{
    public MyButton (IntPtr handle) : base (handle)
    {
    }
}

 

Nothing special going on in there, as you can see.  We’ve created outlets for each button in our View2Controller class. 

The easiest way we’ve found to see when garbage collection / object release is failing to occur is by overriding the Dispose method on your UIViewController, as so:

 

protected override void Dispose (bool disposing)
{
    Console.WriteLine (String.Format ("{0} controller disposed - {1}", 
        this.GetType (), this.GetHashCode ()));
    base.Dispose (disposing);
}

 

Here we’re just logging a message when our UIViewController is disposed.  The garbage collector runs very aggressively when debugging in the iOS Simulator, so you should see your message logged to the console when you’ve popped the controller from the navigation stack, say by using the back button of a UINavigationController.

 

The Test(s)

 

Now let’s examine how each of our buttons affects the garbage collection of View2Controller depending on how we attach event handlers.

 

In each of our tests, we’ll perform the following steps:

 

  • Tap the big “Continue” button on the root view
  • Tap the button we’re working with, “normal” or “custom”
  • Tap the back button on the UINavigationController

 

We’ll have our buttons do the same thing, call the logButtonClick() method:

 

void logButtonClick ()
{
    Console.WriteLine("Button was clicked");
}

 

Using lambdas or anonymous delegates

 

This seems to be the Xamarin preferred way of attaching event handlers.  Indeed, you see code similar to this in many of their code samples and in the MonoTouch/Xamarin community in general.  Let’s try it out!  In our ViewDidLoad override, we’ll attach the event handler to our normal UIButton like so:

 

NormalButton.TouchUpInside += (sender, e) => {
    logButtonClick();
};

 

Next we’ll perform the test steps listed above and stop the app.  Sure enough, the application output window has this logged:

 

2013-06-16 23:18:04.810 MemoryTestApp[32154:c07] Button was clicked

2013-06-16 23:18:07.691 MemoryTestApp[32154:2c03] View2Controller disposed

 

Great!  We’re really on the right track here and everything’s working as expected.

 

Let’s try the same thing with our custom button:

 

CustomButton.TouchUpInside += (sender, e) => {
    logButtonClick();
};

 

Fast-forwarding to the results, here’s what we see:

 

2013-06-16 23:59:56.694 MemoryTestApp[32310:c07] Button was clicked

 

Hmm… that’s weird, no disposal notification.  Let’s look at why this is happening.

 

Why it doesn’t work

 

If you view the Xamarin Evolve talk that was linked in the introduction to this series, you’ll find a nugget that bears mentioning.  Xamarin’s garbage collector treats their wrapper classes (such as UIButton) differently than custom, user-defined types when it comes to handling the reference counting for a given object.  For user types only, a GC handle (like a static field) is created if the object’s reference count rises above 1.  (Refer to slides 39 – 43 of the presentation for the details here).

 

Really, when you dig down into it, we’ve essentially created another reference cycle like in Pitfall #1, but this time the event handler is pointing back to the view controller rather than the “parent” variable reference.

 

This means that by a) subclassing UIButton, and b) attaching an event handler to a button event, we’ve incremented the reference count above this threshold, created a cyclical reference, and Xamarin will not garbage collect the object without our help.  This also keeps the view controller from being collected as well since one of those references (the event handler) is pointing to the view controller class.

 

How we can help

 

So how can we help the GC make sense of this?  The simple answer is to remove the event handlers when it makes sense.

 

There are two hurdles to doing this:

 

  1. There is no way to remove an event handler that was attached using a lambda or anonymous delegate.  The standard syntax of object.event -= handler just doesn’t quite work here.  We could assign the delegate to a variable and += and -= the variable, but that starts to look horrible and takes the “cool factor” of lambdas/anon delegates way down.
  2. When do we do it?  How do we know the view controller is going away and the handlers can safely be removed?

 

We’ve developed some methodologies and a pattern than can help with both of these:

 

  1. Attach handlers the old school way, to methods within your controller:

    CustomButton.TouchUpInside += handler; 

    It may not be as cool, but it does allow you to unhook it later, and that proves to be extremely useful.

  2. Attach handlers in ViewWillAppear and detach them in ViewWillDisappear.  This ensures that anytime the view is visible and receptive the user’s touch, the events are hooked up and ready to go.  But when the view goes away we can safely unhook the handlers.

 

The Payoff

 

Let’s see this in action!

 

Putting all the above into place, our View2Controller now looks like this:

 

        protected override void Dispose (bool disposing)
        {
            base.Dispose (disposing);
            
            Console.WriteLine (String.Format ("{0} controller disposed - {1}",
                 this.GetType (), this.GetHashCode ()));
        }
        
        public override void ViewWillAppear (bool animated)
        {
            base.ViewWillAppear (animated);
            
            CustomButton.TouchUpInside += handler;
        }
        
        public override void ViewWillDisappear (bool animated)
        {
            CustomButton.TouchUpInside -= handler;
            
            base.ViewWillDisappear (animated);
        }
        
        void handler (object sender, EventArgs e)
        {
            logButtonClick();
        }
        
        void logButtonClick ()
        {
            Console.WriteLine("Button was clicked");
        }

 

Running our test yields the following results:

 

2013-06-17 00:38:49.067 MemoryTestApp[32467:c07] Button was clicked

2013-06-17 00:38:50.946 MemoryTestApp[32467:2c03] MemoryTestApp.View2Controller controller disposed – 495710208

 

Excellent!  Life is now good and everything is in order.

 

Takeaways

 

It’s important to note that this is not a UIButton-specific problem.  Extending any of the “wrapper” classes around native objects (think UIAlertView, UIDatePicker, etc.) will put you in the same situation.  We’ve singled out buttons as they are most often customized and the “lambda method” of attaching event handlers is widely used and prevalent in code samples.

 

Overriding the Dispose() method as we’ve done here is a good, albeit primitive, trick to know when something is keeping your UIViewController class from being collected. 

Posted in: Xamarin Mobile and Tablet | Permalink

Posted by Bluetube