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;
}
}
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...
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:calls the
viewControllers
selector on theUIPageViewController
. That returns: "The view controllers displayed by the page view controller." which is stillDetailDialog
at that point (even ifmaster
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, afterDispose
, 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.
This removes the managed references to
_master
but again (same as above) it won't be freed (and neither will beDetailDialog
) as long as the native_master
instance is used (with native references).So who got a reference to
_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.