Memory leak when ListView template contains an event listener.
Titanium SDK 3.2.0 (HEAD)
Android 4.2.1
Nexus S
Only tested this on Android. Maybe it should be checked if this leak also occurs on iOS.
The code for the test case is taken from the ListItem example in the documentation. It has been extended, so that the window with the list view
- can be opened and closed as to expose the memory leak
- is a heavyweight window
var rootWin = Ti.UI.createWindow({});
var button = Ti.UI.createButton({
title: 'Open'
});
button.addEventListener('click', function(evt){
var win = Ti.UI.createWindow({backgroundColor: 'white', modal: false });
var plainTemplate = {
childTemplates: [
{ // Image justified left
type: 'Ti.UI.ImageView', // Use an image view for the image
bindId: 'pic', // Maps to a custom pic property of the item data
properties: { // Sets the image view properties
width: '50dp', height: '50dp', left: 0
}
},
{ // Title
type: 'Ti.UI.Label', // Use a label for the title
bindId: 'title', // Maps to a custom title property of the item data
properties: { // Sets the label properties
color: 'black',
font: { fontFamily:'Arial', fontSize: '20dp', fontWeight:'bold' },
left: '60dp', top: 0,
},
},
{ // Subtitle
type: 'Ti.UI.Label', // Use a label for the subtitle
bindId: 'subtitle', // Maps to a custom subtitle property of the item data
properties: { // Sets the label properties
color: 'gray',
font: { fontFamily:'Arial', fontSize: '14dp' },
left: '60dp', top: '25dp',
}
}
],
// Binds a callback to the click event, which catches events bubbled by the view subcomponents.
events: {click: toggleCheck }
};
// The following API calls are equivalent to using jQuery.extend(true, {}, oldObject)
// Copy the plainTemplate
var redTemplate = JSON.parse(JSON.stringify(plainTemplate));
// Change the text color to red
redTemplate.childTemplates[1].properties.color = 'red';
redTemplate.childTemplates[2].properties.color = 'red';
// Rebind the click event callback
redTemplate.events.click = toggleCheck;
var listView = Ti.UI.createListView({
// Maps plainTemplate to 'uncheck' and redTemplate to 'check'
templates: { 'uncheck': plainTemplate, 'check': redTemplate },
// Use 'uncheck', that is, the plainTemplate created earlier for all items
// Can be overridden by the item's template property
defaultItemTemplate: 'uncheck'
});
var tasks = [
{id: 'trash', name: 'Take Out the Trash', person: 'Yakko', icon: 'trash.png'},
{id: 'dishes', name: 'Do the Dishes', person: 'Wakko', icon: 'dishes.png'},
{id: 'doggie', name: 'Walk the Dog', person: 'Dot', icon: 'doggie.png'}
];
var data = [];
for (var i = 0; i < tasks.length; i++) {
data.push({
// Maps to the title component in the template
// Sets the text property of the Label component
title : { text: tasks[i].name },
// Maps to the subtitle component in the template
// Sets the text property of the Label component
subtitle : { text : tasks[i].person },
// Maps to the pic component in the template
// Sets the image property of the ImageView component
pic : { image : tasks[i].icon },
// Sets the regular list data properties
properties : {
itemId: tasks[i].id,
accessoryType: Ti.UI.LIST_ACCESSORY_TYPE_NONE,
}
});
}
var section = Ti.UI.createListSection();
section.setItems(data);
listView.sections = [section];
// Modified version of the itemclick
event listener
// Changes the item template rather than the list item's color property
function toggleCheck (e) {
var item = section.getItemAt(e.itemIndex);
if (item.properties.accessoryType == Ti.UI.LIST_ACCESSORY_TYPE_NONE) {
item.properties.accessoryType = Ti.UI.LIST_ACCESSORY_TYPE_CHECKMARK;
item.template = 'check';
}
else {
item.properties.accessoryType = Ti.UI.LIST_ACCESSORY_TYPE_NONE;
item.template = 'uncheck';
}
section.updateItemAt(e.itemIndex, item);
}
win.add(listView);
win.open();
});
rootWin.add(button);
rootWin.open();
Steps to reproduce:
1. Start the application
2. Create a heap dump in DDMS
3. Tap the 'open' button
4. Tap the back button
5. Cause GC
6. Create a second heap dump in DDMS
7. Do a comparison of the two heap dumps with Eclipse Memory Analyzer. The second heap dump has multiple new and living objects compared to the first heap dump, e.g. TiBaseWindowProxy, ListViewProxy, etc. This does not happen if the event listener is removed in the template.
Memory Leak occurs on iOS as well when using events on gridTemplates.
Memory is leaking because of a circular reference between section and list item (via 'toggleCheck' function). There are two possible fixes for this: 1. Break the circular reference from Java side - i.e clean up event listeners when the list view is removed from view/window. 2. Break the circular reference from Javascript side. Option 1 would cause ListViews to stop working properly with ScrollableView since ScrollableView remove and re-add children as we flip 'pages'. Our current architecture makes it difficult to add event listeners on Java side. Option 2 is quite simple. Instead of using 'section' in 'toggleCheck' function, simply use 'e.section'. This would break the circular reference and memory would not leak.
[~bhatfield] we're going to move this to the backlog. Can we get the workaround in the docs?
The work around does not seem to work, even if I have an empty function for the event it causes a leak. One step further if I have more than one template and only one of the templates has an event listener on it, even if it is never applied, it leaks, I have seen it for iOS and Android