From 26982f304121cd235cc6b04b9dc3a3c87614a8bd Mon Sep 17 00:00:00 2001 From: Gary Mathews Date: Tue, 24 Mar 2020 10:25:20 -0700 Subject: [PATCH] fix(android): improve memory handling of TiBlob image methods (#11412) * implement v8 gc functionality * attempt to reclaim memory when limited * optimize TiBlob image manipulation methods for memory * update to androidx Fixes TIMOB-27695 --- .../org/appcelerator/kroll/KrollRuntime.java | 34 +++++++++ .../kroll/runtime/v8/V8Runtime.java | 6 ++ .../appcelerator/titanium/TiApplication.java | 13 ++++ .../org/appcelerator/titanium/TiBlob.java | 70 +++++++++++++------ tests/Resources/ti.blob.addontest.js | 69 ++++++++++++++++++ 5 files changed, 171 insertions(+), 21 deletions(-) create mode 100644 tests/Resources/ti.blob.addontest.js diff --git a/android/runtime/common/src/java/org/appcelerator/kroll/KrollRuntime.java b/android/runtime/common/src/java/org/appcelerator/kroll/KrollRuntime.java index b19aacaa17c..20d2f73faf7 100644 --- a/android/runtime/common/src/java/org/appcelerator/kroll/KrollRuntime.java +++ b/android/runtime/common/src/java/org/appcelerator/kroll/KrollRuntime.java @@ -13,12 +13,14 @@ import org.appcelerator.kroll.KrollExceptionHandler.ExceptionMessage; import org.appcelerator.kroll.common.Log; import org.appcelerator.kroll.common.TiMessenger; +import org.appcelerator.kroll.runtime.v8.V8Runtime; import org.appcelerator.kroll.util.KrollAssetHelper; import android.content.Context; import android.os.Handler; import android.os.Looper; import android.os.Message; +import androidx.annotation.Nullable; /** * The common Javascript runtime instance that Titanium interacts with. @@ -109,11 +111,16 @@ public static void init(Context context, KrollRuntime runtime) runtime.doInit(); } + @Nullable public static KrollRuntime getInstance() { + // TODO: Prevent this method from requiring `null` checks. return instance; } + /** + * Suggest V8 garbage collection during idle. + */ public static void suggestGC() { if (instance != null) { @@ -121,6 +128,28 @@ public static void suggestGC() } } + /** + * Force V8 garbage collection. + */ + public static void softGC() + { + // Force V8 garbage collection. + if (instance != null) { + instance.forceGC(); + } + } + + /** + * Force both V8 and JVM garbage collection. + */ + public static void hardGC() + { + softGC(); + + // Force JVM garbage collection. + System.gc(); + } + public static boolean isInitialized() { if (instance != null) { @@ -441,6 +470,11 @@ public void setGCFlag() // No-op V8 should override. } + public void forceGC() + { + // No-op V8 should override. + } + public State getRuntimeState() { return runtimeState; diff --git a/android/runtime/v8/src/java/org/appcelerator/kroll/runtime/v8/V8Runtime.java b/android/runtime/v8/src/java/org/appcelerator/kroll/runtime/v8/V8Runtime.java index 313ac98559b..4b1a6101d1f 100644 --- a/android/runtime/v8/src/java/org/appcelerator/kroll/runtime/v8/V8Runtime.java +++ b/android/runtime/v8/src/java/org/appcelerator/kroll/runtime/v8/V8Runtime.java @@ -60,6 +60,12 @@ public static boolean isEmulator() || "google_sdk".equals(Build.PRODUCT); } + @Override + public void forceGC() + { + nativeIdle(); + } + @Override public void initRuntime() { diff --git a/android/titanium/src/java/org/appcelerator/titanium/TiApplication.java b/android/titanium/src/java/org/appcelerator/titanium/TiApplication.java index 1f48d3a5adc..c4a1aeac0e6 100644 --- a/android/titanium/src/java/org/appcelerator/titanium/TiApplication.java +++ b/android/titanium/src/java/org/appcelerator/titanium/TiApplication.java @@ -425,6 +425,13 @@ public void onLowMemory() // Release all the cached images TiBlobLruCache.getInstance().evictAll(); TiImageLruCache.getInstance().evictAll(); + + // Perform hard garbage collection to reclaim memory. + KrollRuntime instance = KrollRuntime.getInstance(); + if (instance != null) { + instance.hardGC(); + } + super.onLowMemory(); } @@ -436,6 +443,12 @@ public void onTrimMemory(int level) // Release all the cached images TiBlobLruCache.getInstance().evictAll(); TiImageLruCache.getInstance().evictAll(); + + // Perform soft garbage collection to reclaim memory. + KrollRuntime instance = KrollRuntime.getInstance(); + if (instance != null) { + instance.softGC(); + } } super.onTrimMemory(level); } diff --git a/android/titanium/src/java/org/appcelerator/titanium/TiBlob.java b/android/titanium/src/java/org/appcelerator/titanium/TiBlob.java index 8951f1a66f3..c0774f2e8fb 100644 --- a/android/titanium/src/java/org/appcelerator/titanium/TiBlob.java +++ b/android/titanium/src/java/org/appcelerator/titanium/TiBlob.java @@ -17,6 +17,7 @@ import org.appcelerator.kroll.KrollDict; import org.appcelerator.kroll.KrollProxy; +import org.appcelerator.kroll.KrollRuntime; import org.appcelerator.kroll.annotations.Kroll; import org.appcelerator.kroll.common.Log; import org.appcelerator.kroll.util.KrollStreamHelper; @@ -688,14 +689,18 @@ public TiBlob imageAsCropped(Object params) } catch (OutOfMemoryError e) { TiBlobLruCache.getInstance().evictAll(); Log.e(TAG, "Unable to crop the image. Not enough memory: " + e.getMessage(), e); - return null; } catch (IllegalArgumentException e) { Log.e(TAG, "Unable to crop the image. Illegal Argument: " + e.getMessage(), e); - return null; } catch (Throwable t) { Log.e(TAG, "Unable to crop the image. Unknown exception: " + t.getMessage(), t); - return null; + } finally { + // Perform soft garbage collection to reclaim memory. + KrollRuntime instance = KrollRuntime.getInstance(); + if (instance != null) { + instance.softGC(); + } } + return null; } @Kroll.method @@ -786,14 +791,18 @@ public TiBlob imageAsResized(Number width, Number height) } catch (OutOfMemoryError e) { TiBlobLruCache.getInstance().evictAll(); Log.e(TAG, "Unable to resize the image. Not enough memory: " + e.getMessage(), e); - return null; } catch (IllegalArgumentException e) { Log.e(TAG, "Unable to resize the image. Illegal Argument: " + e.getMessage(), e); - return null; } catch (Throwable t) { Log.e(TAG, "Unable to resize the image. Unknown exception: " + t.getMessage(), t); - return null; + } finally { + // Perform soft garbage collection to reclaim memory. + KrollRuntime instance = KrollRuntime.getInstance(); + if (instance != null) { + instance.softGC(); + } } + return null; } @Kroll.method @@ -826,13 +835,10 @@ public TiBlob imageAsCompressed(Number compressionQuality) } catch (OutOfMemoryError e) { TiBlobLruCache.getInstance().evictAll(); Log.e(TAG, "Unable to get the thumbnail image. Not enough memory: " + e.getMessage(), e); - return null; } catch (IllegalArgumentException e) { Log.e(TAG, "Unable to get the thumbnail image. Illegal Argument: " + e.getMessage(), e); - return null; } catch (Throwable t) { Log.e(TAG, "Unable to get the thumbnail image. Unknown exception: " + t.getMessage(), t); - return null; } finally { // [MOD-309] Free up memory to work around issue in Android if (img != null) { @@ -840,6 +846,12 @@ public TiBlob imageAsCompressed(Number compressionQuality) img = null; } bos = null; + + // Perform soft garbage collection to reclaim memory. + KrollRuntime instance = KrollRuntime.getInstance(); + if (instance != null) { + instance.softGC(); + } } return result; @@ -923,14 +935,18 @@ public TiBlob imageAsThumbnail(Number size, @Kroll.argument(optional = true) Num } catch (OutOfMemoryError e) { TiBlobLruCache.getInstance().evictAll(); Log.e(TAG, "Unable to get the thumbnail image. Not enough memory: " + e.getMessage(), e); - return null; } catch (IllegalArgumentException e) { Log.e(TAG, "Unable to get the thumbnail image. Illegal Argument: " + e.getMessage(), e); - return null; } catch (Throwable t) { Log.e(TAG, "Unable to get the thumbnail image. Unknown exception: " + t.getMessage(), t); - return null; + } finally { + // Perform soft garbage collection to reclaim memory. + KrollRuntime instance = KrollRuntime.getInstance(); + if (instance != null) { + instance.softGC(); + } } + return null; } @Kroll.method @@ -975,14 +991,18 @@ public TiBlob imageWithAlpha() } catch (OutOfMemoryError e) { TiBlobLruCache.getInstance().evictAll(); Log.e(TAG, "Unable to get the image with alpha. Not enough memory: " + e.getMessage(), e); - return null; } catch (IllegalArgumentException e) { Log.e(TAG, "Unable to get the image with alpha. Illegal Argument: " + e.getMessage(), e); - return null; } catch (Throwable t) { Log.e(TAG, "Unable to get the image with alpha. Unknown exception: " + t.getMessage(), t); - return null; + } finally { + // Perform soft garbage collection to reclaim memory. + KrollRuntime instance = KrollRuntime.getInstance(); + if (instance != null) { + instance.softGC(); + } } + return null; } @Kroll.method @@ -1035,14 +1055,18 @@ public TiBlob imageWithRoundedCorner(Number cornerRadius, @Kroll.argument(option } catch (OutOfMemoryError e) { TiBlobLruCache.getInstance().evictAll(); Log.e(TAG, "Unable to get the image with rounded corner. Not enough memory: " + e.getMessage(), e); - return null; } catch (IllegalArgumentException e) { Log.e(TAG, "Unable to get the image with rounded corner. Illegal Argument: " + e.getMessage(), e); - return null; } catch (Throwable t) { Log.e(TAG, "Unable to get the image with rounded corner. Unknown exception: " + t.getMessage(), t); - return null; + } finally { + // Perform soft garbage collection to reclaim memory. + KrollRuntime instance = KrollRuntime.getInstance(); + if (instance != null) { + instance.softGC(); + } } + return null; } @Kroll.method @@ -1090,14 +1114,18 @@ public TiBlob imageWithTransparentBorder(Number size) } catch (OutOfMemoryError e) { TiBlobLruCache.getInstance().evictAll(); Log.e(TAG, "Unable to get the image with transparent border. Not enough memory: " + e.getMessage(), e); - return null; } catch (IllegalArgumentException e) { Log.e(TAG, "Unable to get the image with transparent border. Illegal Argument: " + e.getMessage(), e); - return null; } catch (Throwable t) { Log.e(TAG, "Unable to get the image with transparent border. Unknown exception: " + t.getMessage(), t); - return null; + } finally { + // Perform soft garbage collection to reclaim memory. + KrollRuntime instance = KrollRuntime.getInstance(); + if (instance != null) { + instance.softGC(); + } } + return null; } @Override diff --git a/tests/Resources/ti.blob.addontest.js b/tests/Resources/ti.blob.addontest.js new file mode 100644 index 00000000000..fbba217e310 --- /dev/null +++ b/tests/Resources/ti.blob.addontest.js @@ -0,0 +1,69 @@ +/* + * Appcelerator Titanium Mobile + * Copyright (c) 2020 by Axway, Inc. All Rights Reserved. + * Licensed under the terms of the Apache Public License + * Please see the LICENSE included with this distribution for details. + */ +/* eslint-env mocha */ +/* eslint no-unused-expressions: "off" */ +'use strict'; +var should = require('./utilities/assertions'); + +describe('Titanium.Blob', function () { + var win; + + afterEach(function (done) { + if (win) { + // If `win` is already closed, we're done. + let t = setTimeout(function () { + if (win) { + win = null; + done(); + } + }, 3000); + + win.addEventListener('close', function listener () { + clearTimeout(t); + + if (win) { + win.removeEventListener('close', listener); + } + win = null; + done(); + }); + win.close(); + } else { + win = null; + done(); + } + }); + + it('resize very large image', function (finish) { + + win = Ti.UI.createWindow({ backgroundColor: 'gray' }); + const img = Ti.UI.createImageView(); + + // Obtain large image blob. (8000px, 8000px) + let blob = Ti.Filesystem.getFile('large.jpg').read(); + should(blob).be.an.Object; + + win.addEventListener('open', () => { + + // Keep re-sizing the image down by 10% + for (let i = 0; i < 10; i++) { + + // De-reference original blob so it can be freed. + blob = blob.imageAsResized(blob.width / 1.1, blob.height / 1.1); + should(blob).be.an.Object; + } + + // Display re-sized image. + img.image = blob; + + finish(); + }); + + win.add(img); + win.open(); + }); +});