Titanium JIRA Archive
Titanium SDK/CLI (TIMOB)

[TIMOB-15885] iOS: Memory leak on iOS ListView when template contains event listener

GitHub Issuen/a
TypeBug
PriorityLow
StatusClosed
ResolutionDuplicate
Resolution Date2014-02-18T22:39:46.000+0000
Affected Version/sRelease 3.1.3
Fix Version/sn/a
ComponentsiOS
LabelsSupportTeam
ReporterPhilippe Wueger
AssigneeIngo Muschenetz
Created2013-11-23T21:29:33.000+0000
Updated2017-03-21T16:57:11.000+0000

Description

Problem

iOS: Memory leak on iOS ListView when template contains event listener. (Similar to Android issue TIMOB-13747)

Test case

- Run the app below in Instruments and profile memory usage - With every opening of the window containing the list view, 8 TiUILabelProxy and 4 TiUIListItemProxy objects are allocated and never freed again.
var baseWin = Ti.UI.createWindow({backgroundColor: 'red'});

var b = Ti.UI.createButton({
	title: 'Click',
	color: 'red',
	selectedColor: 'blue',
	height: '100dp',
	width: '150dp',
	backgroundImage: 'appicon.png',
	backgroundColor: 'green',
	style: Titanium.UI.iPhone ? Titanium.UI.iPhone.SystemButtonStyle.PLAIN : 0 
});


b.addEventListener('click', function(evt) {

	var win = Ti.UI.createWindow({backgroundColor: 'white'});

	
	// Create a custom template that displays an image on the left, 
	// then a title next to it with a subtitle below it.
	var myTemplate = {
	    childTemplates: [
	        {                            // Title 
	            type: 'Ti.UI.Label',     // Use a label for the title 
	            bindId: 'info',          // Maps to a custom info property of the item data
	            properties: {            // Sets the label properties
	                color: 'black',
	                font: { fontFamily:'Arial', fontSize: '20dp', fontWeight:'bold' },
	                left: '60dp', top: 0,
	            },
	            events: {
	            	'click': function(evt) {
	            		var item = evt.section.getItemAt(evt.itemIndex);
	            		item.info.text = 'Clicked';
	            		evt.section.updateItemAt(evt.itemIndex, item);
	            	}
	            }
	        },
	        {                            // Subtitle
	            type: 'Ti.UI.Label',     // Use a label for the subtitle
	            bindId: 'es_info',       // Maps to a custom es_info property of the item data
	            properties: {            // Sets the label properties
	                color: 'gray',
	                font: { fontFamily:'Arial', fontSize: '14dp' },
	                left: '60dp', top: '25dp',
	            }
	        }
	    ]
	};
	
	var listView = Ti.UI.createListView({
	    // Maps myTemplate dictionary to 'template' string
	    templates: { 'template': myTemplate },
	    // Use 'template', that is, the myTemplate dict created earlier
	    // for all items as long as the template property is not defined for an item.
	    defaultItemTemplate: 'template'
	});
	var sections = [];
	
	var fruitSection = Ti.UI.createListSection({ headerTitle: 'Fruits / Frutas'});
	var fruitDataSet = [
	    // the text property of info maps to the text property of the title label
	    // the text property of es_info maps to text property of the subtitle label
	    // the image property of pic maps to the image property of the image view
	    { info: {text: 'Apple'}, es_info: {text: 'Manzana'}, pic: {image: 'apple.png'}},
	    { info: {text: 'Banana'}, es_info: {text: 'Banana'}, pic: {image: 'banana.png'}}
	];
	fruitSection.setItems(fruitDataSet);
	sections.push(fruitSection);
	
	var vegSection = Ti.UI.createListSection({ headerTitle: 'Vegetables / Verduras'});
	var vegDataSet = [
	    { info: {text: 'Carrot'}, es_info: {text: 'Zanahoria'}, pic: {image: 'carrot.png'}},
	    { info: {text: 'Potato'}, es_info: {text: 'Patata'}, pic: {image: 'potato.png'}}
	];
	vegSection.setItems(vegDataSet);
	sections.push(vegSection);

	listView.setSections(sections);
	
	
	var button = Ti.UI.createButton({
		title: 'Close Window',
		color: 'black',
		height: '100dp',
		width: '150dp',
		backgroundColor: 'green',
	});
	
	button.addEventListener('click', function(evt) {
		win.close();
		win.remove(listView);
		listView = null;
		win.remove(button);
		button = null;
		win = null;
	});	
	
	win.add(listView);
	win.add(button);
	win.open();
	
});

baseWin.add(b);
baseWin.open();

