Android - 带有 ImageView、LRUcache 和 ViewHolder 的滞后 ListView

Android - Laggy ListView with ImageView, LRUcache and ViewHolder

我有一个 ListView,其中每一行都有一些文本和两个 ImageView:一个对每一行都相同,另一个取决于当前项目。

这是我的适配器:

mArrayAdapter(Context context, int layoutResourceId, ArrayList<Exhibition>  data) {
    super(context, layoutResourceId, data);
    this.context = context;
    this.layoutResourceId = layoutResourceId;
    this.list = data;
    this.originalList = data;
    viewHolder = new ViewHolder();
    final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
    final int cacheSize = maxMemory / 8;

    mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
        @Override
        protected int sizeOf(String key, Bitmap bitmap) {
            return bitmap.getByteCount() / 1024;
        }
    };

}

@Override
@NonNull
public View getView(final int position, View convertView, @NonNull final ViewGroup parent) {
    View row;
    final Exhibition ex;
    if(convertView==null){
        row = LayoutInflater.from(getContext()).inflate(R.layout.row,parent,false);

        viewHolder.expand = (ImageView)row.findViewById(R.id.expand);
        row.setTag(viewHolder);
    }
    else {
        row = convertView;
        viewHolder = (ViewHolder)row.getTag();
    }
    ex = list.get(position);


    descr = (TextView)row.findViewById(R.id.descr);
    ttl = (TextView)row.findViewById(R.id.title);
    city = (TextView)row.findViewById(R.id.city);
    dates = (TextView)row.findViewById(R.id.dates);
    museum = (TextView)row.findViewById(R.id.location);
    header = (ImageView)row.findViewById(R.id.hd);

    ttl.setText(ex.name);
    descr.setText(ex.longdescr);
    museum.setText(ex.museum);
    city.setText(ex.city);

    final Bitmap bitmap = getBitmapFromMemCache(ex.key);
    if (bitmap != null) {
        header.setImageBitmap(bitmap);
    } else {
        header.setImageBitmap(ex.getHeader());
        addBitmapToMemoryCache(ex.key,ex.header);
    }


    SimpleDateFormat myFormat = new SimpleDateFormat("dd/MM/yyyy", Locale.ITALY);
    Date start = new Date(ex.getStart()), end = new Date(ex.getEnd());

    String startEx = myFormat.format(start);
    String endEx = myFormat.format(end);

    String finalDate = getContext().getResources().getString(R.string.ex_date, startEx, endEx);

    dates.setText(finalDate);

    viewHolder.expand.setId(position);

    if(position == selectedId){
        descr.setVisibility(View.VISIBLE);
        ttl.setMaxLines(Integer.MAX_VALUE);
        dates.setMaxLines(Integer.MAX_VALUE);
        museum.setMaxLines(Integer.MAX_VALUE);
        city.setMaxLines(Integer.MAX_VALUE);
    }else{
        descr.setVisibility(View.GONE);
        ttl.setMaxLines(1);
        dates.setMaxLines(1);
        museum.setMaxLines(1);
        city.setMaxLines(1);
    }

    viewHolder.expand.setOnClickListener(this.onCustomClickListener);

    return row;
}

public void setDescr(int p){
    selectedId = p;
}

public void setOnCustomClickListener(final View.OnClickListener onClickListener) {
    this.onCustomClickListener = onClickListener;
}

public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
    if (getBitmapFromMemCache(key) == null) {
        mMemoryCache.put(key, bitmap);
    }
}

public Bitmap getBitmapFromMemCache(String key) {
    return mMemoryCache.get(key);
}


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

@Override
public boolean isEnabled(int position)
{
    return true;
}

@Override
public Exhibition getItem (int pos){
    return list.get(pos);
}

void resetData() {

    list = originalList;
}

private class ViewHolder {

    ImageView expand,header;

}

@Override
@NonNull
public Filter getFilter() {
    if (valueFilter == null) {
        Log.d("SEARCH1","New filter");
        valueFilter = new ValueFilter();
    }
    return valueFilter;
}

private class ValueFilter extends Filter {
    @Override
    protected FilterResults performFiltering(CharSequence constraint) {

        FilterResults results = new FilterResults();
        if(constraint == null || constraint.length() == 0){
            results.values = originalList;
            results.count = originalList.size();
        }
        else {

            List<Exhibition> nExhList = new ArrayList<>();

            for(Exhibition e : list){
                Log.d("NAMEE",e.name + " " + constraint.toString());
                if (e.getName().toUpperCase().contains(constraint.toString().toUpperCase()) || e.getCity().toUpperCase().contains(constraint.toString().toUpperCase())
                        ||e.getMuseum().toUpperCase().contains(constraint.toString().toUpperCase()) || e.getLongDescription().toUpperCase().contains(constraint.toString().toUpperCase())
                        || e.getDescription().toUpperCase().contains(constraint.toString().toUpperCase()) || e.getCategory().toUpperCase().contains(constraint.toString().toUpperCase())){
                    nExhList.add(e);
                }
            }
            results.values= nExhList;
            results.count=nExhList.size();
        }
        return results;
    }

