Titanium JIRA Archive
Titanium SDK/CLI (TIMOB)

[TIMOB-3572] Updating views on a ScrollableView does not release previously set views from memory..

GitHub Issuen/a
TypeBug
PriorityMedium
StatusClosed
ResolutionFixed
Resolution Date2011-05-18T15:12:59.000+0000
Affected Version/sn/a
Fix Version/sSprint 2011-16
ComponentsiOS
Labelsenterprise, ios
ReporterFred Spencer
AssigneeBlain Hamon
Created2011-04-15T03:46:46.000+0000
Updated2011-05-18T15:12:59.000+0000

Description

Expectation:

var scrollable = Ti.UI.createScrollableView();

scrollable.views = [set of views]; // initial

scrollable.views = [new set of views]; // further in flow; release previous set of views

Steps to recreate:

1) Run on device and load Instruments (all processes)
2) Tap 'Generate Section' button to confirm memory is being released on recreation.
3) Tap 'Update Views' button to confirm memory is not being released. Application will send memory notifications and eventually crash on approx. 15-20 attempts.

var win = Ti.UI.createWindow({ backgroundColor:'#fff', top:0, right:0, bottom:0, left:0 });

var contentContainer = Ti.UI.createView({ backgroundColor:'#000' });
var sectionContainer = Ti.UI.createView({ backgroundColor:'#f00' });

var scrollable = Ti.UI.createScrollableView({ top:0, right:0, left:0, bottom:50, backgroundColor:'#ff0' });
var button1 = Ti.UI.createButton({ bottom:0, left:0, width:'40%', height:50, title:'GENERATE SECTION' });
var button2 = Ti.UI.createButton({ bottom:0, right:0, width:'40%', height:50, title:'UPDATE VIEWS' });

var sectionState = false;

var currentImages = [];

function createButton() {
    var button = Ti.UI.createButton({ left:0, right:0, height:30, bottom:0, title:'Button' });
    
    button.addEventListener('click', function() {
        Ti.API.info('Tapped button.');
    });
    
    return button;
}

// ### Border Radius Performance - toImage improves this - larger memory footprint
function generateItem(img) {
    var itemContainer = Ti.UI.createView({ width:200, height:200, top:5, left:5, bottom:5, right:5, backgroundColor:'#0f0' });
    var image = Ti.UI.createImageView({ width:200, height:100, image:img, borderRadius:5 }); // slow performance
    var comp = Ti.UI.createImageView({ width:200, height:100 });
    var title = Ti.UI.createLabel({ touchEnabled:'false', text:'Title', textAlign:'center', top:0, height:'auto' });
    var button = createButton();
    
    itemContainer.add(image);
    itemContainer.add(title);
    itemContainer.add(button);
    
    itemContainer.addEventListener('singletap', function() {
        Ti.API.info('Tapped view.');
    });

    image.addEventListener('load', function() {
        comp.image = image.toImage();
        itemContainer.remove(image);
        itemContainer.add(comp);
    });
    
    return itemContainer;
}

function createCustomView(images) {
    var view = Ti.UI.createView({ top:0, left:0, right:0, bottom:0, layout:'horizontal', backgroundColor:'#f00' });
    
    var item;
            
    for (var i = 0; i < 12; i++) {
        item = generateItem(images[i]);
        view.add(item);
    }
    
    item.addEventListener('singletap', function() {
        Ti.API.info('Tapped view.');
    });
    
    return view;
}

// SUCCESS ON RELEASE FROM MEMORY
function generateSection() {    
    if (sectionState) {
        contentContainer.remove(sectionContainer);
    }
    
    setTimeout(function() {
        var images = [];
        
        for (var i = 0; i < 12; i++) {
            images.push('image.png');
        }
        
        currentImages = images;             
        
        sectionContainer = Ti.UI.createView({ backgroundColor:'#f00' });
        contentContainer.add(sectionContainer);

        scrollable = Ti.UI.createScrollableView({ top:0, right:0, left:0, bottom:50, backgroundColor:'#ff0' });

        scrollable.views = [
            createCustomView(images),
            createCustomView(images),
            createCustomView(images),
            createCustomView(images)
        ];

        sectionContainer.add(scrollable);

        sectionState = true;
    }, 500);
}

