Forum Xamarin.iOS

MonoTouch garbage collector doesn't seem to collect non-reachable custom managed object. Bug?

SuperCoder1SuperCoder1 USMember
edited July 2017 in Xamarin.iOS

Reposting a question from StackOverflow that hasnt received any responses there:

[For context, my question is a follow-up to the accepted answer on this SO post]

I'm trying to understand precisely when and why the MonoTouch GC fails to collect a cycle that spans managed and native objects.

The basic behavior as I understand:

  1. When there is a custom managed object derived from a framework class (such as UIViewController), it is free to be garbage collected if its native peer has a ref count of 1.
  2. Conversely, if the native object's ref count is more than 1, its managed peer is prevented from being collected.
  3. So if a cycle involves a native object referencing a custom managed object's native peer, then the GC will not collect the native peer (as per #.2 above) and therefore there will be a resource/memory leak.

I created a simple example to show a cycle where a native peer does NOT have another native object referencing it. I expected this type of cycle to be collected correctly by the GC due to #.1 above, but this doesnt happen. Any ideas whats going on?

Briefly, here is whats happening in the code:

  1. Create OuterVC, MidVC and InnerVC each extending UIViewController.
  2. OuterVC adds MidVC.View as a subview
  3. MidVC adds InnerVC.View as a subview
  4. InnerVC adds a button with a delegate referencing a field 'string foo' in InnerVC.
  5. Invoke MidVC.View.RemoveFromSuperview()
  6. I see that MidVC is disposed from the finalizer as expected.
  7. InnerVC is not disposed - this is the part I don't understand why, since there shouldn't be any native references to InnerVC's native object at this point. Given that the managed MidVC object is disposed, its native peer (which was the only reference to InnerVC's native object earlier) should be released.
  8. As a side note, commenting out the access to foo in the button's delegate results in InnerVC being disposed from the finalizer.
  9. I have tried invoking GC.Collect() and GC.WaitForPendingFinalizers() multiple times but that doesnt change the results.

At the moment, I'm more curious to know whats going on behind scenes rather than suggestions on how to solve the problem. I already know that breaking the cycle will resolve the issue (say by assigning foo to a local variable and using that in the delegate instead of the field to avoid baking-in 'this').

Any insight on why the GC doesnt collect InnerVC in this case? I dont see why/how the managed InnerVC object would be reachable from a root object and prevent collection.

Here is the code:

    public class OuterVC : UIViewController
    {
        private MidVC midVC;
        public override void ViewDidLoad()
        {
            base.ViewDidLoad();

            midVC = new MidVC();
            View.AddSubview(midVC.View);

            Task.Factory.StartNew(() => {
                Thread.Sleep(5000);
                BeginInvokeOnMainThread(() => { midVC.View.RemoveFromSuperview(); midVC = null; });
            });
        }
    }

    public class MidVC : UIViewController
    {
        public override void ViewDidLoad()
        {
            base.ViewDidLoad();

            View.AddSubview(new InnerVC().View);
        }

        protected override void Dispose(bool disposing)
        {
            Console.WriteLine("MidVC.Dispose invoked. disposing = " + disposing); //Always invoked disposing = False
            base.Dispose(disposing);
        }
    }

    public class InnerVC : UIViewController
    {
        string foo;

        public override void ViewDidLoad()
        {
            base.ViewDidLoad();
            UIButton button = new UIButton();
            button.TouchUpInside += (object sender, System.EventArgs e) =>
            {
                //Commenting this invokes InnerVC's Dispose from finalizer
                var bar = foo;
            };
            View.AddSubview(button);
        }

        protected override void Dispose(bool disposing)
        {
            Console.WriteLine("InnerVC.Dispose invoked. disposing = " + disposing); //Only invoked if button's delegate does not access foo
            base.Dispose(disposing);
        }
    }
Sign In or Register to comment.