UIPageViewController does not release the at last

2019-07-29 09:54发布

问题:

I am presenting a simple UIPageViewController and adding some really simple and stupid child view controllers to it. When the UIPageViewController gets dismissed I am disposing all child view controllers, the ones currently not displayed (listed in ChildViewControllers) and the one displayed (listed in ViewControllers). The not displayed ones get released, the displayed one gets not.

I have broken this down to a simple failing test, so I am sure it's not about the content of the child view controllers or some other issues around that. I have no idea what is retaining it.

Sample:

Master (presented)

public class MasterDialog : UIPageViewController
{
    public event EventHandler OnDialogClosed;

    private UIBarButtonItem _backButton;

    public MasterDialog() :  base(
        UIPageViewControllerTransitionStyle.Scroll, 
        UIPageViewControllerNavigationOrientation.Horizontal, 
        UIPageViewControllerSpineLocation.None, 
        25)
    {
        _backButton = new UIBarButtonItem(UIBarButtonSystemItem.Cancel);
        _backButton.Clicked += Close;

        NavigationItem.SetLeftBarButtonItem(_backButton, false);
    }

    public override void ViewDidDisappear(bool animated)
    {
        base.ViewDidDisappear(animated);

        OnDialogClosed(this, EventArgs.Empty);
    }

    private void Close(object sender, EventArgs arguments)
    {
        _backButton.Clicked -= Close;

        NavigationController.DismissViewController(true, null);
    }

    protected override void Dispose(bool disposing)
    {
        base.Dispose(disposing);

        Console.WriteLine("Master disposed");
    }
}

Master Data Source

public class DataSource : UIPageViewControllerDataSource
{
     public override UIViewController GetPreviousViewController(
        UIPageViewController pageViewController, UIViewController referenceViewController)
     {
         var detail = (DetailDialog)referenceViewController;

         if (detail.Page - 1 == 0)
             return null;

         return GetViewController(detail.Page - 1);
     }

     public override UIViewController GetNextViewController(
        UIPageViewController pageViewController, UIViewController referenceViewController)
     {
         var detail = (DetailDialog)referenceViewController;

         return GetViewController(detail.Page + 1);
     }

     public UIViewController GetViewController(int page)
     {
         return new DetailDialog(page);
     }
}

Detail (Child)

public class DetailDialog : UITableViewController
{
    public int Page { get; private set; }

    public DetailDialog(int page) : base(UITableViewStyle.Plain)
    {
        Page = page;
    }

    public override void ViewDidLoad()
    {
        base.ViewDidLoad();

        Console.WriteLine("Detail init: " + Page + " / " + GetHashCode());

        var label = new UILabel();
        label.Text = "#" + Page;
        label.ContentMode = UIViewContentMode.Center;
        label.Frame = new System.Drawing.RectangleF(0, 100, 320, 50);
        label.BackgroundColor = UIColor.Green;

        Add(label);
    }

    protected override void Dispose(bool disposing)
    {
        base.Dispose(disposing);

        Console.WriteLine("Detail disposed: " + Page + " / " + GetHashCode());
    }
}

The opening dialog (starting point)

    public class StartDialog : UIViewController
    {
        private DataSource _dataSource;
        private MasterDialog _master;

        public StartDialog()
        {
            Title = "WTF";
        }

        public override void ViewDidLoad()
        {
            base.ViewDidLoad();

            var button = new UIButton(UIButtonType.Custom);
            button.SetTitle("Open", UIControlState.Normal);
            button.BackgroundColor = UIColor.Green;
            button.Frame = new System.Drawing.RectangleF(20, 150, 280, 44);

            Add(button);

            button.TouchDown += OpenMasterDialog;
        }

        private void OpenMasterDialog(object sender, EventArgs arguments)
        {
            _dataSource = new DataSource();

            _master = new MasterDialog();
            _master.DataSource = _dataSource;
            _master.OnDialogClosed += HandleOnDialogClosed;

            _master.SetViewControllers(
                new [] { _dataSource.GetViewController(1) }, 
                UIPageViewControllerNavigationDirection.Forward, 
                false, 
                null
            );

            NavigationController.PresentViewController(
                new UINavigationController(_master), 
                true, 
                null
            );
        }

        private void HandleOnDialogClosed(object sender, EventArgs e)
        {
            _dataSource.Dispose();
            _dataSource = null;

            Console.WriteLine("Before: " + _master.ChildViewControllers.Length +
                "/" + _master.ViewControllers.Length + ")");

            var childs = _master
                .ChildViewControllers.ToList()
                    .Union(_master.ViewControllers);

            foreach (UIViewController child in childs)
            {
                child.RemoveFromParentViewController();
                child.Dispose();
            }

            Console.WriteLine("After: " + _master.ChildViewControllers.Length + 
                "/" + _master.ViewControllers.Length + ")");

            _master.OnDialogClosed -= HandleOnDialogClosed;
            _master.Dispose();
            _master = null;
        }
    }

回答1:

I might be misunderstanding your code/intent but in this case it seems to me that everything is almost fine. Anyway here's my findings...

Detail disposed: 1 / 36217954
After: 0/1)

Line #2 shows /1 which I assume to be the issue. This is normal because you're re-surfacing the view controller, IOW the code:

_master.ViewControllers.Length

calls the viewControllers selector on the UIPageViewController. That returns: "The view controllers displayed by the page view controller." which is still DetailDialog at that point (even if master is not displayed anymore).

This is not Xamarin specific, an ObjC application would return the same (native) instance at that same point of time.

That's explained - but it still not freed later, why ?

Under the new Dispose semantics the managed object is kept, after Dispose, as long as the native side requires it (but without a native reference so it can be natively released and, subsequently, released on the managed side).

In this case the lifecycle of the native object is not yet over (i.e. iOS still has reference to it) so it remains alive on the managed side.

        _master.Dispose();
        _master = null;

This removes the managed references to _master but again (same as above) it won't be freed (and neither will be DetailDialog) as long as the native _master instance is used (with native references).

So who got a reference to _master ?

      NavigationController.PresentViewController(
            new UINavigationController(_master), 

^ That creates a UINavigationController and as long as it's alive the there are references to the others.

When I dispose of the UINavigationController (I kept it in a field) then the Master* and Detail* instances disappear from HeapShot.

    _nav.Dispose();
    _nav = null;