// MEMORY ISSUES
function refreshViews() {   
    // works on simulator, crash on device (log below) - perhaps this should be done in a reverse order (remove last, first)
    // for (var i = 0, sl = scrollable.views.length; i < sl; i++) {
    //      scrollable.removeView(scrollable.views[i]);
    //  }
    // [ERROR] The application has crashed with an unhandled exception. Stack trace:
    // 
    // 0   CoreFoundation                      0x3173463d __exceptionPreprocess + 96
    // 1   libobjc.A.dylib                     0x3642dc5d objc_exception_throw + 24
    // 2   CoreFoundation                      0x31734491 +[NSException raise:format:arguments:] + 68
    // 3   CoreFoundation                      0x317344cb +[NSException raise:format:] + 34
    // 4   QuartzCore                          0x309ed61d _ZL18CALayerSetPositionP7CALayerRKN2CA4Vec2IdEEb + 140
    // 5   QuartzCore                          0x309ed58b -[CALayer setPosition:] + 38
    // 6   UIKit                               0x32e1136b -[UIView(Geometry) setCenter:] + 22
    // 7   scrollabletest2                     0x00043115 -[TiViewProxy relayout] + 632
    // 8   scrollabletest2                     0x00042673 -[TiViewProxy refreshView:] + 362
    // 9   scrollabletest2                     0x00043445 -[TiViewProxy layoutChildrenIfNeeded] + 136
    // 10  scrollabletest2                     0x0009d459 performLayoutRefresh + 344
    // 11  CoreFoundation                      0x3170ba47 __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__ + 14
    // 12  CoreFoundation                      0x3170decb __CFRunLoopDoTimer + 850
    // 13  CoreFoundation                      0x3170e845 __CFRunLoopRun + 1088
    // 14  CoreFoundation                      0x3169eec3 CFRunLoopRunSpecific + 230
    // 15  CoreFoundation                      0x3169edcb CFRunLoopRunInMode + 58
    // 16  GraphicsServices                    0x3288741f GSEventRunModal + 114
    // 17  GraphicsServices                    0x328874cb GSEventRun + 62
    // 18  UIKit                               0x32e07d69 -[UIApplication _run] + 404
    // 19  UIKit                               0x32e05807 UIApplicationMain + 670
    // 20  scrollabletest2                     0x00003bb3 main + 70
    // 21  scrollabletest2                     0x000036a8 start + 40
    // 
    // 
    // 2011-04-13 09:19:00.904 scrollabletest2[8820:707] *** Terminating app due to uncaught exception 'CALayerInvalidGeometry', reason: 'CALayer position contains NaN: [nan 4.0726e-10]'
    // *** Call stack at first throw:
    // (
    //  0   CoreFoundation                      0x3173464f __exceptionPreprocess + 114
    //  1   libobjc.A.dylib                     0x3642dc5d objc_exception_throw + 24
    //  2   CoreFoundation                      0x31734491 +[NSException raise:format:arguments:] + 68
    //  3   CoreFoundation                      0x317344cb +[NSException raise:format:] + 34
    //  4   QuartzCore                          0x309ed61d _ZL18CALayerSetPositionP7CALayerRKN2CA4Vec2IdEEb + 140
    //  5   QuartzCore                          0x309ed58b -[CALayer setPosition:] + 38
    //  6   UIKit                               0x32e1136b -[UIView(Geometry) setCenter:] + 22
    //  7   scrollabletest2                     0x00043115 -[TiViewProxy relayout] + 632
    //  8   scrollabletest2                     0x00042673 -[TiViewProxy refreshView:] + 362
    //  9   scrollabletest2                     0x00043445 -[TiViewProxy layoutChildrenIfNeeded] + 136
    //  10  scrollabletest2                     0x0009d459 performLayoutRefresh + 344
    //  11  CoreFoundation                      0x3170ba47 __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__ + 14
    //  12  CoreFoundation                      0x3170decb __CFRunLoopDoTimer + 850
    //  13  CoreFoundation                      0x3170e845 __CFRunLoopRun + 1088
    //  14  CoreFoundation                      0x3169eec3 CFRunLoopRunSpecific + 230
    //  15  CoreFoundation                      0x3169edcb CFRunLoopRunInMode + 58
    //  16  GraphicsServices                    0x3288741f GSEventRunModal + 114
    //  17  GraphicsServices                    0x328874cb GSEventRun + 62
    //  18  UIKit                               0x32e07d69 -[UIApplication _run] + 404
    //  19  UIKit                               0x32e05807 UIApplicationMain + 670
    //  20  scrollabletest2                     0x00003bb3 main + 70
    //  21  scrollabletest2                     0x000036a8 start + 40
    // )
    // terminate called after throwing an instance of 'NSException'
    
    // this seems to work, but memory jumps on regenerating section
    // for (var i = 0, sl = scrollable.views.length; i < sl; i++) {
    //      scrollable.views[i] = createCustomView(currentImages);
    // }
    
    // previous views do not seem to be released from memory
    scrollable.views = [
        createCustomView(currentImages),
        createCustomView(currentImages),
        createCustomView(currentImages),
        createCustomView(currentImages)
    ];
}

