5

For various Android applications, I need large ListViews, i.e. such views with 100-300 entries.

All entries must be loaded in bulk when the application is started, as some sorting and processing is necessary and the application cannot know which items to display first, otherwise.

So far, I've been loading the images for all items in bulk as well, which are then saved in an ArrayList<CustomType> together with the rest of the data for each entry.

But of course, this is not a good practice, as you're very likely to have an OutOfMemoryException then: The references to all images in the ArrayList prevent the garbage collector from working.

So the best solution is, obviously, to load only the text data in bulk whereas the images are then loaded as needed, right? The Google Play application does this, for example: You can see that images are loaded as you scroll to them, i.e. they are probably loaded in the adapter's getView() method. But with Google Play, this is a different problem, anyway, as the images must be loaded from the Internet, which is not the case for me. My problem is not that loading the images takes too long, but storing them requires too much memory.

So what should I do with the images? Load in getView(), when they are really needed? Would make scrolling sluggish. So calling an AsyncTask then? Or just a normal Thread? Parametrize it?

I could save the images that are already loaded into a HashMap<String,Bitmap>, so that they don't need to be loaded again in getView(). But if this is done, you have the memory problem again: The HashMap stores references to all images, so in the end, you could have the OutOfMemoryException again.

I know that there are already lots of questions here that discuss "Lazy loading" of images. But they mainly cover the problem of slow loading, not too much memory consumption.

Edit: I've now decided to start AsyncTasks in getView() which load the image into the ListView in the background. But this causes my application to run into an RejectedExecutionException. What should I do now?

