[TIMOB-6242] Android: V8 Memory Leak: views which contain backgroundImages not based packaged image assets
| GitHub Issue | n/a |
|---|---|
| Type | Bug |
| Priority | High |
| Status | Closed |
| Resolution | Fixed |
| Resolution Date | 2012-01-09T16:47:24.000+0000 |
| Affected Version/s | Release 1.8.0.1 |
| Fix Version/s | Sprint 2011-46, Release 1.8.0.1 |
| Components | Android |
| Labels | branch-v8, module_memory, qe-testadded |
| Reporter | Bill Dawson |
| Assignee | Bill Dawson |
| Created | 2011-11-19T13:39:58.000+0000 |
| Updated | 2012-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 viawin.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();
When a packaged asset is used as the source of a background, the end result is that
nativeDecodeAsset(in BitmapFactory.cpp) is called:In that case the bitmap is always forced to be purgeable, no matter the options passed in. In other cases (sdcard, network url),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); }nativeDecodeStreamis called:In that case, purgeability is absolutely disallowed. That's thestatic 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; }falsevalue you see up there that is passed todoDecodefor theallowPurgeableparameter. If you look atdoDecode(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":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.if (!isPurgeable) { decoder->setAllocator(&javaAllocator); }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();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.
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
Reopening/closing to add/remove labels