-
Notifications
You must be signed in to change notification settings - Fork 1k
UI code Reference counting and you
This document lists several common leaky patterns in MonoDevelop code, together with how to avoid them.
There are 3 major toolkits being used throughout MonoDevelop code, each with their own quirks, meaning they have to be handled differently:
- Gtk
- Xamarin.Mac
- Xwt
Both Gtk and Xamarin.Mac are bindings on top of native libraries with reference counted semantics. To implement the support for them to work under a garbage collected runtime, a ToggleRef is used.
What is a ToggleRef?
A ToggleRef is a construct ties a native object to a managed object using the following logic:
- Add a strong reference to the native object with a callback that notifies when the reference count changes.
- Removes the native construction strong reference
- Hold a managed reference to the object
In Gtk#, for example, the implementation looks like:
class ToggleRef
{
IntPtr handle;
object reference;
GCHandle gch;
}
The handle is the native object's pointer.
The GCHandle is a special runtime class which wraps an object and tells the garbage collector to not collect that object until the GCHandle is freed.
When the refcount change callback is called (that means, even when the initial ref is removed after adding the ToggleRef), what happens is:
- if refcount == 1 ->
reference
is aWeakReference
to the managed object - meaning that it is eligible for garbage collection if only the ToggleRef (and other weak references) are holding the object alive - if refcount > 1 ->
reference
is a strong reference to managed object (aka, the object itself), so managed will keep it alive, native too.
When a native wrapped object is disposed or finalized, logic is done to free the GCHandle of the ToggleRef and remove the last ref, so native will finally cleanup after.
Usual leaks mean that the refcount > 1, usually because a child widget holds a managed reference to its parent, or managed objects have reference between eachother.
Usually, disposing of a native object will free the toggleref, removing 1 refcount. Therefore, it's up to native to clean up. If the refcount never gets to 0, the native object will always leak.
Below, we will tackle each toolkit's ref cycle common causes and how to fix them.
Compared to other toolkits, Gtk# has a Destroy
method which is called in the toplevel and iterates all the children calling the same method.
Destroy
informs implementations that they should releases all the refs they have added.
Thus, we have 2 ways to approach these issues with Gtk:
- Using WeakReferences
- Using OnDestroyed and fixing up refs.
// MyWidget has a ref to MyButton in native.
class MyWidget : Widget
{
public MyWidget()
{
Add (new Button(this));
}
public void NotifyClicked() {}
}
class MyButton : Button
{
// MyButton has a ref to MyWidget in managed
MyWidget widget;
public MyButton (MyWidget widget)
{
this.widget = widget;
}
protected override void OnClicked() => widget.DoClicked();
}
class MyButton : Button
{
// MyButton has a ref to MyWidget in managed
WeakReference<MyWidget> widgetRef;
public MyButton (MyWidget widget)
{
this.widgetRef = new WeakReference(widget);
}
protected override void OnClicked()
{
if (widgetRef.TryGetTarget (out var widget))
widget.DoClicked();
}
}
class MyButton : Button
{
// MyButton has a ref to MyWidget in managed
MyWidget widget;
public MyButton (MyWidget widget)
{
this.widget = widget;
}
protected override void OnClicked() => widget.NotifyClicked();
protected override OnDestroyed()
{
// Clean the strong reference here.
widget = null;
}
}
It is idiomatic in C# to use event handlers, but these have a pitfall caused by how eventhandlers actually work, because they are delegates. Usually, they have a Target
and a Method
. The Method is runtime information on what is being invoked, which does not matter here. The Target
on the other hand, is the object which holds the Method
in hand.
If possible, static handlers should be used, as they are much cheaper than instance handlers (both in code maintainability and in performance), and those definitely do not leak.
class MyWidget : Gtk.Widget
{
public MyWidget()
{
var button = new Button();
// Lambda below captures this, so we have a refcycle:
// Widget -> button in native
// button -> Widget in managed
button.Clicked += (sender, args) => DoClicked();
Content = button;
}
void DoClicked() => {}
}
Static event handlers are not problematic, as they don't have a Target
object.
class MyWidget : Widget
{
public MyWidget()
{
var button = new Button();
button.Clicked += OnClicked;
Content = button;
}
// A similar implementation could be done with a lambda.
static void OnClicked(object sender, EventArgs args)
{
var button = (Button)sender;
var widget = (MyWidget)button.Parent;
widget.DoClicked();
}
void DoClicked() {}
}
class MyWidget : Widget
{
// We now hold a ref from MyWidget to button in managed too.
Button button;
public MyWidget()
{
button = new Button();
// Lambda no longer captures any state
button.Clicked += OnClicked;
Content = button;
}
void OnClicked(object sender, EventArgs args) => DoClicked();
void DoClicked() {}
protected override void OnDestroyed()
{
button.Clicked -= OnClicked;
// nulling out is not absolutely needed, since it's not a cycle, but usually good to do.
button = null;
}
}
Unlike Gtk#, Xamarin.Mac does not have a Destroy method. Therefore, we require to make refcounting bookkeeping in all the code.
But, Xamarin.Mac has the niceties of exposing the retaining semantics of every bound method/property.
There are multiple values for ArgumentSemantic, which means that every method has the chance of possibly changing the object's refcounting.
This can be useful if you want an object to be released as soon as possible, without waiting on the finalizer queue.
Given that Dispose is not called on the whole view hierarchy, it tends to get messy if the Dispose method is preferred, since manually disposing is needed everywhere.
Therefore, for Xamarin.Mac, prefer weak semantics.
// MyView has a ref to MyButton in native.
class MyView : NSView
{
public MyView()
{
AddSubView (new MyButton(this));
}
public void NotifyClicked() {}
}
class MyButton : NSButton
{
// MyButton has a ref to MyView in managed
MyView widget;
public MyButton (MyView widget)
{
this.widget = widget;
Action = new Selector("onClicked:");
Target = this;
}
[Export("onClicked:")]
public void OnClicked(NSObject target)
{
widget.NotifyClicked();
}
}
// MyView has a ref to MyButton in native.
class MyView : NSView
{
public MyView()
{
AddSubView (new MyButton(this));
}
public void NotifyClicked() {}
}
class MyButton : NSButton
{
// MyButton has a ref to MyView in managed
WeakReference<MyView> widgetRef;
public MyButton (MyView widget)
{
widgetRef = new WeakReference (widget);
Action = new Selector("onClicked:");
Target = this;
}
[Export("onClicked:")]
public void OnClicked(NSObject target)
{
if (widgetRef.TryGetTarget (out var widget))
widget.NotifyClicked();
}
}
// MyView has a ref to MyButton in native.
class MyView : NSView
{
public MyView()
{
AddSubView (new MyButton(this));
}
[Export("onClicked:")]
public void OnClicked(NSObject target) => NotifyClicked();
public void NotifyClicked() {}
}
class MyButton : NSButton
{
public MyButton (MyView widget)
{
Action = new Selector("onClicked:");
Target = widget; // this has ArgumentSemantic.Weak, so will not increase the refcount
}
}
// MyView has a ref to MyButton in native.
class MyView : NSView
{
public MyView()
{
var button = new NSButton {
Target = this,
Action = new Selector("onClicked:"),
}
AddSubView (button);
}
[Export("onClicked:")]
public void OnClicked(NSObject target) => NotifyClicked();
public void NotifyClicked() {}
}
Always prefer selectors and weakreferences as opposed to event handlers. Avoid event handlers, if possible. They are usually implemented on top of the native APIs, so you can inspect how they're implemented, so you can make your own version.
Event handlers can cause ref cycles and manually bookkeeping disposing of resources can get tedious.
Always prefer weak delegates, if available. These remove the need to manually bookkeep where to remove references from delegates.
TODO
Similar to how GCHandles are GC roots (as in tree root, a node that has its children - references - retained in memory), static contexts are also GC roots.
Therefore if an object is referenced in another static object or an event handler is subscribed to a static event, we have the same problem. Manual bookkeeping is required there too.
Bulding and Running
Writing Add-ins
MonoDevelop API
MonoDevelop Design and Architecure
- The Project Model
- Error and Exception Handling
- The Command System
- The Service Model
- The Document/View Model
MonoDevelop Coding Guides