I have seen a lot of questions about OOM exceptions and caching lately. The developer guide has a really good article on this, but some tends to fail on implementing it in a suitable way.
Because of this I wrote an example application that demonstrates caching in an Android environment. This implementation has not yet gotten an OOM.
Look at the end of this answer for a link to the source code.
Requirements:
- Android API 2.1 or higher (I simply could not manage to get the available memory for an application in API 1.6 - that is the only piece of code that doesn't work in API 1.6)
- Android support package
Features:
- Retains the cache if there is an orientation change, using a singleton
- Use one eighth of the assigned application memory to the cache (modify if you want)
- Large bitmaps gets scaled (you can define the maximum pixels that you want to allow)
- Controls that there is an internet connection available before downloading the bitmaps
- Makes sure that you are only instantiating one task per row
- If you are flinging the
ListView
away, it simply won't download the bitmaps between
This does not include:
- Disk caching. This should be easy to implement anyway - just point to a different task that grabs the bitmaps from the disk
Sample code:
The images that are being downloaded are images (75x75) from Flickr. However, put whatever image urls you want to be processed, and the application will scale it down if it exceeds the maximum. In this application the urls are simply in a String
array.
The LruCache
has a good way to deal with bitmaps. However, in this application I put an instance of an LruCache
inside another cache class that I created in order to get the application more feasible.
Cache.java's critical stuff (the loadBitmap()
method is the most important):
public Cache(int size, int maxWidth, int maxHeight) {
// Into the constructor you add the maximum pixels
// that you want to allow in order to not scale images.
mMaxWidth = maxWidth;
mMaxHeight = maxHeight;
mBitmapCache = new LruCache<String, Bitmap>(size) {
protected int sizeOf(String key, Bitmap b) {
// Assuming that one pixel contains four bytes.
return b.getHeight() * b.getWidth() * 4;
}
};
mCurrentTasks = new ArrayList<String>();
}
/**
* Gets a bitmap from cache.
* If it is not in cache, this method will:
*
* 1: check if the bitmap url is currently being processed in the
* BitmapLoaderTask and cancel if it is already in a task (a control to see
* if it's inside the currentTasks list).
*
* 2: check if an internet connection is available and continue if so.
*
* 3: download the bitmap, scale the bitmap if necessary and put it into
* the memory cache.
*
* 4: Remove the bitmap url from the currentTasks list.
*
* 5: Notify the ListAdapter.
*
* @param mainActivity - Reference to activity object, in order to
* call notifyDataSetChanged() on the ListAdapter.
* @param imageKey - The bitmap url (will be the key).
* @param imageView - The ImageView that should get an
* available bitmap or a placeholder image.
* @param isScrolling - If set to true, we skip executing more tasks since
* the user probably has flinged away the view.
*/
public void loadBitmap(MainActivity mainActivity,
String imageKey, ImageView imageView,
boolean isScrolling) {
final Bitmap bitmap = getBitmapFromCache(imageKey);
if (bitmap != null) {
imageView.setImageBitmap(bitmap);
} else {
imageView.setImageResource(R.drawable.ic_launcher);
if (!isScrolling && !mCurrentTasks.contains(imageKey) &&
mainActivity.internetIsAvailable()) {
BitmapLoaderTask task = new BitmapLoaderTask(imageKey,
mainActivity.getAdapter());
task.execute();
}
}
}
You shouldn't need to edit anything in the Cache.java file unless you want to implement disk caching.
MainActivity.java's critical stuff:
public void onScrollStateChanged(AbsListView view, int scrollState) {
if (view.getId() == android.R.id.list) {
// Set scrolling to true only if the user has flinged the
// ListView away, hence we skip downloading a series
// of unnecessary bitmaps that the user probably
// just want to skip anyways. If we scroll slowly it
// will still download bitmaps - that means
// that the application won't wait for the user
// to lift its finger off the screen in order to
// download.
if (scrollState == SCROLL_STATE_FLING) {
mIsScrolling = true;
} else {
mIsScrolling = false;
mListAdapter.notifyDataSetChanged();
}
}
}
// Inside ListAdapter...
@Override
public View getView(final int position, View convertView, ViewGroup parent) {
View row = convertView;
final ViewHolder holder;
if (row == null) {
LayoutInflater inflater = getLayoutInflater();
row = inflater.inflate(R.layout.main_listview_row, parent, false);
holder = new ViewHolder(row);
row.setTag(holder);
} else {
holder = (ViewHolder) row.getTag();
}
final Row rowObject = getItem(position);
// Look at the loadBitmap() method description...
holder.mTextView.setText(rowObject.mText);
mCache.loadBitmap(MainActivity.this,
rowObject.mBitmapUrl, holder.mImageView,
mIsScrolling);
return row;
}
getView()
gets called very often. It's normally not a good idea to download images there if we haven't implemented a check that ensure us that we won't start an infinite amount of threads per row. Cache.java checks whether the rowObject.mBitmapUrl
already is in a task and if it is, it won't start another. Therefore, we are most likely not exceeding the work queue restriction from the AsyncTask
pool.
Download:
You can download the source code from my Google code page.
Last words:
I have tested this for a few weeks now, I haven't gotten a single OOM exception yet. I have tested this on the emulator, on my Nexus One and on my Nexus S. I have tested image urls that contain images that were in HD quality. The only bottleneck is that it takes more time to download.
There is only one possible scenario where I can imagine that the OOM will appear, and that is if we download many, really big images, and before they get scaled and put into cache, will simultaneously take up more memory and cause an OOM. But that isn't even an ideal situation anyway and it most likely won't be possible to solve in a more feasible way.
Report errors in the comments! :-)
I think best way to avoid the OutOfMemoryError
is to face it and understand it.
I made an app to intentionally cause OutOfMemoryError
, and monitor memory usage.
After I've done a lot of experiments with this App, I've got the following conclusions:
I'm gonna talk about SDK versions before Honey Comb first.
-
Bitmap is stored in native heap, but it will get garbage collected automatically, calling recycle() is needless.
-
If {VM heap size} + {allocated native heap memory} >= {VM heap size limit for the device}, and you are trying to create bitmap, OOM will be thrown.
NOTICE: VM HEAP SIZE is counted rather than VM ALLOCATED MEMORY.
-
VM Heap size will never shrink after grown, even if the allocated VM memory is shrinked.
-
So you have to keep the peak VM memory as low as possible to keep VM Heap Size from growing too big to save available memory for Bitmaps.
-
Manually call System.gc() is meaningless, the system will call it first before trying to grow the heap size.
-
Native Heap Size will never shrink too, but it's not counted for OOM, so no need to worry about it.
Then, let's talk about SDK Starts from Honey Comb.
-
Bitmap is stored in VM heap, Native memory is not counted for OOM.
-
The condition for OOM is much simpler: {VM heap size} >= {VM heap size limit for the device}.
-
So you have more available memory to create bitmap with the same heap size limit, OOM is less likely to be thrown.
Here is some of my observations about Garbage Collection and Memory Leak.
You can see it yourself in the App. If an Activity executed an AsyncTask that was still running after the Activity was destroyed, the Activity will not get garbage collected until the AsyncTask finish.
This is because AsyncTask is an instance of an anonymous inner class, it holds a reference of the Activity.
Calling AsyncTask.cancel(true) will not stop the execution if the task is blocked in an IO operation in background thread.
Callbacks are anonymous inner classes too, so if a static instance in your project holds them and do not release them, memory would be leaked.
If you scheduled a repeating or delayed task, for example a Timer, and you do not call cancel() and purge() in onPause(), memory would be leaked.