Circular References
From REALbasicWiki
A circular reference exists between objects if they reference each other, forming a loop.
Circular References cause a problem in REALbasic: Object leaks. While usually all object instances that are not used any more get freed from memory, this doesn`t automatically happen with objects having such a reference loop.
Contents |
[edit] Avoiding circular references
See if a WeakRef can be used in place of a direct reference in one place forming the reference loop.
For instance, if you have a tree structure with a parent - children relationship, where each node (item) has both an array of children rerefences and a reference to its parent, make the parent reference a WeakRef. That breaks the loop and avoids memory leaks.
[edit] Example of a circular reference and its solution
Consider a class name "Person" with the following properties:
children() as Person father as Person mother as Person
Now let's start with two initial instances:
dim adam, eve as Person adam = new Person eve = new Person
And add a child:
dim child as Person child = new Person
Link them together:
adam.children.Append child eve.children.Append child child.father = adam child.mother = eve
So far, all is good.
Now consider you want to destroy the objects again because you're finished with them. To do that, you'd set the root objects to nil:
adam = nil eve = nil
However, this does not destruct the 3 created Person objects yet (their destructor won't be called), because the objects are still referenced by each other, keeping them "in use". This is the classic situation of a circular reference.
Even worse, at this point, you have no more reference to them so that you can not even clear the loop between the objects any more.
There are two suggested solutions:
[edit] Solving the circular reference using a Release call
The classic approach is to ask the object to clear its references before forgetting about the object.
Add a method Release to the Person object:
sub Release()
father = nil
mother = nil
for each child as Person in children
child.Release
next
end sub
Note that not only the own references get cleared here, but the Release function must also be called for its own references!
Of course, the Release function must now be called before clearing the root objects as well, like this:
adam.Release adam = nil eve.Release eve = nil
[edit] Solving the circular reference using a WeakRef
The more worry-free solution is to use a WeakRef for those references leading to the reference loop:
In our case, either the children or the two parent references should use WeakRefs, but not all. If all would be made into WeakRefs, no counted references between them would exist any more and the objects would be freed right away.
I suggest the rule of always making the backward links (i.e. those linking back to the root objects) WeakRefs.
The class "Person" would then have the following properties:
children() as Person father as WeakRef // -> Person mother as WeakRef // -> Person
These links would be assigned like this:
child.father = new WeakRef (adam) child.mother = new WeakRef (eve)
To reference such an object, use the following code:
dim itsFather as Person if aPerson.father = nil then // this must be a root node (e.g. adam or eve) else itsFather = aPerson.father.Value end
With this construct, if eve and adam get set to nil, all its children objects get automatically destructed because there is no (counted) reference loop any more.
[edit] Detecting circular references
There are several steps to detect circular references in your programs:
- Notice that you have leaks, e.g. by monitoring the memory your application uses, seeing that it increases constantly, with no apparent end
- Find out which type of objects are constantly growing in numbers - those are the ones susceptible to being the cause of a leak, usually.
- Analyse the classes of the leaking objects to find out which properties are holding the circular references.
Step one is fairly easy. On Mac OS X, you can use the Utility application Activity Monitor to monitor an app's memory consumption, on Windows it's the Task Manager.
For the other two steps, we've provided some smart functions here that help you with the tasks: Use the method ObjectStatisticsChanges(), may in a Timer that fires every few seconds, to see how the number of objects grow in your application, and compare that to your expectations. If you see a certain class increasing in unexpected numbers, it may be the cause of a leak. Once you suspect a class to cause leaks, either manually inspect its properties and look for possible circular references as the Person example above shows, or use the ShowCircularRefs method on an object of the class to see if it has actual circular references.
[edit] Monitoring the increase of objects
There are two functions here. The first gathers a list of all objects in current existence. The second, when called repeatedly, returns a text describing the changes of count per object type (class). This is what you can use to tell whether certain objects keep growing in numbers unexpectedly.
Protected Sub GetObjectStatistics(ByRef typesOut() as String, ByRef countOut() as Integer)
// Originally written by Thomas Tempelmann
// Returns a list of all types of objects and their number
redim typesOut(-1)
redim countOut(-1)
dim types() as String
dim iter as Runtime.ObjectIterator = Runtime.IterateObjects
while iter.MoveNext
dim o as Introspection.TypeInfo = Introspection.GetType(iter.Current)
if o.IsClass then
dim name as String = o.FullName
types.Append name
end
wend
types.Sort
dim currType as String
dim currCount as Integer
for i as Integer = 0 to types.Ubound
dim t as String = types(i)
if t <> currType then
if currCount > 0 then
if currType.Left(14) <> "Introspection." and currType.Left(1) <> "_" then
typesOut.Append currType
countOut.Append currCount
end
end
currCount = 1
currType = t
else
currCount = currCount + 1
end if
next
End Sub
Protected Function ObjectStatisticsChanges() As String
dim l() as String
static prevStats as new Dictionary
dim t, t1() as String, prevC, c, c1() as Integer
GetObjectStatistics (t1,c1)
for i as Integer = 0 to t1.Ubound
t = t1(i)
c = c1(i)
prevC = prevStats.Lookup(t,0)
if c <> prevC then
dim diff as Integer = c - prevC
l.Append t + ": " + Format(diff,"+#")
prevStats.Value(t) = c
end
next
return Join(l,EndOfLine)
End Function
[edit] Seeing where circular references exist in an object
What follows is two methods to figure out if an object is part of a circular reference. Put these two methods into a module and make them public.
To check if a object is part of a reference loop, invoke its ShowCircularRefs member method. For instance, to check if the object "o" is involved in a circular reference, call o.ShowCircularRefs. If it has a ref loop, a MsgBox will show the properties involved in the loop.
Protected Function GetCircularRefs(extends obj as Object, result() as String, stack() as Object) As Integer
// Originally written by Thomas Tempelmann
// Note: finds no more than one circle
// Note: It may go into endless recursions leading to stack overflows with some internal classes.
// If you happen to run into such a problem, add the internal class to the systemTypes below.
#if RBVersion >= 2008 // previous versions do not support introspection
if stack.Ubound > 100 then break // this it getting suspiciously deep
dim type as Introspection.TypeInfo
type = Introspection.GetType(obj)
static systemTypes as Dictionary
if systemTypes = nil then
systemTypes = new Dictionary
systemTypes.Value("MemoryBlock") = true
//not sure if these are needed:
// systemTypes.Value("FolderItem") = true
// systemTypes.Value("Window") = true
// systemTypes.Value("Control") = true
end
if systemTypes.HasKey(type.Name) then
// skip these as they can lead to infinite recursions
return -1
end
// check if this object instance in in the stack
dim pos as Integer
pos = stack.IndexOf(obj)
if pos >= 0 then
result.Append "" // type.Name + "." + prop.Name
return stack.Ubound - pos
end
stack.Append obj
dim props() as Introspection.PropertyInfo
props = type.GetProperties( )
for each prop as Introspection.PropertyInfo in props
dim pname as String = prop.Name
if prop.CanRead and pname.LenB > 0 and pname.LeftB(1) <> "_" then
dim propType as Introspection.TypeInfo
propType = prop.PropertyType
if propType.IsArray then
dim elemType as Introspection.TypeInfo
elemType = propType.GetElementType
if elemType.IsClass then
dim elems() as Object
elems = prop.Value(obj)
// this leads to a RuntimeException in 2008r1fc3:
#if false
if propType.GetArrayRank > 1 then
// multi-dimensional
break // can`t handle this one yet!
end
#endif
if elems.Ubound(-1) >= 2 then
// multi-dimensional array
break // can`t handle this yet!
end
for each elem as Object in elems
if elem <> nil then
pos = elem.GetCircularRefs (result, stack)
if pos >= 0 then
result(result.Ubound) = type.Name + "." + prop.Name + " -> " + result(result.Ubound)
return pos - 1
end
end
next
end if
elseif propType.IsClass then
dim elem as Variant
elem = prop.Value(obj)
if not elem.IsNull then
pos = elem.GetCircularRefs (result, stack)
if pos >= 0 then
result(result.Ubound) = type.Name + "." + prop.Name + " -> " + result(result.Ubound)
return pos - 1
end
end
end
end if
next
call stack.Pop
return -1
#endif
End Function
Sub ShowCircularRefs(extends obj as Object)
dim s() as String, stack() as Object
call obj.GetCircularRefs(s, stack)
if s.Ubound >= 0 then
MsgBox "Circular Refs:"+EndOfLine+EndOfLine+Join(s, EndOfLine)
end
End Sub