caw
  • 30,999
  • 61
  • 181
  • 291
  • where does the text data come from? – techi.services Sep 13 '12 at 21:07
  • 2
    While lazy loading is often in the context of slow loading, it does very much apply to dealing with excessive memory usage as well. That's why good lazy loading `ListView` solutions have bounded caches. I'd recommend [`LruCache`](http://developer.android.com/reference/android/util/LruCache.html) and loading asynchronously from disk. There are a lot of solutions out there already that do this. – kabuko Sep 13 '12 at 21:23
  • Thank you! I can't use `LruCache` as I'm offering apps for API level 8+. – caw Sep 13 '12 at 21:47
  • `LruCache` is included in the Compatibility Library. – techi.services Sep 13 '12 at 22:23
  • But if I don't use the Compatibility Library, either, I can just copy the source from here, can't I? http://code.google.com/p/xlarge-demos/source/browse/trunk/PhotoAlbum/src/com/example/android/photoalbum/LruCache.java – caw Sep 13 '12 at 22:36
  • And what about memory leaks? I'm using an `Activity` with `android:launchMode="singleTask"`, so there's no risk of memory leaks with that cache, is it? If there is, should I rather use a LruCache with `SoftReferences`? (http://blog.wu-man.com/2012/01/lrucache-with-softreference-on-android.html) – caw Sep 13 '12 at 22:37
  • is this just a list of images and text that continually change as list goes up and down? does the text and image location come from a database? (i presume images are on device storage of some kind). explain more what it is you are trying to achieve. – techi.services Sep 13 '12 at 23:12
  • Yes, both text and images are stored on the device. I get everything from a database, but for the images I only get the `Uri`. So I have to use the `Uri` to load the `Bitmap`s from local storage when needed. – caw Sep 13 '12 at 23:14
  • And yes, it's a long list with unique items, i.e. you have 100-300 different images and text parts. – caw Sep 13 '12 at 23:16
  • if its all coming from a database except the bitmap why don't you use a `CursorAdapter` of, for example, 10 rows and use `onScrollListener` to initiate fetching new rows and load the bitmap(s) sequentially on a `HandlerThread`. – techi.services Sep 14 '12 at 00:01
  • @techiServices: This is basically what I want to do ;) Except that I'll use the `ListView`'s `getView()` instead of the `onScrollListener` and `AsyncTask` instead of `HandlerThread`. Or is this worse? – caw Sep 14 '12 at 00:18
  • `onScrollListener` would be used as the logic to test when a new database fetch should be initiated, i.e. end of list reached, get more data. i don't see any point duplicating data from a cursor into any array. To populate a `Cursor` and fetch a bitmap you should use a background thread. What type of background thread you use depends. I would use `HandlerThread` or `IntentService` rather than `AsyncTask`. – techi.services Sep 14 '12 at 00:40
  • Thank you! Can you explain why you would recommend those rather than `AsyncTask`? And what about `IntentService`, do I need a broadcast then which is sent when the data has loaded? Isn't this slower than `AsyncTask`? Furthermore, I don't really get why you recommend `onScrollListener` instead of `getView()` as the point to start fetching data. As it's local data, fetching it is very fast, anyway. – caw Sep 14 '12 at 00:58
  • Google have changed the way `AsyncTask` has worked 3 times and if you understand multi threading in Java you don't need it. `IntentService` uses a `HandlerThread` and takes `Intent`s of data to process and when it's completed the queue of intent data it stops itself. Similar can be achieved using just a `HandlerThread` and creating an interface for communication between it and the `Activity` or `ListView` or `Adapter`. The `OnScrollListener` is not a substitute for `getView`. It controls the logic you need to tell the background thread to get new data. `getView` then displays that data. – techi.services Sep 14 '12 at 01:23
  • 1
    Have you try with ImageLoader class ? – Shreyash Mahajan Sep 18 '12 at 05:38
  • @iDroidExplorer: Do you refer to this one? https://github.com/thest1/LazyList – caw Sep 18 '12 at 14:40
  • Yes, I have refer it and also have used before. Also have done some modification to work with it. If you not getting the answer then let me know. – Shreyash Mahajan Sep 20 '12 at 04:43
  • Thanks, it seems to be quite helpful! – caw Sep 20 '12 at 14:15
  • Can you describe the images? are these small thumbnails already of a small size? (say 48x48) or are the Uri's returning bigger images that you need to convert to a bitmap from an input stream? – pjco Sep 20 '12 at 20:22
  • It is mixed. One half of the images comes from the contacts and needs to be scaled down. The other half is already optimal size (between 72 and 144px). – caw Sep 20 '12 at 20:37

7 Answers7

9

I took the approach of loading the images with an AsyncTask and attaching the task to the view in the adapter's getView function to keep track of which task is loading in which view. I use this in an app of mine and there's no scroll lag and all images are loaded in the proper position with no exceptions being thrown. Also, because the task does no work if it's canceled, you can perform a fling on your list and it should lag up at all.

The task:

public class DecodeTask extends AsyncTask<String, Void, Bitmap> {

private static int MaxTextureSize = 2048; /* True for most devices. */

public ImageView v;

public DecodeTask(ImageView iv) {
    v = iv;
}

protected Bitmap doInBackground(String... params) {
    BitmapFactory.Options opt = new BitmapFactory.Options();
    opt.inPurgeable = true;
    opt.inPreferQualityOverSpeed = false;
    opt.inSampleSize = 0;

    Bitmap bitmap = null;
    if(isCancelled()) {
        return bitmap;
    }

    opt.inJustDecodeBounds = true;
    do {
        opt.inSampleSize++;
        BitmapFactory.decodeFile(params[0], opt);
    } while(opt.outHeight > MaxTextureSize || opt.outWidth > MaxTextureSize)
    opt.inJustDecodeBounds = false;

    bitmap = BitmapFactory.decodeFile(params[0], opt);
    return bitmap;
}

@Override
protected void onPostExecute(Bitmap result) {
    if(v != null) {
        v.setImageBitmap(result);
    }
}

}

The adapter stores an ArrayList that contains the file paths of all the images that need loaded. The getView function looks like this:

@Override
public View getView(int position, View convertView, ViewGroup parent) {
    ImageView iv = null;
    if(convertView == null) {
        convertView = getLayoutInflater().inflate(R.id.your_view, null); /* Inflate your view here */
        iv = convertView.findViewById(R.id.your_image_view);
    } else {
        iv = convertView.findViewById(R.id.your_image_view);
        DecodeTask task = (DecodeTask)iv.getTag(R.id.your_image_view);
        if(task != null) {
            task.cancel(true);
        }
    }
    iv.setImageBitmap(null);
    DecodeTask task = new DecodeTask(iv);
    task.execute(getItem(position) /* File path to image */);
    iv.setTag(R.id.your_image_view, task);

    return convertView;
}

NOTE: Just a caveat here, this might still give you memory problems on versions 1.5 - 2.3 since they use a thread pool for AsyncTask. 3.0+ go back to the serial model by default for executing AsyncTasks which keeps it to one task running at a time, thus using less memory at any given time. So long as your images aren't too big though, you should be fine.

UPDATE: While this solution will still work, there have been great additions to the open source community for solving this problem in a cleaner way. Libraries like Glide or Picasso both handle loading items in a list quite well and I'd recommend you look into one of those solutions if possible.

Michael Celey
  • 12,645
  • 6
  • 57
  • 62
  • Thank you! I've tried that and got an `RejectedExecutionException` while scrolling. Scrolling is the main problem, anyway. You `AsyncTask` will load tons of images that aren't needed anymore when there is a fling, for example. – caw Sep 21 '12 at 14:23
  • 2
    This won't load a ton of images on a fling because getView will cancel the tasks that haven't executed yet and if you look at the task, there's a check for if the task was canceled before doing any work. Canceled tasks are removed from the queue which should avoid your RejectedExecutionException. – Michael Celey Sep 21 '12 at 14:25
  • Ah, okay, failed to see that. Thank you! – caw Sep 21 '12 at 14:58
2

1) To solve your memory problem with HashMap<String, Bitmap>: Can you use WeakHashMap so that images are recycled when needed? The same should work for the ArrayList you mentioned in the beginning, if your CustomType has weak references to images.

