Tuesday, September 21, 2010

C# Developers: Shred your forms to avoid memory leaks

If you develop an application which continues running for some time and uses several forms - you might have memory leaks you are unaware of. This might not be a big issue if your forms aren't utilizing too many system resources. In a recent project I was involved in, I had to tackle huge memory leaks due to Forms basic memory leaking faults: events.

Continue reading and view code below fold...



Try the following code to avoid or resolve similar situations: (code placed inside a form code file, using 'System.Reflection' and 'System.Collections')

protected override void OnClosed(EventArgs e)
{
    base.OnClosed(e);
    ShredForm(this);
}
/// <summary>
/// for reguralry declared event - extract compiler created delegate by accessing it as a field
/// </summary>
/// <param name="classInstance"></param>
/// <param name="eventName"></param>
/// <returns></returns>
public static Delegate GetEventHandler(object classInstance, string eventName)
{
    Type classType = classInstance.GetType();
    FieldInfo eventField = classType.GetField(eventName, BindingFlags.GetField | BindingFlags.NonPublic | BindingFlags.Instance);
    if (eventField == null) return null;
    Delegate eventDelegate = (Delegate)eventField.GetValue(classInstance);
    // eventDelegate will be null if no listeners are attached to the event
    if (eventDelegate == null)
    {
        return null;
    }

    return eventDelegate;
}
/// <summary>
/// unhook event from oWithEvent with name eventName to object toO
/// </summary>
/// <param name="oWithEvent"></param>
/// <param name="eventName"></param>
/// <param name="toO"></param>
public static void UnhookEvent(object oWithEvent, string eventName, object toO)
{
    var del = GetEventHandler(oWithEvent, eventName);
    if (del == null) return;
    ArrayList dumpList = new ArrayList();
    foreach (var di in del.GetInvocationList())
        if (di.Target == toO)
            dumpList.Add(di);
    var ei = oWithEvent.GetType().GetEvent(eventName);
    var remMi = ei.GetRemoveMethod();
    foreach (Delegate di in dumpList)
    {
        remMi.Invoke(oWithEvent, new object[] { di });
    }
}
/// <summary>
/// through reflection, enumerate events in fromO and unhook each event referenc to toO
/// </summary>
/// <param name="fromO"></param>
/// <param name="toO"></param>
public static void UnhookAllEvents(object fromO, object toO)
{
    if (fromO == null || toO == null) return;
    var fromT = fromO.GetType();
    foreach (var e in fromT.GetEvents())
        UnhookEvent(fromO, e.Name, toO);
}

/// <summary>
/// using reflection, for each field in fromO - if it references toO - set to null
/// </summary>
/// <param name="fromO"></param>
/// <param name="toO"></param>
public static void ClearReferences(object fromO, object toO)
{
    if (fromO == null || toO == null) return;
    var fromT = fromO.GetType();
    var toT = toO.GetType();
    var liT = typeof(IList);
    foreach (var fi in
        fromT.GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.FlattenHierarchy))
    {
        if (fi.FieldType.IsAssignableFrom(toT))
        {
            var fV = fi.GetValue(fromO);
            if (fV == toO)
                fi.SetValue(fromO, null);
            continue;
        }
        if (liT.IsAssignableFrom(fi.FieldType))
        {
            if (fi.FieldType.IsArray && !fi.FieldType.GetElementType().IsAssignableFrom(toT))
                continue;
            var li = (IList)fi.GetValue(fromO);
            if (li == null) continue;
            for (int i = 0; i < li.Count; i++)
                if (li[i] == toO)
                    li[i] = null;
        }
    }

}

class TwoObjects
{
    public object key;
    public object handler;
}

/// <summary>
/// clear a Control or Form's EventHandlerList to assist untaggling GC forests created by Form's designers and assist
/// in shredding the form from existance
/// </summary>
/// <param name="c"></param>
public static void RemoveAllListedEventHandles_ClearReferences(Control c)
{
    var eventsPi = c.GetType().GetProperty("Events", BindingFlags.NonPublic | BindingFlags.Instance);
    var list = (EventHandlerList)eventsPi.GetValue(c, null);
    var listHeadPi = list.GetType().GetField("head", BindingFlags.NonPublic | BindingFlags.Instance);
    var head = listHeadPi.GetValue(list);
    if (head == null) return;
    var handlerPi = head.GetType().GetField("handler", BindingFlags.NonPublic | BindingFlags.Instance);
    var keyPi = head.GetType().GetField("key", BindingFlags.NonPublic | BindingFlags.Instance);
    var nextPi = head.GetType().GetField("next", BindingFlags.NonPublic | BindingFlags.Instance);

    var li = head;
    ArrayList dumpItems = new ArrayList();
    while (li != null)
    {
        var key = keyPi.GetValue(li);
        var handler = handlerPi.GetValue(li) as Delegate;
        li = nextPi.GetValue(li);
        if (handler != null)
        {
            dumpItems.Add(new TwoObjects { key = key, handler = handler });
        }
    }
    foreach (TwoObjects tuple in dumpItems)
        list.RemoveHandler(tuple.key, (Delegate)tuple.handler);
    foreach (TwoObjects tuple in dumpItems)
    {
        var target = ((Delegate)tuple.handler).Target;
        ClearReferences(target, c);
        UnhookAllEvents(target, c);
    }
}
/// <summary>
/// clear all event handles from a regularly declared event. For forms component events, call RemoveTargetEventHandles and similiar methods
/// </summary>
/// <param name="oWithEvent"></param>
/// <param name="eventName"></param>
public static void SetEventEmpty(object oWithEvent, string eventName)
{
    var del = GetEventHandler(oWithEvent, eventName);
    if (del == null) return;
    ArrayList dumpList = new ArrayList();
    foreach (var di in del.GetInvocationList())
        dumpList.Add(di);
    var ei = oWithEvent.GetType().GetEvent(eventName);
    var remMi = ei.GetRemoveMethod();
    foreach (Delegate di in dumpList)
    {
        remMi.Invoke(oWithEvent, new object[] { di });
    }
}
/// <summary>
/// for each reflected event name - try to set as empty
/// </summary>
/// <param name="oWithEvent"></param>
public static void SetAllEventsEmpty(object oWithEvent)
{
    var fromT = oWithEvent.GetType();
    foreach (var e in fromT.GetEvents())
        SetEventEmpty(oWithEvent, e.Name);
}
/// <summary>
/// using reflection, force calling finalize method and suppress finalize for GC
/// </summary>
/// <param name="o"></param>
public static void CallFinalize(object o)
{
    var t = o.GetType();
    var m = t.GetMethod("Finalize", BindingFlags.NonPublic | BindingFlags.IgnoreCase | BindingFlags.IgnoreReturn | BindingFlags.Instance);
    if (m == null) return;
    m.Invoke(o, null);
    GC.SuppressFinalize(o);
}
public static void ShredForm(Form f)
{
    if (!f.IsDisposed)
        f.Dispose();
    RemoveAllListedEventHandles_ClearReferences(f);
    SetAllEventsEmpty(f);
    CallFinalize(f);
}

The proper use of this code would be to place it in some static utility class. Presto, form memory leaks squashed.

Cheers!

No comments:

Post a Comment