3

I have a custom ListView where I display some weapons which are retrieved from a local database. I have a total of 88 rows, for each row a text and an image is set each time getView() is called. The ListView lags while scrolling fast and the garbage collector is going insane, deleting some 1M objects per second. I'm not getting why.

Before I post my Adapter implementation, some explanation about how the Image is set. My Weapon class is simply a data holder with setters and getters. This is how names and images are getting set when the database gets created (yeah it might seem very strange, but all other solutions work even more slowly):

    private Weapon buildWeapon(Cursor cursor) {
    Weapon w = new Weapon();
    w.setId(cursor.getLong(0));
    w.setName(cursor.getString(1));
    w.setImage(Constants.ALL_WEAPON_IMAGES[(int) cursor.getLong(0)-1]);


    return w;
}

So I have an Array containing all weapon images in the form of R.drawable.somegun. The data structure is implemented such way that ID-1 always points to the right drawable reference in my Array. The image field in the Weapon class is an Integer. Now you have an idea how my getImage() method works and below goes my Adapter:

 public class Weapon_Adapter extends BaseAdapter {
private List<Weapon> items;
private LayoutInflater inflater = null;
private WeaponHolder weaponHolder;
private Weapon wp;


static class WeaponHolder {
    public TextView text;
    public ImageView image;
}

// Context and all weapons of specified class are passed here

public Weapon_Adapter(List<Weapon> items, Context c) {
    this.items = (List<Weapon>) items;
    inflater = LayoutInflater.from(c);
    Log.d("Adapter:", "Adapter created");
}

@Override
public int getCount() {
    return items.size();
}

@Override
public Weapon getItem(int position) {
    return items.get(position);
}


@Override
public long getItemId(int position) {
    return position;
}

@Override
public View getView(int position, View convertView, ViewGroup parent) {

    wp = (Weapon) getItem(position);

    if (convertView == null) {
        convertView = inflater.inflate(R.layout.category_row, null);
        weaponHolder = new WeaponHolder();
        weaponHolder.text = (TextView) convertView
                .findViewById(R.id.tvCatText);
        weaponHolder.image = (ImageView) convertView
                .findViewById(R.id.imgCatImage);
        convertView.setTag(weaponHolder);
    }

      weaponHolder = (WeaponHolder) convertView.getTag();   


    weaponHolder.text.setText(wp.getName());
    weaponHolder.image.setImageResource(wp.getImage());
           // weaponHolder.image.setImageResource(R.drawable.ak74m);




    return convertView;

}}

Now the strange thing: using the outcommented line to statically set the same image for all items removes all lags and GC in not even called once! I'm not getting it.. wp.getImage() returns exactly the same thing, only R.drawable.name is different for each weapon. But GC removes tons of objects and the ListView lags while scrolling. Any ideas what I'm doing wrong?

UPDATE

I've moved setting images to an AsyncTask and the lag is gone now:

    public class AsyncImageSetter extends AsyncTask<Void, Void, Void> {

private ImageView img;
private int image_resId;
private Bitmap bmp;
private Context c;

public AsyncImageSetter(Context c, ImageView img, int image_ResId, Bitmap bmp) {

    this.img = img;
    this.image_resId = image_ResId;
    this.bmp = bmp;
    this.c = c;

}

@Override
protected Void doInBackground(Void... params) {

    bmp = BitmapFactory.decodeResource(c.getResources(), image_resId);

    return null;
}

@Override
protected void onPostExecute(Void result) {

    img.setImageBitmap(bmp);
    bmp = null;

    super.onPostExecute(result);
}

   }

However, GC is still called like crazy and RAM consumption increases when scrolling the whole List up and down. The question is now: how can I optimize image recycling to avoid RAM usage increase?

Droidman
  • 11,485
  • 17
  • 93
  • 141
  • use lazy loading technique to load images. check this link http://stackoverflow.com/questions/16789676/caching-images-and-displaying/16978285#16978285 might help you also if the image is large try to scale down the image – Raghunandan Aug 25 '13 at 01:55
  • I've read about that and it's suitable for loading bunch of data from somewhere outside the app. I have very limited number of images and they all come from the drawable folder. I'm trying to figure out why the outcommented line works fast while my getImage() method which does pretty much the same causes the List to lag.. The images are not really large and get scaled by the ImageView's ScaleType parameter – Droidman Aug 25 '13 at 01:59
  • How big is each image? – m0skit0 Aug 25 '13 at 17:29

2 Answers2