2) What about NOT loading images while user scrolls through the list, but instead when user stops scrolling, load images at that moment. Yes, the list will not look fancy without images, but it will be very efficient during scroll, and while user scrolls he does not see details anyway. Techinally it should work like this:

listView.setOnScrollListener(new OnScrollListener() {
  @Override
  public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
      int totalItemCount) {
    // Don't care.
  }

  @Override
  public void onScrollStateChanged(final AbsListView view, int scrollState) {
    // Load images here. ListView can tell you what rows are visible, you load images for these rows and update corresponding View-s.
  }
})

3) I have an app that loads about 300 images during app start and keeps them for app lifetime. So that's an issue for me too, but so far I have seen very few OOM reports and I suspect they happened because of a different leak. I try to save some memory by using RGB_565 profile for ListView images (there's no significant difference in quality for this purpose) and I use 96x96 max image size, that should be enough for standard list item height.

smok
  • 1,658
  • 13
  • 13
  • 1
    Thank you! I've heard that beginning with Android 2.3, the garbage collector's behavior was changed so that `WeakReference`s are very likely to be collected. This means that they aren't really useful anymore for caching. Is that true? And I don't see how the `onScrollListener` can help caching the right items. You don't know which direction the user will scroll to next. But all items that are currently visible have `getView()` called, anyway, so you can do the image loading there. – caw Sep 18 '12 at 14:24
  • Not sure about GC behavior, weakreferences should stay for a while, depending on how much free memory the device has for its current tasks, of course. – smok Sep 18 '12 at 15:51
  • Regarding onScrollListener, that's just a different approach to the proble. What I suggest is to not load images (and do not display them) unless user is looking at a steady list view (NOT scrolling). You won't have to cache anything. The point here is that while user scrolls, `getView()` may be called 100 or more times, you probably don't want to load 100 images because that would make scroll janky. But when user stops scrolling, there are <10 (usually) items visible, load images and display them. `OnScrollListener` is to catch that moment when user stops scrolling and you want to load images – smok Sep 18 '12 at 15:54
  • "In the past, a popular memory cache implementation was a SoftReference or WeakReference bitmap cache, however this is not recommended. Starting from Android 2.3 (API Level 9) the garbage collector is more aggressive with collecting soft/weak references which makes them fairly ineffective." (http://developer.android.com/training/displaying-bitmaps/cache-bitmap.html) – caw Sep 18 '12 at 18:21
  • Okay, the scroll thing is right. But probably you'll want to load images even if the user is scrolling slowly. The only event that makes image loading pointless is a fling gesture, probably. – caw Sep 18 '12 at 18:22
  • 2
    To avoid Out of memory, a good idea also it to keep images in the memory not as Bitmap object, but as ByteArrayStream or just byte[] and decode them when needed. This is still quick but you can keep 10x more images in memory. – Mark Sep 19 '12 at 08:17
  • Is this really true? How can the underlying byte data have only 10% of the Bitmap's size?? – caw Sep 20 '12 at 16:08
  • The actual image stored on disk is a compressed PNG or JPEG. The bitmap allocates (by default) 4 bytes per pixel, which means 3.7MB in memory for a 1200x800 image, while storing the actual bytes of the compressed file would require perhaps a quarter of that. Not saying this is the best approach, but it could help. – Sky Kelsey Apr 02 '13 at 20:15
1

Do not store all of the images in a list because it's too heavy. You should start an AsyncTask in getView, get,decode you image in InBackground and draw an image on the imageView in PostExecute. To maintain performance of your list you could also use the convertView parameter from getView method, but it starts to be complicated with AsyncTask because your view can be recycled before AsyncTask finishes and you should handle this extra...

You could use LruCache but this only make sense when images are downloaded from internet. When they are stored localy there is no point in using it.

Mark
  • 5,466
  • 3
  • 23
  • 24
  • Thank you! Isn't there a limit of 5 concurrent `AsyncTasks`s in total or so? If so, this could cause problems with fast scrolling and subsequent calls to `getView()`. Should I only use one custom `AsyncTask` which receives the `Bitmap` input as an `Uri` (for example) in `execute(...)`? For recycling views, can't I just use `View v = super.getView(position, convertView, parent); if (v != null) { ... }` and there will be no problem? – caw Sep 13 '12 at 23:11
  • `AsyncTask` operates differently in the background over the various APIs. Its reference gives detail. I would never use it. – techi.services Sep 13 '12 at 23:15
  • Or is this the solution? http://stackoverflow.com/questions/7729133/using-asynctask-to-load-images-in-listview – caw Sep 13 '12 at 23:18
  • Or this? http://lucasr.org/2012/04/05/performance-tips-for-androids-listview/ Before I asked this question, I didn't think about handling recycled views that are not the views to be updated anymore. – caw Sep 13 '12 at 23:31
1

check this out: https://github.com/DHuckaby/Prime

I would use this for images in a ListView vs trying to solve it yourself.. I actually use it for all remote images in my apps.. or at least read the source code.. image management is a pain.. lean on a mature library to get you going.

JustinMorris
  • 7,259
  • 3
  • 30
  • 36
  • Thank you very much! This looks really interesting and is definitely what I needed. – caw Sep 21 '12 at 12:50
0

The link that you provided is good for understanding what is convertView, asyncTask etc.I dont think doing View v = super.getView(position, convertView, parent); would work. If you want to recycle views you should do if(convertView != null){ myView = convertView} else{ inflate(myView)};

About AsyncTask that's right its different in different APIS but when you use execute() for old API and executeOnExecutor on the new one - I think everything is fine. You should pass URI and ImageView to your AsyncTask. Here you could have problem with convertView that it appears in a new row with AsyncTask working on it's image. You can hold for example hashmap of ImageView-AsyncTask and cancel these which are not valid any more.

PS.Sorry for creating a new comment but it was too long for inline answer :)