Comments

  1. Mauro Parra-Miranda 2013-11-26

    Hello! Can you please remove the elements of the window, then null them after closing it? Check this sample: https://gist.github.com/mauropm/5923980 Right now, your code says "I will close this window, but I didn't null it, so I can reopen it if I want". Best, Mauro
  2. Philippe Wueger 2013-11-26

    Hi Mauro Thanks for your input. I have changed my test app in the following way:
       	button.addEventListener('click', function(evt) {
       		win.close();
       		win.remove(listView);
       		listView = null;
       		win.remove(button);
       		button = null;
       		win = null;
       	});
       
    When I do this, the leak goes away. However, if I change the event listener to
       	            events: {
       	            	'click': function(evt) {
       	            		var item = evt.section.getItemAt(evt.itemIndex);
       	            		item.info.text = 'Clicked';
       	            		evt.section.updateItemAt(evt.itemIndex, item);
       	            	}
       
    and I click on the "info" label, I have the memory leak again. The memory leak does not occur if I do not click the "info" label. Any further suggestions? (I will adjust the description in this item to reflect the new test app.) Many thanks & regards, Philippe
  3. Mauro Parra-Miranda 2013-11-26

    Hello! In the second example of click, please remove the event from the label before nulling it. Remember: in order to remove an event, you have not to use anonymous functions, but named ones, so it will be easier. Right now your object is holding in memory because it does have an event listener attached. BTW, the GC won't usually kick in all time. You need to repeat your testing a lot of times, so the memory reach a memory warning or similar, so that will force the GC to clean the memory. Best, Mauro
  4. Philippe Wueger 2013-11-26

    Hi Mauro {quote} In the second example of click, please remove the event from the label before nulling it. {quote} In my understanding, I do not have direct access to the objects created by a list item, so how can I remove the event listener on the label? Do you mean that I should remove the 'click' event listener from the template used in the ListView? I tried this (see code below), but this did not fix the problem. Any further ideas? Many thanks & regards, Philippe
       var baseWin = Ti.UI.createWindow({backgroundColor: 'red'});
       
       var b = Ti.UI.createButton({
       	title: 'Click',
       	color: 'red',
       	selectedColor: 'blue',
       	height: '100dp',
       	width: '150dp',
       	backgroundImage: 'appicon.png',
       	backgroundColor: 'green',
       	style: Titanium.UI.iPhone ? Titanium.UI.iPhone.SystemButtonStyle.PLAIN : 0 
       });
       
       
       b.addEventListener('click', function(evt) {
       
       	var win = Ti.UI.createWindow({backgroundColor: 'white'});
       	
       	var clickCallback = function(evt) {
       		var item = evt.section.getItemAt(evt.itemIndex);
       		item.info.text = 'Clicked';
       		evt.section.updateItemAt(evt.itemIndex, item);		
       	};
       
       	
       	// Create a custom template that displays an image on the left, 
       	// then a title next to it with a subtitle below it.
       	var myTemplate = {
       	    childTemplates: [
       	        {                            // Title 
       	            type: 'Ti.UI.Label',     // Use a label for the title 
       	            bindId: 'info',          // Maps to a custom info property of the item data
       	            properties: {            // Sets the label properties
       	                color: 'black',
       	                font: { fontFamily:'Arial', fontSize: '20dp', fontWeight:'bold' },
       	                left: '60dp', top: 0,
       	            },
       	            events: {
       	            	'click': clickCallback
       	            }
       	        },
       	        {                            // Subtitle
       	            type: 'Ti.UI.Label',     // Use a label for the subtitle
       	            bindId: 'es_info',       // Maps to a custom es_info property of the item data
       	            properties: {            // Sets the label properties
       	                color: 'gray',
       	                font: { fontFamily:'Arial', fontSize: '14dp' },
       	                left: '60dp', top: '25dp',
       	            }
       	        }
       	    ]
       	};
       	
       	var listView = Ti.UI.createListView({
       	    // Maps myTemplate dictionary to 'template' string
       	    templates: { 'template': myTemplate },
       	    // Use 'template', that is, the myTemplate dict created earlier
       	    // for all items as long as the template property is not defined for an item.
       	    defaultItemTemplate: 'template'
       	});
       	var sections = [];
       	
       	var fruitSection = Ti.UI.createListSection({ headerTitle: 'Fruits / Frutas'});
       	var fruitDataSet = [
       	    // the text property of info maps to the text property of the title label
       	    // the text property of es_info maps to text property of the subtitle label
       	    // the image property of pic maps to the image property of the image view
       	    { info: {text: 'Apple'}, es_info: {text: 'Manzana'}, pic: {image: 'apple.png'}},
       	    { info: {text: 'Banana'}, es_info: {text: 'Banana'}, pic: {image: 'banana.png'}}
       	];
       	fruitSection.setItems(fruitDataSet);
       	sections.push(fruitSection);
       	
       	var vegSection = Ti.UI.createListSection({ headerTitle: 'Vegetables / Verduras'});
       	var vegDataSet = [
       	    { info: {text: 'Carrot'}, es_info: {text: 'Zanahoria'}, pic: {image: 'carrot.png'}},
       	    { info: {text: 'Potato'}, es_info: {text: 'Patata'}, pic: {image: 'potato.png'}}
       	];
       	vegSection.setItems(vegDataSet);
       	sections.push(vegSection);
       
       	listView.setSections(sections);
       	
       	var button = Ti.UI.createButton({
       		title: 'Close Window',
       		color: 'black',
       		height: '100dp',
       		width: '150dp',
       		backgroundColor: 'green',
       	});
       	
       	button.addEventListener('click', function(evt) {
               win.close();
               myTemplate.childTemplates[0].events.click = null;
       		clickCallback = null;
               win.remove(listView);
               listView = null;
               win.remove(button);
               button = null;
               win = null;
       	});	
       	
       	win.add(listView);
       	win.add(button);
       	win.open();
       	
       });
       
       baseWin.add(b);
       baseWin.open();
       
  5. Lee Morris 2017-03-21

    Closing ticket as duplicate.

JSON Source