Rewrite the app art caching and fetching (again!) to finally address OOM problems and speed up art loading

This commit is contained in:
Cameron Gutman 2015-02-27 01:16:06 -05:00
parent 194037ff41
commit 80d8c5953e
8 changed files with 98 additions and 128 deletions

View File

@ -67,10 +67,6 @@ dependencies {
compile group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.51' compile group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.51'
compile group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '1.51' compile group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '1.51'
compile group: 'com.google.android', name: 'support-v4', version:'r7'
compile group: 'com.koushikdutta.ion', name: 'ion', version:'2.0.5'
compile group: 'com.google.code.gson', name: 'gson', version:'2.3.1'
compile group: 'com.squareup.okhttp', name: 'okhttp', version:'2.2.0' compile group: 'com.squareup.okhttp', name: 'okhttp', version:'2.2.0'
compile group: 'com.squareup.okio', name:'okio', version:'1.2.0' compile group: 'com.squareup.okio', name:'okio', version:'1.2.0'

Binary file not shown.

View File

@ -2,10 +2,12 @@ package com.limelight.grid;
import android.app.Activity; import android.app.Activity;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.util.DisplayMetrics;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import com.limelight.AppView; import com.limelight.AppView;
import com.limelight.LimeLog;
import com.limelight.R; import com.limelight.R;
import com.limelight.grid.assets.CachedAppAssetLoader; import com.limelight.grid.assets.CachedAppAssetLoader;
import com.limelight.grid.assets.DiskAssetLoader; import com.limelight.grid.assets.DiskAssetLoader;
@ -26,6 +28,10 @@ import java.util.concurrent.ConcurrentHashMap;
public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> { public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
private final Activity activity; private final Activity activity;
private static final int ART_WIDTH_PX = 300;
private static final int SMALL_WIDTH_DP = 100;
private static final int LARGE_WIDTH_DP = 150;
private final CachedAppAssetLoader loader; private final CachedAppAssetLoader loader;
private final ConcurrentHashMap<WeakReference<ImageView>, CachedAppAssetLoader.LoaderTuple> loadingTuples = new ConcurrentHashMap<>(); private final ConcurrentHashMap<WeakReference<ImageView>, CachedAppAssetLoader.LoaderTuple> loadingTuples = new ConcurrentHashMap<>();
private final ConcurrentHashMap<Object, CachedAppAssetLoader.LoaderTuple> backgroundLoadingTuples = new ConcurrentHashMap<>(); private final ConcurrentHashMap<Object, CachedAppAssetLoader.LoaderTuple> backgroundLoadingTuples = new ConcurrentHashMap<>();
@ -33,8 +39,26 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
public AppGridAdapter(Activity activity, boolean listMode, boolean small, ComputerDetails computer, String uniqueId) throws KeyManagementException, NoSuchAlgorithmException { public AppGridAdapter(Activity activity, boolean listMode, boolean small, ComputerDetails computer, String uniqueId) throws KeyManagementException, NoSuchAlgorithmException {
super(activity, listMode ? R.layout.simple_row : (small ? R.layout.app_grid_item_small : R.layout.app_grid_item), R.drawable.image_loading); super(activity, listMode ? R.layout.simple_row : (small ? R.layout.app_grid_item_small : R.layout.app_grid_item), R.drawable.image_loading);
int dpi = activity.getResources().getDisplayMetrics().densityDpi;
int dp;
if (small) {
dp = SMALL_WIDTH_DP;
}
else {
dp = LARGE_WIDTH_DP;
}
double scalingDivisor = ART_WIDTH_PX / (dp * (dpi / 160));
if (scalingDivisor < 1.0) {
// We don't want to make them bigger before draw-time
scalingDivisor = 1.0;
}
LimeLog.info("Art scaling divisor: " + scalingDivisor);
this.activity = activity; this.activity = activity;
this.loader = new CachedAppAssetLoader(computer, uniqueId, new NetworkAssetLoader(context), this.loader = new CachedAppAssetLoader(computer, uniqueId, scalingDivisor,
new NetworkAssetLoader(context, uniqueId),
new MemoryAssetLoader(), new DiskAssetLoader(context.getCacheDir())); new MemoryAssetLoader(), new DiskAssetLoader(context.getCacheDir()));
} }
@ -102,7 +126,7 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
} }
@Override @Override
public void notifyLoadComplete(Object object, final Bitmap bitmap) { public void notifyLoadComplete(Object object, Bitmap bitmap) {
final WeakReference<ImageView> viewRef = (WeakReference<ImageView>) object; final WeakReference<ImageView> viewRef = (WeakReference<ImageView>) object;
loadingTuples.remove(viewRef); loadingTuples.remove(viewRef);
@ -117,12 +141,13 @@ public class AppGridAdapter extends GenericGridAdapter<AppView.AppObject> {
return; return;
} }
final Bitmap viewBmp = bitmap;
activity.runOnUiThread(new Runnable() { activity.runOnUiThread(new Runnable() {
@Override @Override
public void run() { public void run() {
ImageView view = viewRef.get(); ImageView view = viewRef.get();
if (view != null) { if (view != null) {
view.setImageBitmap(bitmap); view.setImageBitmap(viewBmp);
fadeInImage(view); fadeInImage(view);
} }
} }

View File

@ -1,10 +1,12 @@
package com.limelight.grid.assets; package com.limelight.grid.assets;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import com.limelight.nvstream.http.ComputerDetails; import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.NvApp; import com.limelight.nvstream.http.NvApp;
import java.io.InputStream;
import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
@ -12,21 +14,34 @@ import java.util.concurrent.TimeUnit;
public class CachedAppAssetLoader { public class CachedAppAssetLoader {
private final ComputerDetails computer; private final ComputerDetails computer;
private final String uniqueId; private final String uniqueId;
private final double scalingDivider;
private final ThreadPoolExecutor foregroundExecutor = new ThreadPoolExecutor(8, 8, Long.MAX_VALUE, TimeUnit.DAYS, new LinkedBlockingQueue<Runnable>()); private final ThreadPoolExecutor foregroundExecutor = new ThreadPoolExecutor(8, 8, Long.MAX_VALUE, TimeUnit.DAYS, new LinkedBlockingQueue<Runnable>());
private final ThreadPoolExecutor backgroundExecutor = new ThreadPoolExecutor(2, 2, Long.MAX_VALUE, TimeUnit.DAYS, new LinkedBlockingQueue<Runnable>()); private final ThreadPoolExecutor backgroundExecutor = new ThreadPoolExecutor(2, 2, Long.MAX_VALUE, TimeUnit.DAYS, new LinkedBlockingQueue<Runnable>());
private final NetworkLoader networkLoader; private final NetworkAssetLoader networkLoader;
private final CachedLoader memoryLoader; private final MemoryAssetLoader memoryLoader;
private final CachedLoader diskLoader; private final DiskAssetLoader diskLoader;
public CachedAppAssetLoader(ComputerDetails computer, String uniqueId, NetworkLoader networkLoader, CachedLoader memoryLoader, CachedLoader diskLoader) { public CachedAppAssetLoader(ComputerDetails computer, String uniqueId, double scalingDivider,
NetworkAssetLoader networkLoader, MemoryAssetLoader memoryLoader,
DiskAssetLoader diskLoader) {
this.computer = computer; this.computer = computer;
this.uniqueId = uniqueId; this.uniqueId = uniqueId;
this.scalingDivider = scalingDivider;
this.networkLoader = networkLoader; this.networkLoader = networkLoader;
this.memoryLoader = memoryLoader; this.memoryLoader = memoryLoader;
this.diskLoader = diskLoader; this.diskLoader = diskLoader;
} }
private static Bitmap scaleBitmapAndRecyle(Bitmap bmp, double scalingDivider) {
Bitmap newBmp = Bitmap.createScaledBitmap(bmp, (int)(bmp.getWidth() / scalingDivider),
(int)(bmp.getHeight() / scalingDivider), true);
if (newBmp != bmp) {
bmp.recycle();
}
return newBmp;
}
private Runnable createLoaderRunnable(final LoaderTuple tuple, final Object context, final LoadListener listener) { private Runnable createLoaderRunnable(final LoaderTuple tuple, final Object context, final LoadListener listener) {
return new Runnable() { return new Runnable() {
@Override @Override
@ -36,7 +51,7 @@ public class CachedAppAssetLoader {
return; return;
} }
Bitmap bmp = diskLoader.loadBitmapFromCache(tuple); Bitmap bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
if (bmp == null) { if (bmp == null) {
// Notify the listener that this may take a while // Notify the listener that this may take a while
listener.notifyLongLoad(context); listener.notifyLongLoad(context);
@ -48,20 +63,24 @@ public class CachedAppAssetLoader {
return; return;
} }
bmp = networkLoader.loadBitmap(tuple); InputStream in = networkLoader.getBitmapStream(tuple);
if (bmp != null) { if (in != null) {
break; // Write the stream straight to disk
diskLoader.populateCacheWithStream(tuple, in);
// Read it back scaled
bmp = diskLoader.loadBitmapFromCache(tuple, (int) scalingDivider);
if (bmp != null) {
break;
}
} }
// Wait 1 second with a bit of fuzz // Wait 1 second with a bit of fuzz
try { try {
Thread.sleep((int) (1000 + (Math.random()*500))); Thread.sleep((int) (1000 + (Math.random() * 500)));
} catch (InterruptedException e) {} } catch (InterruptedException e) {
} break;
}
if (bmp != null) {
// Populate the disk cache
diskLoader.populateCache(tuple, bmp);
} }
} }
@ -95,7 +114,7 @@ public class CachedAppAssetLoader {
} }
private LoaderTuple loadBitmapWithContext(NvApp app, Object context, LoadListener listener, boolean background) { private LoaderTuple loadBitmapWithContext(NvApp app, Object context, LoadListener listener, boolean background) {
LoaderTuple tuple = new LoaderTuple(computer, uniqueId, app); LoaderTuple tuple = new LoaderTuple(computer, app);
// First, try the memory cache in the current context // First, try the memory cache in the current context
Bitmap bmp = memoryLoader.loadBitmapFromCache(tuple); Bitmap bmp = memoryLoader.loadBitmapFromCache(tuple);
@ -125,15 +144,13 @@ public class CachedAppAssetLoader {
public class LoaderTuple { public class LoaderTuple {
public final ComputerDetails computer; public final ComputerDetails computer;
public final String uniqueId;
public final NvApp app; public final NvApp app;
public boolean notified; public boolean notified;
public boolean cancelled; public boolean cancelled;
public LoaderTuple(ComputerDetails computer, String uniqueId, NvApp app) { public LoaderTuple(ComputerDetails computer, NvApp app) {
this.computer = computer; this.computer = computer;
this.uniqueId = uniqueId;
this.app = app; this.app = app;
} }
@ -150,15 +167,6 @@ public class CachedAppAssetLoader {
} }
} }
public interface NetworkLoader {
public Bitmap loadBitmap(LoaderTuple tuple);
}
public interface CachedLoader {
public Bitmap loadBitmapFromCache(LoaderTuple tuple);
public void populateCache(LoaderTuple tuple, Bitmap bitmap);
}
public interface LoadListener { public interface LoadListener {
// Notifies that the load didn't hit any cache and is about to be dispatched // Notifies that the load didn't hit any cache and is about to be dispatched
// over the network // over the network

View File

@ -11,20 +11,21 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
public class DiskAssetLoader implements CachedAppAssetLoader.CachedLoader { public class DiskAssetLoader {
private final File cacheDir; private final File cacheDir;
public DiskAssetLoader(File cacheDir) { public DiskAssetLoader(File cacheDir) {
this.cacheDir = cacheDir; this.cacheDir = cacheDir;
} }
@Override public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple, int sampleSize) {
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple) {
InputStream in = null; InputStream in = null;
Bitmap bmp = null; Bitmap bmp = null;
try { try {
in = CacheHelper.openCacheFileForInput(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png"); in = CacheHelper.openCacheFileForInput(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
bmp = BitmapFactory.decodeStream(in); BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = sampleSize;
bmp = BitmapFactory.decodeStream(in, null, options);
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
} finally { } finally {
@ -42,13 +43,11 @@ public class DiskAssetLoader implements CachedAppAssetLoader.CachedLoader {
return bmp; return bmp;
} }
@Override public void populateCacheWithStream(CachedAppAssetLoader.LoaderTuple tuple, InputStream input) {
public void populateCache(CachedAppAssetLoader.LoaderTuple tuple, Bitmap bitmap) {
OutputStream out = null; OutputStream out = null;
try { try {
// PNG ignores quality setting
out = CacheHelper.openCacheFileForOutput(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png"); out = CacheHelper.openCacheFileForOutput(cacheDir, "boxart", tuple.computer.uuid.toString(), tuple.app.getAppId() + ".png");
bitmap.compress(Bitmap.CompressFormat.PNG, 0, out); CacheHelper.writeInputStreamToOutputStream(input, out);
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
} finally { } finally {

View File

@ -5,9 +5,9 @@ import android.util.LruCache;
import com.limelight.LimeLog; import com.limelight.LimeLog;
public class MemoryAssetLoader implements CachedAppAssetLoader.CachedLoader { public class MemoryAssetLoader {
private static final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); private static final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
private static final LruCache<String, Bitmap> memoryCache = new LruCache<String, Bitmap>(maxMemory / 8) { private static final LruCache<String, Bitmap> memoryCache = new LruCache<String, Bitmap>(maxMemory / 12) {
@Override @Override
protected int sizeOf(String key, Bitmap bitmap) { protected int sizeOf(String key, Bitmap bitmap) {
// Sizeof returns kilobytes // Sizeof returns kilobytes
@ -19,7 +19,6 @@ public class MemoryAssetLoader implements CachedAppAssetLoader.CachedLoader {
return tuple.computer.uuid.toString()+"-"+tuple.app.getAppId(); return tuple.computer.uuid.toString()+"-"+tuple.app.getAppId();
} }
@Override
public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple) { public Bitmap loadBitmapFromCache(CachedAppAssetLoader.LoaderTuple tuple) {
Bitmap bmp = memoryCache.get(constructKey(tuple)); Bitmap bmp = memoryCache.get(constructKey(tuple));
if (bmp != null) { if (bmp != null) {
@ -28,7 +27,6 @@ public class MemoryAssetLoader implements CachedAppAssetLoader.CachedLoader {
return bmp; return bmp;
} }
@Override
public void populateCache(CachedAppAssetLoader.LoaderTuple tuple, Bitmap bitmap) { public void populateCache(CachedAppAssetLoader.LoaderTuple tuple, Bitmap bitmap) {
memoryCache.put(constructKey(tuple), bitmap); memoryCache.put(constructKey(tuple), bitmap);
} }

View File

@ -2,109 +2,44 @@ package com.limelight.grid.assets;
import android.content.Context; import android.content.Context;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import com.koushikdutta.ion.Ion;
import com.limelight.LimeLog; import com.limelight.LimeLog;
import com.limelight.binding.PlatformBinding; import com.limelight.binding.PlatformBinding;
import com.limelight.nvstream.http.ComputerDetails; import com.limelight.nvstream.http.ComputerDetails;
import com.limelight.nvstream.http.LimelightCryptoProvider; import com.limelight.nvstream.http.NvHTTP;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress; import java.net.InetAddress;
import java.net.Socket;
import java.security.KeyManagementException; import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.Principal;
import java.security.PrivateKey;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import javax.net.ssl.HostnameVerifier; public class NetworkAssetLoader {
import javax.net.ssl.KeyManager;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSession;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509KeyManager;
import javax.net.ssl.X509TrustManager;
public class NetworkAssetLoader implements CachedAppAssetLoader.NetworkLoader {
private final Context context; private final Context context;
private final LimelightCryptoProvider cryptoProvider; private final String uniqueId;
private final SSLContext sslContext;
public NetworkAssetLoader(Context context) throws NoSuchAlgorithmException, KeyManagementException { public NetworkAssetLoader(Context context, String uniqueId) throws NoSuchAlgorithmException, KeyManagementException {
this.context = context; this.context = context;
this.uniqueId = uniqueId;
cryptoProvider = PlatformBinding.getCryptoProvider(context);
sslContext = SSLContext.getInstance("SSL");
sslContext.init(ourKeyman, trustAllCerts, new SecureRandom());
} }
private final TrustManager[] trustAllCerts = new TrustManager[] { public InputStream getBitmapStream(CachedAppAssetLoader.LoaderTuple tuple) {
new X509TrustManager() { NvHTTP http = new NvHTTP(getCurrentAddress(tuple.computer), uniqueId, null, PlatformBinding.getCryptoProvider(context));
public X509Certificate[] getAcceptedIssuers() {
return new X509Certificate[0];
}
public void checkClientTrusted(X509Certificate[] certs, String authType) {}
public void checkServerTrusted(X509Certificate[] certs, String authType) {}
}};
private final KeyManager[] ourKeyman = new KeyManager[] { InputStream in = null;
new X509KeyManager() { try {
public String chooseClientAlias(String[] keyTypes, in = http.getBoxArt(tuple.app);
Principal[] issuers, Socket socket) { } catch (IOException e) {}
return "Limelight-RSA";
}
public String chooseServerAlias(String keyType, Principal[] issuers, if (in != null) {
Socket socket) {
return null;
}
public X509Certificate[] getCertificateChain(String alias) {
return new X509Certificate[] {cryptoProvider.getClientCertificate()};
}
public String[] getClientAliases(String keyType, Principal[] issuers) {
return null;
}
public PrivateKey getPrivateKey(String alias) {
return cryptoProvider.getClientPrivateKey();
}
public String[] getServerAliases(String keyType, Principal[] issuers) {
return null;
}
}
};
// Ignore differences between given hostname and certificate hostname
private final HostnameVerifier hv = new HostnameVerifier() {
public boolean verify(String hostname, SSLSession session) { return true; }
};
@Override
public Bitmap loadBitmap(CachedAppAssetLoader.LoaderTuple tuple) {
// Set SSL contexts correctly to allow us to authenticate
Ion.getDefault(context).getHttpClient().getSSLSocketMiddleware().setTrustManagers(trustAllCerts);
Ion.getDefault(context).getHttpClient().getSSLSocketMiddleware().setSSLContext(sslContext);
Ion.getDefault(context).getHttpClient().getSSLSocketMiddleware().setHostnameVerifier(hv);
Ion.getDefault(context).getBitmapCache().clear();
Bitmap bmp = Ion.with(context)
.load("https://" + getCurrentAddress(tuple.computer).getHostAddress() + ":47984/appasset?uniqueid=" +
tuple.uniqueId + "&appid=" + tuple.app.getAppId() + "&AssetType=2&AssetIdx=0")
.asBitmap()
.tryGet();
if (bmp != null) {
LimeLog.info("Network asset load complete: " + tuple); LimeLog.info("Network asset load complete: " + tuple);
} }
else { else {
LimeLog.info("Network asset load failed: " + tuple); LimeLog.info("Network asset load failed: " + tuple);
} }
return bmp; return in;
} }
private static InetAddress getCurrentAddress(ComputerDetails computer) { private static InetAddress getCurrentAddress(ComputerDetails computer) {

View File

@ -38,6 +38,15 @@ public class CacheHelper {
return new BufferedOutputStream(new FileOutputStream(openPath(true, root, path))); return new BufferedOutputStream(new FileOutputStream(openPath(true, root, path)));
} }
public static void writeInputStreamToOutputStream(InputStream in, OutputStream out) throws IOException {
byte[] buf = new byte[4096];
int bytesRead;
while ((bytesRead = in.read(buf)) != -1) {
out.write(buf, 0, bytesRead);
}
}
public static String readInputStreamToString(InputStream in) throws IOException { public static String readInputStreamToString(InputStream in) throws IOException {
Reader r = new InputStreamReader(in); Reader r = new InputStreamReader(in);