Mark
  • 5,466
  • 3
  • 23
  • 24
  • Thank you! Everything is clear now except that I don't know which threading mechanism to use. Every `AsyncTask` subclass can only be executed once, can't it? So if I do fast scrolling, the application will try to execute the task several times concurrently which causes an `Exception`. How can I use a `Handler` for this? – caw Sep 14 '12 at 11:40
-1
    private class CallService extends AsyncTask<String, Integer, String>
         {   
                protected String doInBackground(String... u)
                {
                    fetchReasons();
                    return null;
                }
                protected void onPreExecute() 
                {
                    //Define the loader here.

                }
                public void onProgressUpdate(Integer... args)
                {           
                }
                protected void onPostExecute(String result) 
                {               
                    //remove loader
                    //Add data to your view.
                }
         }

public void fetchReasons()
    {
         //Call Your Web Service and save the records in the arrayList
    }

Call new CallService().execute(); in onCreate() method

Amit Thaper
  • 2,117
  • 4
  • 26
  • 49
  • Thanks! This does not help, actually it isn't really a change at all. The problem is that storing hundreds of photos in the `ArrayList` consumes too much memory. So loading all at once is not what I want. – caw Sep 20 '12 at 14:16
  • Then you can do it by paging. – Amit Thaper Sep 21 '12 at 04:27
