I'm implementing an image cache system for caching downloaded image.
My strategy is based upon two-level cache:
Memory-level and disk-level.
My class is very similar to the class used in the droidfu project
My downloaded images are put into an hashmap and the Bitmap objet is
wrapped inside a SoftRererence object. Also every image is saved
permanently to the disk.
If a requested image is not found into the
Hashmap<String,SoftReference<Bitmap>> it will be searched on the disk,
readed, and then pushed back into the hashmap. Otherwise the image will be
downloaded from the network.
Since I store the images into the phisical device momery, I have added a check for preserve the device space and stay under a 1M of occupied space:
private void checkCacheUsage() {
long size = 0;
final File[] fileList = new File(mCacheDirPath).listFiles();
Arrays.sort(fileList, new Comparator<File>() {
public int compare(File f1, File f2) {
return Long.valueOf(f2.lastModified()).compareTo(
f1.lastModified());
}
});
for (File file : fileList) {
size += file.length();
if (size > MAX_DISK_CACHE_SIZE) {
file.delete();
Log.d(ImageCache.class.getSimpleName(),
"checkCacheUsage: Size exceeded " + size + "("
+ MAX_DISK_CACHE_SIZE + ") wiping older file {"+file.toString()+"}");
}
}
}
This method is called sometime afte a disk writing:
Random r = new Random();
int ra = r.nextInt(10);
if (ra % 2 == 0){
checkCacheUsage();
}
What I'd like to add is the same check on the HashMap size to prevent it will grow too much. Something like this:
private synchronized void checkMemoryCacheUsage(){
long size = 0;
for (SoftReference<Bitmap> a : cache.values()) {
final Bitmap b = a.get();
if (b != null && ! b.isRecycled()){
size += b.getRowBytes() * b.getHeight();
}
if (size > MAX_MEMORY_SIZE){
//Remove some elements from the cache
}
}
Log.d(ImageCache.class.getSimpleName(),
"checkMemoryCacheUsage: " + size + " in memory");
}
My question is:
What could be a right MAX_MEMORY_SIZE value?
Also, Is it a good approach?
A good answer also could be: "Don't do it! SoftReference is already enough"
Don't do it! SoftReference is already enough!
Actually SoftReference is designed to do exactly what you need.
Sometimes SoftReference doesn't do what you need. Then you just get rid of SoftReference and write your own memory management logic. But as far as you use SoftReference you should not be worried about memory consumption, SoftReference does it for you.
I am using one-third of the heap for Image cache.
int memoryInMB = activityManager.getMemoryClass();
long totalAppHeap = memoryInMB * 1024 * 1024;
int runtimeCacheLimit = (int)totalAppHeap/3;
By the way, about soft reference, in Android Soft references do not work as you expect. There is a platform issue that soft references are collected too early, even when there is plenty of memory free.
Check http://code-gotcha.blogspot.com/2011/09/softreference.html
I've been looking into different caching mechanisms for my scaled bitmaps, both memory and disk cache examples. The examples where to complex for my needs, so I ended up making my own bitmap memory cache using LruCache.
You can look at a working code-example here or use this code:
Memory Cache:
public class Cache {
private static LruCache<Integer, Bitmap> bitmaps = new BitmapLruCache();
public static Bitmap get(int drawableId){
Bitmap bitmap = bitmaps.get(drawableId);
if(bitmap != null){
return bitmap;
} else {
bitmap = SpriteUtil.createScaledBitmap(drawableId);
bitmaps.put(drawableId, bitmap);
return bitmap;
}
}
}
BitmapLruCache:
public class BitmapLruCache extends LruCache<Integer, Bitmap> {
private final static int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
private final static int cacheSize = maxMemory / 2;
public BitmapLruCache() {
super(cacheSize);
}
#Override
protected int sizeOf(Integer key, Bitmap bitmap) {
// The cache size will be measured in kilobytes rather than number of items.
return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
}
}
Related
I am trying to follow a 2 year old tutorial on android regarding the usage of LruCache and some samples I have googled so far have the same method which is to pass an value(int) that is converted to KiB.
final int maxMemory = (int)(Runtime.getRuntime().maxMemory() / 1024);
final int cacheSize = maxMemory / 8; //use 1/8th of what is available
imageCache = new LruCache<>(cacheSize);
However as taken from Google's documentation, the passed int value seems to be converted to bytes (from MiB):
https://developer.android.com/reference/android/util/LruCache.html
int cacheSize = 4 * 1024 * 1024; // 4MiB
LruCache<String, Bitmap> bitmapCache = new LruCache<String, Bitmap>(cacheSize) {
protected int sizeOf(String key, Bitmap value) {
return value.getByteCount();
}
}
I would like to know which one is the correct unit of measurement.
Any answers would be greatly appreciated..
An LruCache uses the method sizeOf to determine the current size of the cache, and whether or not the cache is full. (i.e., sizeOfis called on each item in the cache and added up to determine the total size). Thus, the correct value for the constructor depends on the implementation of sizeOf.
By default, sizeOf always returns 1, meaning that the int maxSize specified in the constructor is simply the number of items the cache can hold.
In the example, sizeOf has been overridden to return the number of bytes in each bitmap. Thus, the int maxSize in the constructor is the maximum number of bytes the cache should hold.
What you are following comes from https://developer.android.com/training/displaying-bitmaps/cache-bitmap.html
As you can see, the rationale is that LruCache needs an int. Because memory can be to big to address bytes with ints, it considers kilo bytes instead. So:
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
// Use 1/8th of the available memory for this memory cache.
final int cacheSize = maxMemory / 8;
But also, in the same training,
protected int sizeOf(String key, Bitmap bitmap) {
// The cache size will be measured in kilobytes rather than
// number of items.
return bitmap.getByteCount() / 1024;
}
The size of the bitmap is also expressed in kilo bytes.
In the class documentation, the author uses bytes because 4.2^20 fits in an int.
I have a gridview with thumbnails from sdcard. I use an asynctask to lazy load images. It works perfect when i scroll slowly however when I scroll too fast, different images loading on same grid item multiple times and it takes 6 7 second to finally load the correct image. I tried to check if the position of the view is visible by using getFirstVisiblePosition and getLastVisiblePosition, and this time some of the images never loads.
Did you try to cache your images ? With LruCache for exemple.
Here is the doc :
http://developer.android.com/reference/android/util/LruCache.html
And here the official tutorial
http://developer.android.com/training/displaying-bitmaps/cache-bitmap.html
Quick use to it :
LruCache<String, Bitmap> mMemoryCache;
public void onCreate(Bundle b)
{
.....
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
final int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
#Override
protected int sizeOf(String key, Bitmap bitmap) {
// The cache size will be measured in kilobytes rather than
// number of items.
return bitmap.getByteCount() / 1024;
}
};
....
}
public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (getBitmapFromMemCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
}
public Bitmap getBitmapFromMemCache(String key) {
return mMemoryCache.get(key);
}
EDIT:
If you're loading you image asynchronously you have to look this tutorial :
http://developer.android.com/training/displaying-bitmaps/process-bitmap.html
It shows you how to handle concurrency and cancel current task if one is running
Im caching my Bitmaps in GridView to LruCache. I made manager for this, see below:
private LruCache<String, Bitmap> mMemoryCache;
public LruCacheManager(){
init();
}
private void init(){
// Get max available VM memory, exceeding this amount will throw an
// OutOfMemory exception. Stored in kilobytes as LruCache takes an
// int in its constructor.
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
// Use 1/8th of the available memory for this memory cache.
final int cacheSize = maxMemory / 8;
//Log.i("ImageCache","cacheSize: " + cacheSize);
if(mMemoryCache == null){
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
#Override
protected int sizeOf(String key, Bitmap bitmap) {
// The cache size will be measured in kilobytes rather than
// number of items.
// The cache size will be measured in kilobytes rather than
// number of items.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) {
return bitmap.getByteCount() ;
} else {
return bitmap.getRowBytes() * bitmap.getHeight();
}
}
};
}
}
public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (getBitmapFromMemCache(key) == null) {
Log.i("LruCacheManager","Bitmap is getting added, " + key);
mMemoryCache.put(key, bitmap);
}
}
public Bitmap getBitmapFromMemCache(String key) {
return mMemoryCache.get(key);
}
when I call addBitmapToMemoryCache() in my AsyncTask to save Bitmaps to MemoryCache.
But when i call getBitmapFromMemoryCache() its null.
//get cached Bitmap
LruCacheManager imCache = new LruCacheManager();
String imageKey = categoryNames[position];
Bitmap cachedBm = imCache.getBitmapFromMemCache(imageKey);
//Decide whatever use cached image or not
if (cachedBm != null) {
Log.i("AdapterGridView","Using cached image, " + imageKey);
viewHolder.icon.setImageBitmap(cachedBm);
} else {
//starts Asynctask to scale pictures and show them, happens off the main thread
new AsyncTaskImageLoader(viewHolder.icon, imageKey, mContext, imCache, mThumbIds[position]).execute();
}
Which means, AsyncTask is called again and again. In AsyncTask im adding the Bitmaps to LruCache. Because returned Bitmap is null, there is no Bitmap saved in LruCache. But i have no clue why.
I also searched online and it has maybe to do something with recycling/Garbage Collector.
So how can i properly load cached images?
Any help or clarification is appriciate.
EDIT:
I call this inside BaseAdapter in getView() method. I think it has something to do with it. For the first time, each image is added to Cache, but then, the first image is added like 10 times.
First I would set an arbitrary memory size and try with 1 image. The rest looks good... If what I have below doesn't work, give us printouts of your memory, etc. You might not have any.
In my version I get memory by
final int maxMemory = (int) (Runtime.getRuntime().maxMemory());
then set it by a fraction ( I think I picked an 8th)
I do the /1024 when I return get the size of, I do not do it for setting the memory. So if you have 1/1000 of the memory you think you have, that would be the likely issue..
I am using NetworkImageView to show some covers downloaded from a remote URL and I successfully manage to cache and show them, but I want to let users set their own cover images if they want.
I tried to use setImageUrl method with Uri.fromFile(mCoverFile).toString() as arguments, but it doesn't work. Since it is a mix of remote and local images I can't switch to regular ImageViews, so I was wondering if there's any way to enable loading of local images.
I am of course aware of the ImageView's setImageBitmap method, but NetworkImageView automatically resizes the created Bitmap and also prevents View recycling in GridViews and ListViews.
UPDATE: njzk2's answer did the trick. To autoresize the Bitmap according to your View size, then just copy the ImageRequest.doParse method from Volley's source.
NetworkImageView uses ImageLoader, which in turn uses an ImageCache.
You can provide a custom ImageCache with your images, provided you use the same mechanism for keys:
return new StringBuilder(url.length() + 12).append("#W").append(maxWidth)
.append("#H").append(maxHeight).append(url).toString();
url is not tested before the actual request would be done, so no issue here.
Typically, your 'cache' could look like :
public class MyCache implements ImageLoader.ImageCache {
#Override
public Bitmap getBitmap(String key) {
if (key.contains("file://")) {
return BitmapFactory.decodeFile(key.substring(key.indexOf("file://") + 7));
} else {
// Here you can add an actual cache
return null;
}
}
#Override
public void putBitmap(String key, Bitmap bitmap) {
// Here you can add an actual cache
}
}
You use it like :
imageView.setImageUrl(Uri.fromFile(mCoverFile).toString(), new MyCache());
(This has not been actually tested and there may be some adjustments to do)
Thank you for your answer. I wrote some code based on your help.
usage: just use LocalImageCache.class as Cache. No more code to change.
private ImageLoader mLocalImageLoader;
mLocalImageLoader = new ImageLoader(mRequestQueue,
new LocalImageCache(mCtx));
NetworkImageView test = (NetworkImageView) findViewById(R.id.iv_test);
test.setImageUrl("/storage/emulated/0/DCIM/Philm/2017_03_24_01_.png", MySingleton.getInstance(this.getApplicationContext()).getLocalImageLoader());
public class LocalImageCache extends LruCache<String, Bitmap> implements ImageLoader.ImageCache {
public LocalImageCache(int maxSize) {
super(maxSize);
}
public LocalImageCache(Context ctx) {
this(getCacheSize(ctx));
}
#Override
public Bitmap getBitmap(String key) {
key = key.substring(key.indexOf("/"));
Bitmap result = get(key);
Log.d("TAG", key);
if (result == null) {
Bitmap temp = BitmapFactory.decodeFile(key);
put(key, temp);
return temp;
} else {
return result;
}
}
#Override
public void putBitmap(String key, Bitmap bitmap) {
// Here you can add an actual cache
// Never touch here
}
// 默认屏幕5倍的图片缓存
// Returns a cache size equal to approximately three screens worth of images.
public static int getCacheSize(Context ctx) {
final DisplayMetrics displayMetrics = ctx.getResources().
getDisplayMetrics();
final int screenWidth = displayMetrics.widthPixels;
final int screenHeight = displayMetrics.heightPixels;
// 4 bytes per pixel
final int screenBytes = screenWidth * screenHeight * 4;
return screenBytes * 5;
}
#Override
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes() * value.getHeight();
}
}
NetworkImageView extends ImageView. You should be able to use the same methods as a regular ImageView
image.setImageResource(R.drawable.my_image);
or
imageView.setImageBitmap(BitmapFactory.decodeFile(imagePath));
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
final int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
#Override
protected int sizeOf(String key, Bitmap bitmap) {
// The cache size will be measured in kilobytes rather than
// number of items.
return bitmap.getByteCount() / 1024;
}
};
URL url = new URL("http://s2.goodfon.ru/image/260463-1920x1200.jpg");
Bitmap bitmap = BitmapFactory.decodeStream((InputStream) url.getContent(), null, options);
if(bitmap != null)
Log.i("Success", "BITMAP IS NOT NULL");
String key = "myKey";
Log.i("Get is null", "putting myKey");
mMemoryCache.put(key, bitmap);
Bitmap newBitmap = mMemoryCache.get(key);
if(newBitmap == null)
Log.i("newBitmap", "is null");
Hello, here is a code. I get bitmap from URL successfully (Log says Bitmap is not null and I can display it easy). Then I am trying to put it into LruCache and get it back, but it return null. (Log says newBitmap is null). Where is my mistake? Please, tell me.
Android 4.1.2 Cache size 8192 Kb.
If it is 1.19 MB on disk but ~ 9 MB in memory, that means that as a compressed JPEG file, it's 1.19 MB and once you extract that into a Bitmap (uncompressed) that can be displayed, it will take up 9 MB in memory. If it's a 1920 x 1200 pixel image as suggested by the url in your code snippet, the image will take up 1920 x 1200 x 4 bytes of memory (4 bytes for each pixel to represent ARGB values from 0 to 256 times 2.3 million total pixels = 9,216,000 bytes). If you're using 1/8 of your available memory for this cache, it's possible/likely that 9MB exceeds that total memory space so the Bitmap never makes it into the cache or is evicted immediately.
You're probably going to want to downsample the image at decoding time if it's that large (using BitmapFactory.Options.inSampleSize...lot's of documentation on the web for using that if you're not already familiar).
Also, you're using Runtime.maxMemory to compute your cache size. This means you're requesting the maximum amount of memory that the whole VM is allowed to use.
http://developer.android.com/reference/java/lang/Runtime.html#maxMemory%28%29
The more common approach is the use the value given back to you by the ActivityManager.getMemoryClass() method.
Here's an example code snippet and the method definition in the docs for reference.
ActivityManager am = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
int memClassBytes = am.getMemoryClass() * 1024 * 1024;
int cacheSize = memClassBytes / 8;
mMemoryCache = new LruCache<String, Bitmap>(cacheSize)
http://developer.android.com/reference/android/app/ActivityManager.html#getMemoryClass%28%29
You can also recycle bitmaps that pops out from lrucache
final Bitmap bmp = mLruCache.put(key, data);
if (bmp != null)
bmp.recycle();
The Android example was wrong when dividing Runtime maxMemory by 1024 in the following line:
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
The unit of the maxMemory is Byte which is the same with the 'cacheSize' ('/ 8' just means it will use eighth of the available memory of the current Activity). Therefore, '/ 1024' will make the 'cacheSize' extremely small such that no bitmap can be actually 'cached' in 'mMemoryCache'.
The solution will be delete '/ 1024' in the above code.