Titanium JIRA Archive
Titanium SDK/CLI (TIMOB)

[TIMOB-6242] Android: V8 Memory Leak: views which contain backgroundImages not based packaged image assets

GitHub Issuen/a
TypeBug
PriorityHigh
StatusClosed
ResolutionFixed
Resolution Date2012-01-09T16:47:24.000+0000
Affected Version/sRelease 1.8.0.1
Fix Version/sSprint 2011-46, Release 1.8.0.1
ComponentsAndroid
Labelsbranch-v8, module_memory, qe-testadded
ReporterBill Dawson
AssigneeBill Dawson
Created2011-11-19T13:39:58.000+0000
Updated2012-01-09T16:47:24.000+0000

Description

This happens only in V8, and only when the background image is based on something that is *not* a packaged asset. For example, it happens when the image is from a network url or from the device's sdcard.

Fail/Test Case

* Run the app.js below on an emulator. * Open DDMS, select the running app, and turn on heap updates (the green cylinder button). * In DDMS, click on the "Heap" tab. * In the app, click the "Show View" and "Remove View" buttons back and forth many times, and watch how the "Allocated" value on the "Heap" tab continues to grow and grow.

Expected vs Actual Behavior

Each time "Remove View" is clicked, the visible view (with the background image) is unloaded from the window via win.remove(view); and the view variable is set to null. It's *expected* that the view's memory footprint will reduce once it's removed, but what's *actually* happening is that memory usage continues to grow. (Specifically, we know that what's happening is that the bytes for the bitmap behind the backgroundImage of the view are not being released from memory.)
Titanium.UI.setBackgroundColor('#000');
var win = Titanium.UI.createWindow({  
    title:'Test',
    backgroundColor:'#000',
	exitOnClose: true
});

var view = null;

var btn1 = Ti.UI.createButton({
	title: "Show View",
	bottom: "5dp", height: "40dp", left: "10dp", width: "150dp"
});
btn1.addEventListener("click", function() {
	btn1.enabled = false;
	view = Ti.UI.createView({
		backgroundImage: "http://www.appcelerator.com.s3.amazonaws.com/blog/images/frontpage/survey_hero_11112011.png",
		top: "50dp", left: "50dp", bottom: "50dp", right: "50dp"
	});
	win.add(view);
	btn2.enabled = true;
});

var btn2 = Ti.UI.createButton({
	title: "Remove View",
	bottom: "5dp", height: "40dp", left: "165dp", width: "150dp",
	enabled: false
});
btn2.addEventListener("click", function() {
	btn2.enabled = false;
	win.remove(view);
	view = null;
	btn1.enabled = true;
});

win.add(btn1);
win.add(btn2);

win.open();

Comments

  1. Bill Dawson 2011-11-20

    When a packaged asset is used as the source of a background, the end result is that nativeDecodeAsset (in BitmapFactory.cpp) is called:
       static jobject nativeDecodeAsset(JNIEnv* env, jobject clazz,
                                        jint native_asset,    // Asset
                                        jobject padding,       // Rect
                                        jobject options) { // BitmapFactory$Options
           SkStream* stream;
           Asset* asset = reinterpret_cast<Asset*>(native_asset);
           // assets can always be rebuilt, so force this
           bool forcePurgeable = true;
       
           if (forcePurgeable || optionsPurgeable(env, options)) {
               // if we could "ref/reopen" the asset, we may not need to copy it here
               // and we could assume optionsShareable, since assets are always RO
               stream = copyAssetToStream(asset);
               if (NULL == stream) {
                   return NULL;
               }
           } else {
               // since we know we'll be done with the asset when we return, we can
               // just use a simple wrapper
               stream = new AssetStreamAdaptor(asset);
           }
           SkAutoUnref aur(stream);
           return doDecode(env, stream, padding, options, true, forcePurgeable);
       }
       
    In that case the bitmap is always forced to be purgeable, no matter the options passed in. In other cases (sdcard, network url), nativeDecodeStream is called:
       static jobject nativeDecodeStream(JNIEnv* env, jobject clazz,
                                         jobject is,       // InputStream
                                         jbyteArray storage,   // byte[]
                                         jobject padding,
                                         jobject options) {  // BitmapFactory$Options
           jobject bitmap = NULL;
           SkStream* stream = CreateJavaInputStreamAdaptor(env, is, storage);
       
           if (stream) {
               // for now we don't allow purgeable with java inputstreams
               bitmap = doDecode(env, stream, padding, options, false);
               stream->unref();
           }
           return bitmap;
       }
       
    In that case, purgeability is absolutely disallowed. That's the false value you see up there that is passed to doDecode for the allowPurgeable parameter. If you look at doDecode (too long to paste here), you'll see purgeability makes a lot of differences. I don't really understand it all and haven't looked deeper, but for example here's one snippet where it appears that if it's not purgeable then the allocator is specifically set to a "java allocator":
       if (!isPurgeable) {
       	decoder->setAllocator(&javaAllocator);
       }
       
    What I definitely don't fully understand is why it makes a difference, Rhino vs. V8. The most striking difference to me is that when I view a V8 app in MAT, I see that our view proxies are all shown to be "[Native Stack](https://skitch.com/billdawson/gkdqk/memory-analysis-var-folders-9o-9o2efpbafear0jpshi374-ti-tmp-android3582123121634602581.hprof-eclipse-users-bill-projects-eclipse-workspace)", which makes it a "GC root". In Rhino, the view proxies are not GC roots, and their paths to GC roots lead to "androidy" things like view roots and handlers. I'd imagine this difference has something to do with it.
  2. Bill Dawson 2011-11-20

    As additional test for the fix, we should be sure that we are still able to remove and re-add views successfully. To test that, run the app.js below. You should be able to remove/re-add in succession successfully and always get the blue-red-green embedding of views. Also, the "Click Me" button inside the green view should always show an alert of "Thanks".
       Titanium.UI.setBackgroundColor('#000');
       var win = Titanium.UI.createWindow({  
           title:'Test',
           backgroundColor:'#000',
       	exitOnClose: true
       });
       
       var v1, v2, v3, btn;
       
       var viewOptions = {left: "25%", right: "25%", top: "25%", bottom: "25%"};
       
       win.add(v1 = Ti.UI.createView({bottom: "50dp", left: 0, right: 0, top: 0}));
       v1.backgroundColor = "blue";
       
       v1.add(v2 = Ti.UI.createView(viewOptions));
       v2.backgroundColor = "red";
       
       v2.add(v3 = Ti.UI.createView(viewOptions));
       v3.backgroundColor = "green";
       
       v3.add(btn = Ti.UI.createButton({title: "Click me"}));
       btn.addEventListener("click", function() { alert("Thanks"); });
       
       
       var btn1 = Ti.UI.createButton({
       	title: "Remove",
       	bottom: "5dp", height: "40dp", left: "10dp", width: "150dp"
       });
       btn1.addEventListener("click", function() {
       	btn1.enabled = false;
       	win.remove(v1);
       	btn2.enabled = true;
       });
       
       var btn2 = Ti.UI.createButton({
       	title: "Re-add",
       	bottom: "5dp", height: "40dp", left: "165dp", width: "150dp",
       	enabled: false
       });
       btn2.addEventListener("click", function() {
       	btn2.enabled = false;
       	win.add(v1);
       	btn1.enabled = true;
       });
       
       win.add(btn1);
       win.add(btn2);
       
       win.open();
       
  3. Bill Dawson 2011-11-20

    Pull request ready: https://github.com/appcelerator/titanium_mobile/pull/743 Though this solution works and is probably good practice in any case, it doesn't address why there is this difference (see earlier comment) between V8 and Rhino.
  4. Michael Pettiford 2011-12-10

    Tested on Ti Studio 1.0.7.201112080131

 Ti Mob SDK 1.8.0.1.v20111209102124 v8/rhino 

OSX Lion
 emulator 2.2 Tested both test cases and verified that the expected behavior is shown
  5. Michael Pettiford 2012-01-09

    Reopening/closing to add/remove labels

JSON Source