ListView performance is slow

2019-07-16 10:45发布

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?

2条回答
Evening l夕情丶
2楼-- · 2019-07-16 11:01

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.

查看更多
地球回转人心会变
3楼-- · 2019-07-16 11:04

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.

查看更多
登录 后发表回答