fred-ye / summary

my blog
43 stars 9 forks source link

[Android] ListView和Adapter使用的最佳实践 #31

Open fred-ye opened 9 years ago

fred-ye commented 9 years ago

一个ListView使用的好坏全都依赖于Adapter的实现,Adapter为ListView提供数据源,并渲染View。以下我们来分析一下,一个好的Adapter应该如何实现。首先,看这个例子:

public class MyAdapter extends BaseAdapter{
    private List<MyListItem> list;
    private Context context;
    public MyAdapter(Context context) {
        this.context = context;
    }
    @Override
    public int getCount() {
        return list.size();
    }

    @Override
    public Object getItem(int position) {
        return list.get(position);
    }

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

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        convertView = LayoutInflater.from(context).inflate(R.layout.list_item, parent, false);
        ImageView ivTest = (ImageView) convertView.findViewById(R.id.iv_test);
        TextView tvDesc = (TextView) convertView.findViewById(R.id.tv_desc);
        //此处是采用UniversalImageLoader来加载图片。
        ImageLoader.getInstance().displayImage(list.get(position).getImageUrl(), ivTest);
        tvDesc.setText(list.get(position).getDesc());
        Log.i("Adapter", "convertView : " + convertView);
        return convertView;
    }

    public void setList(List<MyListItem> list) {
        this.list = list;
    }

}

代码量很少。我们来一下其getView方法,每一次在渲染一个Item的时候,便会加载一次list_item这个布局文件,由此可见,效率是很低的。 于是针对getView作如下改进:

   @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        if (null == convertView) {
            convertView = LayoutInflater.from(context).inflate(R.layout.list_item, parent, false);
        }
        ImageView ivTest = (ImageView) convertView.findViewById(R.id.iv_test);
        TextView tvDesc = (TextView) convertView.findViewById(R.id.tv_desc);

        ImageLoader.getInstance().displayImage(list.get(position).getImageUrl(), ivTest);
        tvDesc.setText(list.get(position).getDesc());
        Log.i("Adapter", "convertView : " + convertView);
        return convertView;
    }

显然,这样一改进了之后,只会在convertView不存在的情况下,才会去渲染一个新的,如果之前存在过,就采用之前的。这样下来。比如一个ListView中有100条记录,但屏幕上同时只能显示10个,系统则只需要构造10个convertView就可以了。通过看打印出来的Log,便可以证实这一点。

第一个可以优化的地方已经被我们优化了。接着还是看getView这个方法,还有一个可以优化的地方便是findViewById()这个方法的使用。在Android源码中,findViewById()这个方法的实现如下:

    public View findViewById(int id) {
        if (this.id == id) {
            return this;
        }
        for(View child : children) {
            View view = child.findViewById(id);
            if (view != null) {
                return view;
            }
        }
        return null;
    }

由此可以,对某一个View调用其findViewById()方法,便会遍历这个View中的所有子元素进行查找,这也是一种非常耗时的操作。接着,我们便对这个问题进行优化。 接着便出现了ViewHolder这个思路了。其思路在于,当我们已经通过findViewById()这个方法获得View之后,便将这个View存到一个ViewHolder的对象中。下次使用时,直接从ViewHolder对象中取。优化后的代码如下:

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ViewHolder holder;
        if (null == convertView) {
            holder = new ViewHolder();
            convertView = LayoutInflater.from(context).inflate(R.layout.list_item, parent, false);
            holder.ivTest = (ImageView) convertView.findViewById(R.id.iv_test);
            holder.tvDesc = (TextView) convertView.findViewById(R.id.tv_desc);
            convertView.setTag(holder);
        } else {
            holder = (ViewHolder) convertView.getTag();
        }

        ImageLoader.getInstance().displayImage(list.get(position).getImageUrl(), holder.ivTest);
        holder.tvDesc.setText(list.get(position).getDesc());
        Log.i("Adapter", "convertView : " + convertView);
        return convertView;
    }

    private static class ViewHolder {
        public ImageView ivTest;
        public TextView tvDesc;
    }

其它的关于Adapter优化的思路。在文章一文章二中提到了另外一种方式:

public class ViewHolder {
    // I added a generic return type to reduce the casting noise in client code
    @SuppressWarnings("unchecked")
    public static <T extends View> T get(View view, int id) {
        SparseArray<View> viewHolder = (SparseArray<View>) view.getTag();
        if (viewHolder == null) {
            viewHolder = new SparseArray<View>();
            view.setTag(viewHolder);
        }
        View childView = viewHolder.get(id);
        if (childView == null) {
            childView = view.findViewById(id);
            viewHolder.put(id, childView);
        }
        return (T) childView;
    }
}

个人觉得,在Adapter中采用这种方式,只能说是优化代码的结构,并不能优化效率。同时,也会增加代码的复杂度。 最后这个Adapter的代码如下:

public class MyAdapter extends BaseAdapter {
    private List<MyListItem> list;
    private Context context;

    public MyAdapter(Context context) {
        this.context = context;
    }

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

    @Override
    public Object getItem(int position) {
        return list.get(position);
    }

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

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ViewHolder holder;
        if (null == convertView) {
            holder = new ViewHolder();
            convertView = LayoutInflater.from(context).inflate(R.layout.list_item, parent, false);
            holder.ivTest = (ImageView) convertView.findViewById(R.id.iv_test);
            holder.tvDesc = (TextView) convertView.findViewById(R.id.tv_desc);
            convertView.setTag(holder);
        } else {
            holder = (ViewHolder) convertView.getTag();
        }

        ImageLoader.getInstance().displayImage(list.get(position).getImageUrl(), holder.ivTest);
        holder.tvDesc.setText(list.get(position).getDesc());
        Log.i("Adapter", "convertView : " + convertView);
        return convertView;
    }

    private static class ViewHolder {
        public ImageView ivTest;
        public TextView tvDesc;
    }

    public void setList(List<MyListItem> list) {
        this.list = list;
    }

}