Large ListView containing images in Android

2019-01-23 09:49发布

问题:

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?

回答1:

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.



回答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.



回答3:

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.



回答4:

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.



回答5:

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 :)



回答6:

    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



回答7:

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.