[TIMOB-3572] Updating views on a ScrollableView does not release previously set views from memory..
| GitHub Issue | n/a | 
|---|---|
| Type | Bug | 
| Priority | Medium | 
| Status | Closed | 
| Resolution | Fixed | 
| Resolution Date | 2011-05-18T15:12:59.000+0000 | 
| Affected Version/s | n/a | 
| Fix Version/s | Sprint 2011-16 | 
| Components | iOS | 
| Labels | enterprise, ios | 
| Reporter | Fred Spencer | 
| Assignee | Blain Hamon | 
| Created | 2011-04-15T03:46:46.000+0000 | 
| Updated | 2011-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
| File | Date | Size | 
|---|---|---|
| image.png | 2011-04-15T03:46:47.000+0000 | 27324 | 
Image used for example.
ios_proxy_registration
commit: 6c9e23fb5ae2b9d838e841217193964ef4bd274a
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:
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();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)
Verified in instruments, the old imageviews are getting purged. Hooray.
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
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.