3

since it's usually impractical to load all bitmaps into memory for Android, you should assume that you will get GC from time to time.

however, what you can do it consider the next tips:

  1. downscale the bitmaps to the sizes you need to show them. you can use google's way or my way.

  2. check in which folder you've put the image files. many people put them in the res/drawable folder and don't understand why they get to be so much larger than their original sizes (it's because of the density - it's mdpi while the device is probably xhdpi or xxhdpi).

    for example, if an image is in the drawable folder and you run it on an xhdpi device (like the galaxy S3), it would take (WIDTH*2)*(HEIGHT*2)*4 bytes . if the image is 200x200 , its bitmap object would take at least 400*400*4=640,000 bytes . it would be even worse on an xxhdpi device , like the galaxy s4 and htc one .

  3. consider using a memory cache, like the LruCache

  4. if the bitmaps do not have transparency, and you don't see any difference in quality, consider using the RGB_565 config instead of default one. this will take 2 bytes per pixel instead of 4 bytes per pixel.

  5. if you can be responsible enough, you can use JNI for the caching. i've made a small code for this task, here . please read all of the notes i've written there.

btw, i've noticed you used an array of identifiers for the images. if the images have some logic in their names (example : img1,img2,...), you could use getResources().getIdentifier(...) instead.

Community
  • 1
  • 1
android developer
  • 114,585
  • 152
  • 739
  • 1,270
  • I guess the option of caching the images would do for me, do you know any good examples/tutorials/samples for caching images and using them in a ListView? I feel a bit lost since I never worked closely with caching data.. – Droidman Aug 25 '13 at 19:25
  • sure none of the other points have helped you? anyway, here's a link about caching bitmaps: http://developer.android.com/training/displaying-bitmaps/cache-bitmap.html . note that LruCache needs API 12 and above, but it is available as a compatibility library somewhere: http://developer.android.com/reference/android/support/v4/util/LruCache.html . – android developer Aug 25 '13 at 19:30
1

If you fixed the low fps issues and fast scrolling performs smooth by now you're actually good to go.

When the frequent GC action is just a cosmetic issue for you and you're not facing OutOfMemoryException or any other downsides, then you should probably leave it like that. If that is no option for you there is another thing you can do: Besides down sampling and caching you could also add a small, artificial waiting time (50-150ms) after launching the AsyncTask and before actually retrieving the resource file. Then you add a cancel flag to your task that has to be checked after the artificial delay. If it's set to true you don't request the resource file.

Some (not executable) code examples:

class MyImageLoader extends AsyncTask {
    private boolean cancel = false

    private Bitmap bitmap;

    public void cancel() { cancel = true }

    public void doInBackground() {
        sleep(100);
        if(!cancel) {
            bitmap = BitmapFactory.decodeResource(...);
        }
    }
}

class Adapter {

    static class WeaponHolder {
        public TextView text;
        public ImageView image;
        public MyImageLoader loader;
    }

    public View getView(int position, View convertView, ViewGroup parent) {
        WeaponHolder holder;

        if (convertView == null) {
            ...
            holder = new WeaponHolder();
        } else {
            holder = convertView.getTag();
            holder.loader.cancel(); // Cancel currently active loading process
        }

        holder.loader = new MyImageLoader();
        holder.loader.execute();

        return convertView;
    }
}

This way most of the images won't be read from your internal memory if the user is scrolling real fast and you'll save plenty of memory.

Taig
  • 6,718
  • 4
  • 44
  • 65
  • I have an app that relies heavily on an endless scrolling list of large images. When I added the delay method explained above things went much more smooth and the low end devices had less memory issues. If the delay is low enough the user won't feel the additional wait. You should give it a try. – Taig Aug 25 '13 at 22:12
  • yeah thanks, I changed the code to match your example (removed the delay however), it runs very very smoothly on my SGS2 I9100. My minimum SDK is 14, so I (theoretically) won't face really low-performance devices. The thing is that it's not a simple ListView in my case. I have 6 ListFragments inside a ViewPager and each gets filled with data as shown in my code above. So I care about each MB of RAM. In normal layout folder my ImageView is 150x50dp, original bitmaps are larger however. Will it make sense to scale them down in my AsyncTask? – Droidman Aug 25 '13 at 22:28
  • You should stick to [this article](http://developer.android.com/training/displaying-bitmaps/load-bitmap.html). It explains how to downsample a Bitmap depending on the user's screen size! – Taig Aug 25 '13 at 22:40