button1.addEventListener('click', function(e) {
    generateSection();
});

button2.addEventListener('click', function(e) {
    refreshViews();
});

win.add(contentContainer);
win.add(button1);
win.add(button2);

win.open();

generateSection();

Attachments

FileDateSize
image.png2011-04-15T03:46:47.000+000027324

Comments

  1. Fred Spencer 2011-04-15

    Image used for example.

  2. Fred Spencer 2011-04-15

    ios_proxy_registration
    commit: 6c9e23fb5ae2b9d838e841217193964ef4bd274a

  3. Blain Hamon 2011-04-15

    Right now setViews is rather broken in that it doesn't release the old proxies, it's true. Added to that, addView and removeView (Despite handling proxy retention properly) don't update the views property. Unfortunately, the cause lies somewhat deeper, involving the proxies owned by the view instead of the scrollableviewproxy, which makes fixing this sticky.

    In the mean time, there's two solutions:
    1) Treat setViews as a write-once action, and don't recycle a scrollableView to use one collection, then run over and use a different collection; use multiple scrollableViews.

    2) If the former is not an option, the following can be done as a workaround until scrollableView is fixed, given scrollableView foo:

       var oldViews = foo.views;
       for(var i=0; i<oldViews.length; i++){
           foo.removeView(oldViews[i]);
       }
       foo.views = [...newviews...];
       
  4. Fred Spencer 2011-04-19

    Confirmed that this provides fix for second workaround. Memory also appears to be releasing properly. Updated source (image attached, above):
     
       var win = Ti.UI.createWindow({ backgroundColor:'#fff', top:0, right:0, bottom:0, left:0 });
       
       var contentContainer = Ti.UI.createView({ backgroundColor:'#000' });
       var sectionContainer = Ti.UI.createView({ backgroundColor:'#f00' });
       
       var scrollable = Ti.UI.createScrollableView({ top:0, right:0, left:0, bottom:50, backgroundColor:'#ff0' });
       var button1 = Ti.UI.createButton({ bottom:0, left:0, width:'40%', height:50, title:'GENERATE SECTION' });
       var button2 = Ti.UI.createButton({ bottom:0, right:0, width:'40%', height:50, title:'UPDATE VIEWS' });
       
       var sectionState = false;
       
       var currentImages = [];
       
       function createButton() {
       	var button = Ti.UI.createButton({ left:0, right:0, height:30, bottom:0, title:'Button' });
       	
       	button.addEventListener('click', function() {
       		Ti.API.info('Tapped button.');
       	});
       	
       	return button;
       }
       
       // ### Border Radius Performance - toImage improves this - larger memory footprint
       function generateItem(img) {
       	var itemContainer = Ti.UI.createView({ width:200, height:200, top:5, left:5, bottom:5, right:5, backgroundColor:'#0f0' });
       	var image = Ti.UI.createImageView({ width:200, height:100, image:img, borderRadius:5 }); // slow performance
       	var comp = Ti.UI.createImageView({ width:200, height:100 });
       	var title = Ti.UI.createLabel({ touchEnabled:'false', text:'Title', textAlign:'center', top:0, height:'auto', font:{ fontSize:18, fontFamily:'Houschka Pro', fontWeight:'Bold' } });
       	var button = createButton();
       	
       	itemContainer.add(image);
       	itemContainer.add(title);
       	itemContainer.add(button);
       	
       	itemContainer.addEventListener('singletap', function() {
       		Ti.API.info('Tapped view.');
       	});
       
       	image.addEventListener('load', function() {
       		comp.image = image.toImage();
       		
       		itemContainer.remove(image);
       		//image = null;
       		
       		itemContainer.add(comp);
       	});
       	
       	return itemContainer;
       }
       
       function createCustomView(images) {
       	var view = Ti.UI.createView({ top:0, left:0, right:0, bottom:0, layout:'horizontal', backgroundColor:'#f00' });
       	
       	var item;
       			
       	for (var i = 0; i < 12; i++) {
       		item = generateItem(images[i]);
       		view.add(item);
       	}
       	
       	item.addEventListener('singletap', function() {
       		Ti.API.info('Tapped view.');
       	});
       	
       	return view;
       }
       
       // SUCCESS ON RELEASE FROM MEMORY
       function generateSection() {	
       	if (sectionState) {
       		contentContainer.remove(sectionContainer);
       		//sectionContainer = null;
       	}
       	
       	setTimeout(function() {
       		var images = [];
       		
       		for (var i = 0; i < 12; i++) {
       			images.push('image.png');
       		}
       		
       		currentImages = images;				
       		
       		sectionContainer = Ti.UI.createView({ backgroundColor:'#f00' });
       		contentContainer.add(sectionContainer);
       
       		scrollable = Ti.UI.createScrollableView({ top:0, right:0, left:0, bottom:50, backgroundColor:'#ff0' });
       
       		scrollable.views = [
       			createCustomView(images),
       			createCustomView(images),
       			createCustomView(images),
       			createCustomView(images)
       		];
       
       		sectionContainer.add(scrollable);
       
       		sectionState = true;
       	}, 500);
       }
       
       function refreshViews() {	
       	var oldViews = scrollable.views;
       	for (var i = 0, sl = oldViews.length; i < sl; i++) {
       			scrollable.removeView(oldViews[i]);
       			//oldViews[i] = null;
       	}
       	
       //	oldViews = null;
       
       	scrollable.views = [
       		createCustomView(currentImages),
       		createCustomView(currentImages),
       		createCustomView(currentImages),
       		createCustomView(currentImages)
       	];
       }
       
       button1.addEventListener('click', function(e) {
       	generateSection();
       });
       
       button2.addEventListener('click', function(e) {
       	refreshViews();
       });
       
       win.add(contentContainer);
       win.add(button1);
       win.add(button2);
       
       win.open();
       
       generateSection();
       
  5. Fred Spencer 2011-04-19

    Warnings on pressing 'Update Views' button. [WARN] Nil view frame was requested for [object TiUIView] in -[TiViewProxy refreshView:] (TiViewProxy.m:1426) [WARN] Nil view frame was requested for [object TiUIView] in -[TiViewProxy refreshView:] (TiViewProxy.m:1426) [WARN] Nil view frame was requested for [object TiUIView] in -[TiViewProxy refreshView:] (TiViewProxy.m:1426) [WARN] Nil view frame was requested for [object TiUIView] in -[TiViewProxy refreshView:] (TiViewProxy.m:1426) [WARN] Nil view frame was requested for [object TiUIView] in -[TiViewProxy refreshView:] (TiViewProxy.m:1426) [WARN] Nil view frame was requested for [object TiUIView] in -[TiViewProxy refreshView:] (TiViewProxy.m:1426)
  6. Blain Hamon 2011-04-21

    Verified in instruments, the old imageviews are getting purged. Hooray.
  7. Jick Steen 2011-05-10

    Hi Blain, is this behaviour: http://developer.appcelerator.com/question/119465/ios-regression-170-using-more-than-3-views-inside-a-scrollableview-ends-in-blank-tableviews as expected now after working on the leak management or is it a regression? Worked in SDK 1.6.1 but no longer in 1.7.0
  8. Eric Merriman 2011-05-18

    Used revised test code, instruments reports a initial state of 1.76MB. Repeatedly tapping on "Update Views" varied the usage, generally alternating between 2.3x and 1.8x MB. Closing. Note: the revised test code will crash after about 6 taps on the button with an NS exception that may be caused by trying to remove objects that are already removed. We will port the code to a test app and modify when we do to prevent this if possible.

JSON Source