    @Override
    protected void publishResults(CharSequence constraint,
                                  FilterResults results) {
        if(results.count==0){
            notifyDataSetInvalidated();
        }
        else{
            list = (ArrayList<Exhibition>)results.values;
            notifyDataSetChanged();
        }
    }
}

第一个 ImageViewBitmap 存储在 Exhibition 变量中。 第二个更改文本的可见性以获得类似可扩展的效果(因为现在我无法将 ListView 转换为 ExpandableListView)。 我尝试了不同的东西,比如缓存,AsyncTask,删除自定义点击监听器,把所有东西都放在 ViewHolder 但滚动充满了微滞后。适配器有什么问题我没看懂吗?

为了使您的列表顺畅,您可以尝试以下选项,

  1. 除了使用您自己的位图缓存方式,您可以尝试使用流行的库,如 Glide, Picasso 或其他一些开源库
  2. 尽量避免在getView中进行耗时的操作,ex-日期转换可以在构建模型对象时移动到对象级别,每个对象一次。
  3. 您可以试用 Recyclerview 而不是 ListView

您可以采取一些措施来提高性能。

找出到底是什么慢

了解分析可以告诉您哪些函数是 被调用次数最多 and/or 且完成时间最长的。通过这种方式,您可以决定将时间花在修复或更改代码上。

参见 https://developer.android.com/studio/profile/android-profilerhttps://developer.android.com/studio/profile/

ViewHolder 模式

您在滥用 ViewHolder pattern。在您的代码中,适配器的 viewHolder 字段中只有一个 ViewHolder 实例。然后,您可以在 getView() 函数中像使用常规局部变量一样使用此字段。

然后您多次调用 row.findViewById(),即使 convertView 不是 nullfindViewById() 调用很慢,视图持有者的优点是扩展后每个视图只需调用一次(在 if 的 convertView==null 分支中)。

相反,每个行视图应该有 1 个视图持有者。请注意,您并不是在创建一个新的 ViewHolder 来分配 setTag(),而是重复使用同一个。那么 descrttlcity 等变量应该是 ViewHolder 的字段,因此可以快速引用。

创建不必要的对象

内存分配也很慢。

每次 getView() 被调用时,您也在创建对象,您可以创建一次,然后重复使用。

一个这样的例子是 SimpleDateFormat,它可以在适配器构造函数中创建一次并简单地用于生成文本。

研究如何避免创建如此多的 String 对象。使用字符串缓冲区或类似的东西进行格式化。您没有显示 Exhibition class 的源代码,因此不清楚为什么需要使用调用 getStart() 的结果创建一个 Date 对象和getEnd().

如果 Exhibition 对象的 'start' 和 'end' 字段从未用作 longs,请考虑将它们变成不可变的 Dates JSON 解析而不是每次都使用它们。

UI 线程中的潜在调用缓慢

Exhibition class 的源代码没有显示,所以我们无法知道 Exhitition.getHeader() 函数的作用。如果有位图下载 and/or 解码,将其移动到后台线程(并在位图准备好后更新)将提高 ListViews 滚动性能。

不必要的调用

即使不需要,也有正在执行的呼叫。例如在 getView() 末尾分配 On Click 侦听器。当您执行 inflation(当 convertViewnull 时),您可以只设置一次,因为所有行都使用相同的侦听器。

避免填满内存

您提到每个 Exhibition 对象都有一个 Bitmap 字段,该字段在解析 JSON 时设置。这意味着所有位图一直都在内存中。这意味着在这种情况下 LRU 缓存不是必需的,因为总是有对位图的强引用。

这也意味着随着列表中项目数量的增加,所需的内存也会增加。随着更多内存被使用,垃圾收集 (GC) 需要更频繁地发生,而 GC 很慢并且可能导致卡顿或冻结。分析可以告诉您遇到的冻结是否是由于 GC 造成的。

如果内存中一次只有几个位图,列表中当前可见的项目所需的位图,以及更多位图,位图缓存将很有用。如果在缓存中找不到所需的位图,则应从磁盘加载或从网络下载。

P.S.

请记住,您有一个 public setOnCustomClickListener() 函数,它只将引用分配给字段。如果您使用新的侦听器调用它,您当前的代码将在所有未使用新引用刷新和更新的行上使用旧的侦听器。