Skip to content

Commit

Permalink
refactor(android): optimize ImageView image download peformance (#13078)
Browse files Browse the repository at this point in the history
* refactor(android): optimize ImageView image download peformance

Fixes TIMOB-28538

* fix(android): wrong image can be loaded due to hash code collision

Fixes TIMOB-18786

* chore(android): improved TiDrawableReference hashing and added toString()

* chore(android): simplified ImageView image download handling

* refactor(android): image decoding; "defaultRetries" default changed from 5 to 2

* chore(android): change thread pool size to use CPU core count

* chore(android): make singleton instance creation thread safe

* chore(android): simplify ImageView loaded image handling

* chore(android): set min thread pool size to 2 for image/network downloads

* test: fix lint error in ti.ui.imageview

Co-authored-by: Gary Mathews <contact@garymathews.com>
  • Loading branch information
jquick-axway and garymathews committed Sep 23, 2021
1 parent 7041f6f commit bdf7e68
Show file tree
Hide file tree
Showing 15 changed files with 444 additions and 495 deletions.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
import org.appcelerator.kroll.util.KrollAssetHelper;
import org.appcelerator.titanium.util.TiBlobLruCache;
import org.appcelerator.titanium.util.TiFileHelper;
import org.appcelerator.titanium.util.TiImageLruCache;
import org.appcelerator.titanium.util.TiImageCache;
import org.appcelerator.titanium.util.TiResponseCache;
import org.appcelerator.titanium.util.TiUIHelper;
import org.appcelerator.titanium.util.TiWeakList;
Expand Down Expand Up @@ -388,7 +388,7 @@ public void onLowMemory()
{
// Release all the cached images
TiBlobLruCache.getInstance().evictAll();
TiImageLruCache.getInstance().evictAll();
TiImageCache.clear();

// Perform hard garbage collection to reclaim memory.
if (KrollRuntime.getInstance() != null) {
Expand All @@ -405,7 +405,7 @@ public void onTrimMemory(int level)
if (level >= TRIM_MEMORY_RUNNING_LOW) {
// Release all the cached images
TiBlobLruCache.getInstance().evictAll();
TiImageLruCache.getInstance().evictAll();
TiImageCache.clear();

// Perform soft garbage collection to reclaim memory.
if (KrollRuntime.getInstance() != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,35 @@ protected TiBaseFile(int type)
this.binary = false;
}

@Override
public boolean equals(Object value)
{
// Not equal if given null.
if (value == null) {
return false;
}

// Check if give object is same type as this instance such as TiFile, TiResourceFile, etc.
if (value.getClass().equals(getClass()) == false) {
return false;
}

// Compare native paths. (This can return null.)
String thisNativePath = nativePath();
String givenNativePath = ((TiBaseFile) value).nativePath();
if (thisNativePath == null) {
return (givenNativePath == null);
}
return thisNativePath.equals(givenNativePath);
}

@Override
public int hashCode()
{
String nativePath = nativePath();
return (nativePath != null) ? nativePath.hashCode() : 0;
}

/**
* @return true if the file is a File, false otherwise. See {@link java.io.File#isFile()} for more details.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,14 @@ public class TiBlobLruCache extends LruCache<String, Bitmap>
// Use 1/8th of the available memory for this memory cache.
private static final int cacheSize = maxMemory / 8;

protected static TiBlobLruCache _instance;
private static class InstanceHolder
{
private static final TiBlobLruCache INSTANCE = new TiBlobLruCache();
}

public static TiBlobLruCache getInstance()
{
if (_instance == null) {
_instance = new TiBlobLruCache();
}
return _instance;
return InstanceHolder.INSTANCE;
}

public TiBlobLruCache()
Expand All @@ -44,12 +44,12 @@ protected int sizeOf(String key, Bitmap bitmap)
public void addBitmapToMemoryCache(String key, Bitmap bitmap)
{
if (getBitmapFromMemCache(key) == null) {
_instance.put(key, bitmap);
getInstance().put(key, bitmap);
}
}

public Bitmap getBitmapFromMemCache(String key)
{
return _instance.get(key);
return getInstance().get(key);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
import android.os.Message;
import java.io.InputStream;
import java.lang.ref.SoftReference;
import java.lang.reflect.Constructor;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
Expand All @@ -32,6 +31,9 @@
import org.appcelerator.titanium.TiApplication;
import org.appcelerator.titanium.io.TiInputStreamWrapper;

import ti.modules.titanium.network.NetworkModule;
import ti.modules.titanium.network.TiSocketFactory;

/**
* Manages the asynchronous opening of InputStreams from URIs so that
* the resources get put into our TiResponseCache.
Expand All @@ -42,26 +44,26 @@ public class TiDownloadManager implements Handler.Callback
private static final int MSG_FIRE_DOWNLOAD_FINISHED = 1000;
private static final int MSG_FIRE_DOWNLOAD_FAILED = 1001;
private static final int TIMEOUT_IN_MILLISECONDS = 10000;
protected static TiDownloadManager _instance;
public static final int THREAD_POOL_SIZE = 2;

protected Map<String, List<SoftReference<TiDownloadListener>>> listeners = new HashMap<>();
protected List<String> downloadingURIs = Collections.synchronizedList(new ArrayList<>());
protected ExecutorService threadPool;
protected Handler handler;

private static class InstanceHolder
{
private static final TiDownloadManager INSTANCE = new TiDownloadManager();
}

public static TiDownloadManager getInstance()
{
if (_instance == null) {
_instance = new TiDownloadManager();
}
return _instance;
return InstanceHolder.INSTANCE;
}

protected TiDownloadManager()
{
handler = new Handler(Looper.getMainLooper(), this);
threadPool = Executors.newFixedThreadPool(THREAD_POOL_SIZE);
threadPool = Executors.newFixedThreadPool(Math.max(Runtime.getRuntime().availableProcessors(), 2));
}

/**
Expand Down Expand Up @@ -156,13 +158,7 @@ public InputStream blockingDownload(final URI uri) throws Exception
connection.setDoInput(true);
if (connection instanceof HttpsURLConnection) {
final HttpsURLConnection httpsConnection = (HttpsURLConnection) connection;

// NOTE: use reflection to prevent circular reference to the network module
// TODO: move TiDownloadManager into network module
final Class TiSocketFactory = Class.forName("ti.modules.titanium.network.TiSocketFactory");
final Constructor constructor = TiSocketFactory.getConstructors()[0];
final SSLSocketFactory socketFactory = (SSLSocketFactory) constructor.newInstance(null, null, 0);

final SSLSocketFactory socketFactory = new TiSocketFactory(null, null, NetworkModule.TLS_DEFAULT);
httpsConnection.setSSLSocketFactory(socketFactory);
}
if (connection instanceof HttpURLConnection) {
Expand Down Expand Up @@ -315,12 +311,14 @@ public DownloadJob(URI uri)

public void run()
{
boolean wasSuccessful = false;
try {
// Download the file/content referenced by the URI.
// Once all content has been pumped below, content will be made available via "TiResponseCache".
try (InputStream stream = blockingDownload(uri)) {
if (stream != null) {
KrollStreamHelper.pump(stream, null);
wasSuccessful = true;
}
}

Expand Down Expand Up @@ -348,15 +346,17 @@ public void run()
}
}
}

sendMessage(uri, MSG_FIRE_DOWNLOAD_FINISHED);
} catch (Exception e) {

wasSuccessful = false;
Log.e(TAG, "Exception downloading from: " + uri + "\n" + e.getMessage());
} finally {
downloadingURIs.remove(uri.toString());
}

// fire a download fail event if we are unable to download
if (wasSuccessful) {
sendMessage(uri, MSG_FIRE_DOWNLOAD_FINISHED);
} else {
sendMessage(uri, MSG_FIRE_DOWNLOAD_FAILED);
Log.e(TAG, "Exception downloading from: " + uri + "\n" + e.getMessage());
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,20 @@ public class TiFileHelper
private static HashSet<String> resourcePathCache;
private static HashSet<String> foundResourcePathCache;
private static HashSet<String> notFoundResourcePathCache;
private static TiFileHelper _instance = null;

private static class InstanceHolder
{
private static final TiFileHelper INSTANCE = new TiFileHelper(TiApplication.getInstance());
}

/**
* Creates or retrieves the TiFileHelper instance.
* @return the TiFileHelper instance.
*/
public static TiFileHelper getInstance()
{
return InstanceHolder.INSTANCE;
}

public TiFileHelper(Context context)
{
Expand Down Expand Up @@ -112,18 +125,6 @@ public TiFileHelper(Context context)
}
}

/**
* Creates or retrieves the TiFileHelper instance.
* @return the TiFileHelper instance.
*/
public static TiFileHelper getInstance()
{
if (_instance == null) {
_instance = new TiFileHelper(TiApplication.getInstance());
}
return _instance;
}

public InputStream openInputStream(String path, boolean report) throws IOException
{
InputStream is = null;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/**
* Appcelerator Titanium Mobile
* Copyright (c) 2021 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.
*/
package org.appcelerator.titanium.util;

import android.graphics.Bitmap;
import org.appcelerator.kroll.KrollRuntime;
import org.appcelerator.titanium.view.TiDrawableReference;
import java.lang.ref.SoftReference;
import java.util.HashMap;

public final class TiImageCache
{
private static final HashMap<TiDrawableReference.Key, SoftReference<Bitmap>> bitmapCollection = new HashMap<>(64);
private static final HashMap<TiDrawableReference.Key, TiExifOrientation> orientationCollection = new HashMap<>(64);

static
{
KrollRuntime.addOnDisposingListener((KrollRuntime runtime) -> {
clear();
});
}

private TiImageCache()
{
}

public static synchronized void add(TiImageInfo imageInfo)
{
if ((imageInfo != null) && (imageInfo.getKey() != null) && (imageInfo.getBitmap() != null)) {
Bitmap bitmap = imageInfo.getBitmap();
if ((bitmap != null) && !bitmap.isRecycled()) {
bitmapCollection.put(imageInfo.getKey(), new SoftReference<>(imageInfo.getBitmap()));
orientationCollection.put(imageInfo.getKey(), imageInfo.getOrientation());
}
}
}

public static synchronized Bitmap getBitmap(TiDrawableReference.Key key)
{
var bitmapRef = bitmapCollection.get(key);
if (bitmapRef != null) {
var bitmap = bitmapRef.get();
if ((bitmap != null) && !bitmap.isRecycled()) {
return bitmap;
} else {
remove(key);
}
}
return null;
}

public static synchronized TiExifOrientation getOrientation(TiDrawableReference.Key key)
{
return orientationCollection.get(key);
}

public static synchronized void clear()
{
bitmapCollection.clear();
orientationCollection.clear();
}

private static synchronized void remove(TiDrawableReference.Key key)
{
bitmapCollection.remove(key);
orientationCollection.remove(key);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/**
* Appcelerator Titanium Mobile
* Copyright (c) 2021 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.
*/
package org.appcelerator.titanium.util;

import android.graphics.Bitmap;
import org.appcelerator.titanium.view.TiDrawableReference;

public class TiImageInfo
{
private final TiDrawableReference.Key key;
private final Bitmap bitmap;
private final TiExifOrientation orientation;

public TiImageInfo(TiDrawableReference.Key key, Bitmap bitmap, TiExifOrientation orientation)
{
this.key = key;
this.bitmap = bitmap;
this.orientation = orientation;
}

@Override
public boolean equals(Object value)
{
if (value instanceof TiImageInfo) {
return ((TiImageInfo) value).key.equals(this.key);
}
return false;
}

@Override
public int hashCode()
{
return (this.key != null) ? this.key.hashCode() : 0;
}

public TiDrawableReference.Key getKey()
{
return this.key;
}

public Bitmap getBitmap()
{
return this.bitmap;
}

public TiExifOrientation getOrientation()
{
return this.orientation;
}
}
Loading

0 comments on commit bdf7e68

Please sign in to comment.