-3

A small piece of advice would be to disable loading of images when a fling occurs.


dirty HACK to make everything faster:

In your Adapter's getItemViewType(int position), return the position:

@Override
public long getItemViewType(int position) {
     return position;
}
@Override
public long getViewTypeCount(int position) {
     return getCount();
}
@Override
public View getView(int position, View convertView, ViewGroup arg2) {
    if (convertView == null) {
        //inflate your convertView and set everything
    }
    //do not do anything just return the convertView
    return convertView;
}

ListView will decide the amount of images to cache.


Sherif elKhatib
  • 45,786
  • 16
  • 89
  • 106
  • How can `getItemId(...)` make "everything faster"? Can you elaborate on this, please? – caw Sep 17 '12 at 13:48
  • Once you do this, you should return `convertView` in your `getView` function when `convertView!=null`. In other words, you do not need to reset your view – Sherif elKhatib Sep 17 '12 at 16:57
  • I dont think thats true. You can always reuse convertView when it's not null, but I dont think you should rely on the id returned by getItemId() to indicate what data is currently displayed. What if the view has been recycled? – DanielGrech Sep 18 '12 at 07:33
  • @DanielGrech if(convertView == null) {initialise the view with its data} else Preturn convertView without doing anything} – Sherif elKhatib Sep 18 '12 at 08:18
  • 1
    Yep but I dont see how setting 'getItemId()' to return the position will help. The listview will only ever create the number of views which you can fit on screen at once. So for instance if the screen can fit 5 items, but you have 6 items in your adapter, 1 view will be recycled and you will have to reset the data. In this case you cant just 'do nothing and return the convert view' as the data in the item will be incorrect – DanielGrech Sep 18 '12 at 11:31
  • @DanielGrech I have not encountered that behaviour. The listview will cache as much views as possible irrespective of the screen size. – Sherif elKhatib Sep 18 '12 at 13:39
  • What exactly is the link between `getItemId()` and the caching behaviour of the `ListView`? – caw Sep 18 '12 at 14:19
  • That you can return the recycled row as it is when `convertView` is not `null`, is simply wrong. – caw Sep 18 '12 at 14:22
  • @MarcoW. how can a recycled view be not null? please explain. And why don't you try the code (: – Sherif elKhatib Sep 18 '12 at 15:42
  • I don't need to test because I know ;) If `convertView` is `null`, you need to inflate the `View`, okay. But in any case, you need to set up the `View` with the correct date, afterwards. – caw Sep 18 '12 at 18:25
  • Marco is right, you can't simply return the convertView when it's not null. This convertView is currently holding data for a position that is no longer being displayed, and needs to be re-styled to display the data for this new position. If you are saying that this is to be used only during a fling when the user is unlikely to be able to read that data anyhow, that is valid. But you do not show any indication in the code that that is the case. – danh32 Sep 18 '12 at 21:09
  • it is not the ID :$ the viewType! returning it will force the listview to manage caching – Sherif elKhatib Sep 19 '12 at 07:01