From 35d2b160aa95074154d363f14c6d14ae4c46ea40 Mon Sep 17 00:00:00 2001 From: Appcelerator Build Date: Mon, 7 Jun 2021 15:33:27 -0400 Subject: [PATCH] fix(android): improve pre-loading optimization of ListView and TableView (#12870) Fixes TIMOB-28439 --- .../ui/widget/listview/ListItemProxy.java | 1 - .../ui/widget/listview/ListViewHolder.java | 2 +- .../ui/widget/listview/TiListView.java | 35 +++++++-- .../ui/widget/tableview/TiTableView.java | 36 +++++++-- .../kroll/runtime/v8/ReferenceTable.java | 5 +- .../titanium/util/TiDownloadManager.java | 78 +++++++++++-------- 6 files changed, 108 insertions(+), 49 deletions(-) diff --git a/android/modules/ui/src/java/ti/modules/titanium/ui/widget/listview/ListItemProxy.java b/android/modules/ui/src/java/ti/modules/titanium/ui/widget/listview/ListItemProxy.java index ce47308ee93..6971283020e 100644 --- a/android/modules/ui/src/java/ti/modules/titanium/ui/widget/listview/ListItemProxy.java +++ b/android/modules/ui/src/java/ti/modules/titanium/ui/widget/listview/ListItemProxy.java @@ -292,7 +292,6 @@ public void call(Object data) final KrollDict childTemplate = new KrollDict((HashMap) o); final TiViewProxy childView = generateViewFromTemplate(null, childTemplate); if (childView != null) { - childView.setActivity(parent.getActivity()); parent.add(childView); } } diff --git a/android/modules/ui/src/java/ti/modules/titanium/ui/widget/listview/ListViewHolder.java b/android/modules/ui/src/java/ti/modules/titanium/ui/widget/listview/ListViewHolder.java index d88c0529b6d..bbc8312ede5 100644 --- a/android/modules/ui/src/java/ti/modules/titanium/ui/widget/listview/ListViewHolder.java +++ b/android/modules/ui/src/java/ti/modules/titanium/ui/widget/listview/ListViewHolder.java @@ -237,7 +237,7 @@ public void bind(final ListItemProxy proxy, final boolean selected) // Only set header on first row in section. setHeaderFooter(listViewProxy, sectionProperties, true, false); } - if ((indexInSection >= section.getItems().length - 1) + if ((indexInSection >= section.getItemCount() - 1) || (filteredIndex >= section.getFilteredItemCount() - 1) || proxy.isPlaceholder()) { diff --git a/android/modules/ui/src/java/ti/modules/titanium/ui/widget/listview/TiListView.java b/android/modules/ui/src/java/ti/modules/titanium/ui/widget/listview/TiListView.java index 0df7247e750..abb2388e136 100644 --- a/android/modules/ui/src/java/ti/modules/titanium/ui/widget/listview/TiListView.java +++ b/android/modules/ui/src/java/ti/modules/titanium/ui/widget/listview/TiListView.java @@ -20,6 +20,8 @@ import android.graphics.drawable.Drawable; import android.graphics.drawable.ShapeDrawable; import android.graphics.drawable.shapes.RectShape; +import android.os.Handler; +import android.os.SystemClock; import android.view.MotionEvent; import android.view.View; @@ -42,8 +44,8 @@ public class TiListView extends TiSwipeRefreshLayout implements OnSearchChangeLi { private static final String TAG = "TiListView"; - private static final int CACHE_SIZE = 8; - private static final int PRELOAD_SIZE = CACHE_SIZE / 2; + private static final int CACHE_SIZE = 32; + private static final int PRELOAD_INTERVAL = 800; private final ListViewAdapter adapter; private final DividerItemDecoration decoration; @@ -573,12 +575,33 @@ public void update() // Pre-load items of empty list. if (shouldPreload) { - final int preloadSize = Math.min(this.items.size(), PRELOAD_SIZE); + final Handler handler = new Handler(); + final long startTime = SystemClock.elapsedRealtime(); - for (int i = 0; i < preloadSize; i++) { + for (int i = 0; i < Math.min(this.items.size(), PRELOAD_INTERVAL / 8); i++) { + final ListItemProxy item = this.items.get(i); - // Pre-load views for smooth initial scroll. - this.items.get(i).getOrCreateView(); + // Fill event queue with pre-load attempts. + handler.postDelayed(new Runnable() + { + @Override + public void run() + { + final long currentTime = SystemClock.elapsedRealtime(); + final long delta = currentTime - startTime; + + // Only pre-load views for a maximum period of time. + // This prevents over-taxing older devices. + if (delta <= PRELOAD_INTERVAL + && recyclerView.getLastTouchX() == 0 + && recyclerView.getLastTouchY() == 0) { + + // While there is no user interaction; + // pre-load views for smooth initial scroll. + item.getOrCreateView(); + } + } + }, 8); // Pre-load at 120Hz to prevent noticeable UI blocking. } } diff --git a/android/modules/ui/src/java/ti/modules/titanium/ui/widget/tableview/TiTableView.java b/android/modules/ui/src/java/ti/modules/titanium/ui/widget/tableview/TiTableView.java index fd405884f2d..622a34618ef 100644 --- a/android/modules/ui/src/java/ti/modules/titanium/ui/widget/tableview/TiTableView.java +++ b/android/modules/ui/src/java/ti/modules/titanium/ui/widget/tableview/TiTableView.java @@ -20,6 +20,8 @@ import android.graphics.drawable.Drawable; import android.graphics.drawable.ShapeDrawable; import android.graphics.drawable.shapes.RectShape; +import android.os.Handler; +import android.os.SystemClock; import android.view.MotionEvent; import android.view.View; @@ -47,8 +49,8 @@ public class TiTableView extends TiSwipeRefreshLayout implements OnSearchChangeL { private static final String TAG = "TiTableView"; - private static final int CACHE_SIZE = 8; - private static final int PRELOAD_SIZE = CACHE_SIZE / 2; + private static final int CACHE_SIZE = 32; + private static final int PRELOAD_INTERVAL = 800; private final TableViewAdapter adapter; private final DividerItemDecoration decoration; @@ -529,13 +531,35 @@ public void update() this.proxy.fireEvent(TiC.EVENT_NO_RESULTS, null); } + // Pre-load items of empty list. if (shouldPreload) { - final int preloadSize = Math.min(this.rows.size(), PRELOAD_SIZE); + final Handler handler = new Handler(); + final long startTime = SystemClock.elapsedRealtime(); - for (int i = 0; i < preloadSize; i++) { + for (int i = 0; i < Math.min(this.rows.size(), PRELOAD_INTERVAL / 8); i++) { + final TableViewRowProxy row = this.rows.get(i); - // Pre-load views for smooth initial scroll. - this.rows.get(i).getOrCreateView(); + // Fill event queue with pre-load attempts. + handler.postDelayed(new Runnable() + { + @Override + public void run() + { + final long currentTime = SystemClock.elapsedRealtime(); + final long delta = currentTime - startTime; + + // Only pre-load views for a maximum period of time. + // This prevents over-taxing older devices. + if (delta <= PRELOAD_INTERVAL + && recyclerView.getLastTouchX() == 0 + && recyclerView.getLastTouchY() == 0) { + + // While there is no user interaction; + // pre-load views for smooth initial scroll. + row.getOrCreateView(); + } + } + }, 8); // Pre-load at 120Hz to prevent noticeable UI blocking. } } diff --git a/android/runtime/v8/src/java/org/appcelerator/kroll/runtime/v8/ReferenceTable.java b/android/runtime/v8/src/java/org/appcelerator/kroll/runtime/v8/ReferenceTable.java index dabeb5790a3..587a9ca604d 100644 --- a/android/runtime/v8/src/java/org/appcelerator/kroll/runtime/v8/ReferenceTable.java +++ b/android/runtime/v8/src/java/org/appcelerator/kroll/runtime/v8/ReferenceTable.java @@ -23,7 +23,7 @@ public final class ReferenceTable * A simple Map used to hold strong/weak reference to the Java objects we have paired/wrapped in native * titanium::Proxy/JavaObject instances. */ - private static final HashMap references = new HashMap<>(); + private static final HashMap references = new HashMap<>(1024); /** * Incrementing key, used to generate new keys when a new strong reference is created. FIXME Handle "wrapping" the @@ -77,7 +77,6 @@ public static void makeWeakReference(long key) { Log.d(TAG, "Downgrading to weak reference for key: " + key, Log.DEBUG_MODE); Object ref = getReference(key); - references.remove(key); references.put(key, new WeakReference<>(ref)); } @@ -90,7 +89,6 @@ public static void makeSoftReference(long key) { Log.d(TAG, "Downgrading to soft reference for key: " + key, Log.DEBUG_MODE); Object ref = getReference(key); - references.remove(key); references.put(key, new SoftReference<>(ref)); } @@ -105,7 +103,6 @@ public static Object clearReference(long key) { Log.d(TAG, "Upgrading reference to strong for key: " + key, Log.DEBUG_MODE); Object ref = getReference(key); - references.remove(key); references.put(key, ref); return ref; } diff --git a/android/titanium/src/java/org/appcelerator/titanium/util/TiDownloadManager.java b/android/titanium/src/java/org/appcelerator/titanium/util/TiDownloadManager.java index 2ddad956d6a..5b783058453 100644 --- a/android/titanium/src/java/org/appcelerator/titanium/util/TiDownloadManager.java +++ b/android/titanium/src/java/org/appcelerator/titanium/util/TiDownloadManager.java @@ -17,8 +17,11 @@ import java.net.URL; import java.net.URLConnection; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.Iterator; +import java.util.List; +import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicReference; @@ -42,8 +45,8 @@ public class TiDownloadManager implements Handler.Callback protected static TiDownloadManager _instance; public static final int THREAD_POOL_SIZE = 2; - protected HashMap>> listeners = new HashMap<>(); - protected ArrayList downloadingURIs = new ArrayList<>(); + protected Map>> listeners = new HashMap<>(); + protected List downloadingURIs = Collections.synchronizedList(new ArrayList<>()); protected ExecutorService threadPool; protected Handler handler; @@ -242,42 +245,53 @@ private void sendMessage(URI uri, int what) protected void startDownload(URI uri, TiDownloadListener listener) { String key = uri.toString(); - ArrayList> listenerList = null; + List> listenerList; + synchronized (listeners) { - if (!listeners.containsKey(key)) { - listenerList = new ArrayList>(); + listenerList = listeners.get(key); + + if (listenerList == null) { + listenerList = new ArrayList<>(); listeners.put(key, listenerList); - } else { - listenerList = listeners.get(key); } - // We only allow a listener once per URI - for (SoftReference l : listenerList) { - if (l.get() == listener) { + } + + synchronized (listenerList) + { + for (Iterator> i = listenerList.iterator(); i.hasNext(); ) { + TiDownloadListener downloadListener = i.next().get(); + + if (downloadListener == listener) { return; } } - listenerList.add(new SoftReference(listener)); + listenerList.add(new SoftReference<>(listener)); } - synchronized (downloadingURIs) - { - if (!downloadingURIs.contains(key)) { - downloadingURIs.add(key); - threadPool.execute(new DownloadJob(uri)); - } + + if (downloadingURIs.add(key)) { + threadPool.execute(new DownloadJob(uri)); } } protected void handleFireDownloadMessage(URI uri, int what) { - ArrayList> listenerList; + List> listenerList; + synchronized (listeners) { listenerList = listeners.get(uri.toString()); } - if (listenerList != null) { - for (Iterator> i = listenerList.iterator(); i.hasNext();) { + + if (listenerList == null) { + return; + } + + synchronized (listenerList) + { + for (Iterator> i = listenerList.iterator(); i.hasNext(); ) { TiDownloadListener downloadListener = i.next().get(); + if (downloadListener != null) { if (what == MSG_FIRE_DOWNLOAD_FINISHED) { downloadListener.downloadTaskFinished(uri); @@ -310,20 +324,25 @@ public void run() } } - synchronized (downloadingURIs) - { - downloadingURIs.remove(uri.toString()); - } + downloadingURIs.remove(uri.toString()); // If there is additional background task, run it here. - ArrayList> listenerList; + + List> listenerList; + synchronized (listeners) { listenerList = listeners.get(uri.toString()); + + if (listenerList != null) { + listenerList = new ArrayList<>(listenerList); + } } + if (listenerList != null) { - for (Iterator> i = listenerList.iterator(); i.hasNext();) { - TiDownloadListener downloadListener = i.next().get(); + for (SoftReference listener : listenerList) { + TiDownloadListener downloadListener = listener.get(); + if (downloadListener != null) { downloadListener.postDownload(uri); } @@ -333,10 +352,7 @@ public void run() sendMessage(uri, MSG_FIRE_DOWNLOAD_FINISHED); } catch (Exception e) { - synchronized (downloadingURIs) - { - downloadingURIs.remove(uri.toString()); - } + downloadingURIs.remove(uri.toString()); // fire a download fail event if we are unable to download sendMessage(uri, MSG_FIRE_DOWNLOAD_FAILED);