[置顶] ListView 优化篇:从 BaseAdapter 到 BaseViewHolder

Goole 推荐 Android 程序员使用的 ConvertView + ViewHolder 的模式已经被众人所熟知,现已成为最基础的设计方式,能提升至少 70% 的性能。可以说,写 ListView 不用 ViewHolder 都不能算是合格的 Android 程序员。

但是,本文并不打算介绍传统的 ConvertView + ViewHolder 的模式,也不打算直接贴代码敷衍了事,而是站在代码优化的角度,从最基本的 BaseAdapter,教你如何一步步搭建一个通用的万能适配器,最终将引出 Android 中非常重要的”面向 Holder 的编程思想”。

本文全部示例代码,请移步 github

传统模式的缺点

尽管,传统的 ConvertView + ViewHolder 模式能提高性能,但是存在以下缺点:

高耦合

传统的模式将 ViewHolder 直接作为内部类写在 Activity 里,而每一个 Adapter 必须配备一个 ViewHolder,这种方式是难以维护、管理和扩展的。

高冗余

getView 方法,基本是做重复的操作,无非这么几步

  • 判断是否 convertView 为 null
  • 创建 holder 对象
  • findViewBtId 和 setTag getTag
  • 最后返回复用后的 view

阅读性差

传统的模式将控件的 findViewById、赋值操作等业务逻辑,写在一个 getView 方法内,一旦布局中的控件数量一多,getView 方法就变得臃肿无比,就像一个大胖子。例如曾经开发的一个电商项目的购物车逻辑代码量 2000 行左右,其中 getView 方法占了近 1200 行。

传统的代码如下

public class SecondActivity extends Activity{
    private ListView mList;
    private List<AppInfo> mDatas;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);

        // 初始化
        init();
    }

    /* 初始化 ListView */
    private void init() {
        mList = (ListView) findViewById(R.id.list);

        mDatas = new ArrayList<AppInfo>();

        mList.setAdapter(new MyAdapter());
    }

    private class MyAdapter extends BaseAdapter{

        @Override
        public int getCount() {
            return mDatas == null ? 0 : mDatas.size();
        }

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

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

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            ViewHolder holder = null;
            if (convertView == null){
                convertView = View.inflate(SecondActivity.this, R.layout.item_appinfo, null);
                holder = new ViewHolder();

                holder.item_icon = (ImageView) findViewById(R.id.item_icon);
                holder.item_content = (TextView) findViewById(R.id.item_content);
                holder.item_title = (TextView) findViewById(R.id.item_title);
                holder.item_rating = (RatingBar) findViewById(R.id.item_rating);
                holder.item_size = (TextView) findViewById(R.id.item_size);
                holder.item_bottom = (TextView) findViewById(R.id.item_bottom);

                convertView.setTag(holder);
            } else {
                holder = (ViewHolder) convertView.getTag();
            }

            AppInfo info = mDatas.get(position);

            holder.item_icon.setImageResource(R.drawable.ic_action_search);
            holder.item_content.setText(info.getDes());
            holder.item_title.setText(info.getName());
            holder.item_rating.setProgress((int)info.getStars());
            holder.item_size.setText( Formatter.formatFileSize(SecondActivity.this, info.getSize()) );
            holder.item_bottom.setText(info.getDes());

            return convertView;
        }

    }

    private class ViewHolder {
        ImageView item_icon;
        TextView item_content;
        TextView item_title;
        RatingBar item_rating;
        TextView item_size;
        TextView item_bottom;
    }

}

看了上边一坨代码,是否感觉非常臃肿。好吧,下面我们开始优化之旅

开始优化

模块化 View 的创建

第一步,就是把 View 创建相关的 inflate 、 findViewById,移到 ViewHolder 的构造方法,代码就变成了这样。

getView() 方法:

public View getView(int position, View convertView, ViewGroup parent) {
    ViewHolder holder = null;
    if (convertView == null){
        holder = new ViewHolder();
    } else {
        holder = (ViewHolder) convertView.getTag();
    }
    AppInfo info = mDatas.get(position);

    holder.item_icon.setImageResource(R.drawable.ic_action_search);
    holder.item_content.setText(info.getDes());
    holder.item_title.setText(info.getName());
    holder.item_rating.setProgress((int)info.getStars());
    holder.item_size.setText( Formatter.formatFileSize(SecondActivity.this, info.getSize()) );
    holder.item_bottom.setText(info.getDes());
    return convertView;
}

ViewHolder:

private class ViewHolder {
    private View contentView;

    ImageView item_icon;
    TextView item_content;
    TextView item_title;
    RatingBar item_rating;
    TextView item_size;
    TextView item_bottom;

    public ViewHolder(){
        // 初始化布局
        contentView = View.inflate(SecondActivity.this, R.layout.item_appinfo, null);

        // 初始化控件
        item_icon = (ImageView) contentView。findViewById(R.id.item_icon) ;
        item_content = (TextView) contentView。findViewById(R.id.item_content) ;
        item_title = (TextView) contentView。findViewById(R.id.item_title) ;
        item_rating = (RatingBar) contentView。findViewById(R.id.item_rating) ;
        item_size = (TextView) findViewById(R.id.item_size) ;
        item_bottom = (TextView) contentView。findViewById(R.id.item_bottom) ;

        // 寄存 ViewHolder 到布局中
        contentView.setTag(this);
    }
}

模块化实体的设置

这一步,主要是:

  1. 将控件的赋值放到 ViewHolder 内单独的 setData 方法中,别忘了在 getView 方法调用 setData

  2. 为 ViewHolder 创建 getContentView,对外提供根布局

  3. getView 方法返回的是 holder 中保存的布局

getView():

public View getView(int position, View convertView, ViewGroup parent) {
    ViewHolder holder = null;
    if (convertView == null) {
        holder = new ViewHolder();
    } else {
        holder = (ViewHolder) convertView.getTag();
    }

    AppInfo info = mDatas.get(position);
    holder.setData(info);

    // 返回 ViewHolder 保存的布局
    return holder.getContentView();
}

ViewHolder:

private class ViewHolder {
    private View contentView;
    ImageView item_icon;
    TextView item_content;
    TextView item_title;
    RatingBar item_rating;
    TextView item_size;
    TextView item_bottom;

    public ViewHolder() {
        contentView = initView();
        contentView.setTag(this);
    }

    /** 初始化布局 */
    public View initView() {
        View view = View.inflate(SecondActivity.this,R.layout.item_appinfo, null);

        item_icon = (ImageView) contentView.findViewById(R.id.item_icon);
        item_content = (TextView) contentView.findViewById(R.id.item_content);
        item_title = (TextView) contentView.findViewById(R.id.item_title);
        item_rating = (RatingBar) contentView.findViewById(R.id.item_rating);
        item_size = (TextView) contentView.findViewById(R.id.item_size);
        item_bottom = (TextView) contentView.findViewById(R.id.item_bottom);

        return view;
    }

    public void setData(AppInfo data){
        item_icon.setImageResource(R.drawable.ic_action_search);
        item_content.setText(data.getDes());
        item_title.setText(data.getName());
        item_rating.setProgress((int) data.getStars());
        item_size.setText(Formatter.formatFileSize(SecondActivity.this, data.getSize()));
        item_bottom.setText(data.getDes());
    }

    public View getContentView() {
        return contentView;
    }
}

抽取 ViewHolder 共性

现在,我们有了 ViewHolder,并且 ViewHolder 的构造方法、initView 还有 setData 方法是固定的。那么就可以将这些共性抽取到 BaseViewHolder

BaseViewHolder 类的主要作用是:

  1. 用泛型,抽象具体的实体类
  2. 供 initView、setData 给子类实现

BaseViewHolder:

public abstract class BaseViewHolder<T>{
    private View contentView;

    public BaseViewHolder(){
        contentView = initView();
        contentView.setTag(this);
    }

    public abstract View initView();

    public abstract void setData(T data);

    public View getContentView() {
        return contentView;
    }
}

ViewHolder:

private class ViewHolder extends BaseViewHolder<AppInfo>{

    ImageView item_icon;
    TextView item_content;
    TextView item_title;
    RatingBar item_rating;
    TextView item_size;
    TextView item_bottom;

    /** 初始化布局 */
    @Override
    public View initView() {
        View view = View.inflate(SecondActivity.this,
                R.layout.item_appinfo, null);

        item_icon = (ImageView) contentView.findViewById(R.id.item_icon);
        item_content = (TextView) contentView.findViewById(R.id.item_content);
        item_title = (TextView) contentView.findViewById(R.id.item_title);
        item_rating = (RatingBar) contentView.findViewById(R.id.item_rating);
        item_size = (TextView) contentView.findViewById(R.id.item_size);
        item_bottom = (TextView) contentView.findViewById(R.id.item_bottom);

        return view;
    }

    @Override
    public void setData(AppInfo data) {
        item_icon.setImageResource(R.drawable.ic_action_search);
        item_content.setText(data.getDes());
        item_title.setText(data.getName());
        item_rating.setProgress((int) data.getStars());
        item_size.setText(Formatter.formatFileSize(SecondActivity.this,data.getSize()));
        item_bottom.setText(data.getDes());
    }

}

优化 findViewById

对于不同的业务逻辑,不同的布局控件,都要创建对应 View 对象,十分麻烦。所以,我们采用的 Android SDK 提供的 SparseArray,优化赋值操作。

这一步:
1. 定义了 SparseArray 对象,存储 View 对象
2. 创建 setImage、setText,以便为控件赋值

BaseViewHolder:

abstract class BaseViewHolder<T>{
    private View contentView;
    private SparseArray<View> mViews;

    public BaseViewHolder(){
        contentView = initView();
        contentView.setTag(this);
        mViews = new SparseArray<View>();
    }

    public abstract View initView();

    public abstract void setData(T data);

    public View getContentView() {
        return contentView;
    }

    /** 设置文本 */
    public void setText(int resId, String content){
        TextView textView = (TextView) mViews.get(resId);
        if (textView == null){
            textView = (TextView) contentView.findViewById(resId);
            mViews.put(resId, textView);
        }

        textView.setText(content);
    }

    /** 设置图片 */
    public void setImage(int id, int resId){
        ImageView imageView = (ImageView) mViews.get(id);
        if (imageView == null){
            imageView = (ImageView) contentView.findViewById(id);
            mViews.put(id, imageView);
        }

        imageView.setImageResource(resId);
    }

    /** 获取 view 对象 */
    public View getView(int resId){
        View view = mViews.get(resId);
        if (view == null){
            view = contentView.findViewById(resId);
            mViews.put(resId, view);
        }

        return view; 
    }
}

ViewHolder:

private class ViewHolder extends BaseViewHolder<AppInfo>{
    /** 初始化布局 */
    @Override
    public View initView() {
        View view = View.inflate(SecondActivity.this,R.layout.item_appinfo, null);
        return view;
    }

    @Override
    public void setData(AppInfo data) {
        setImage(R.id.item_icon,R.drawable.ic_action_search);
        setText(R.id.item_title, data.getName());
        setText(R.id.item_size,Formatter.formatFileSize(SecondActivity.this,data.getSize()));
        setText(R.id.item_bottom, data.getDes());
    }
}

抽取适配器共性到 DefaultAdapter

经过上面几步的操作,我们的小框架基本成型。我们发现 MyAdapter 的 getItem、getItemId、getCount、getView 几个方法具有共性,这一步,主要为这些方法抽取父类。

需要注意:
1. DefaultAdapter 应当添加泛型 T,以代表各种实体类
2. 数据集合通过构造方法传给 DefaultAdapter
3. getView 中的 holder 交给父类 BaseViewHolder 接收(多态)

DefaultAdapter:

private class DefaultAdapter<T> extends BaseAdapter{
    private List<T> mDatas;

    public DefaultAdapter(List<T> mDatas) {
        this.mDatas = mDatas;
    }

    @Override
    public int getCount() {
        return mDatas == null ? 0 : mDatas.size();
    }

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

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

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        BaseViewHolder holder = null;
        if (convertView == null) {
            holder = new ViewHolder();
        } else {
            holder = (ViewHolder) convertView.getTag();
        }

        T info = mDatas.get(position);
        holder.setData(info);

        return holder.getContentView();
    }
}

MyAdapter:

private class MyAdapter extends DefaultAdapter<AppInfo> {
    public MyAdapter(List<AppInfo> mDatas) {
        super(mDatas);
    }
}

暴露特性,封装完成

经过 抽取适配器共性到 DefaultAdapter ,其实这个框架基本已经完成了。但还是每次要创建 子类 ViewHolder 去实现 initView 创建各自的布局。能不能更优?答案是能。

这一步,主要做了:
1. 将布局的资源 id 通过 DefaultAdapter 的构造方法传入,并传递给 BaseViewHolder

DefaultAdapter:

abstract class DefaultAdapter<T> extends BaseAdapter{
    private List<T> mDatas;
    private int mLayoutId;

    public DefaultAdapter(List<T> mDatas, int layoutId) {
            this.mDatas = mDatas;
            this.mLayoutId = layoutId;
    }

    // getItem、getItemId、getCount略

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        BaseViewHolder holder = null;
        if (convertView == null) {
            holder = new BaseViewHolder<T>(mLayoutId) {

                @Override
                public void setData(T data) {
                    setHolder(this, data);
                }
            };
        } else {
            holder = (BaseViewHolder) convertView.getTag();
        }

        T info = mDatas.get(position);
        holder.setData(info);
        return holder.getContentView();
    }

    protected abstract void setHolder(BaseViewHolder holder, T data);
}

最终效果

终于到了成果展示的时候,看到这下面的代码,你会发现,我们的辛苦是值的。

    private void init() {
        mList = (ListView) findViewById(R.id.list);
        mDataSet = new ArrayList<AppInfo>();
        mList.setAdapter(new MyAdapter(mDataSet, R.layout.item_appinfo));
    }

    private class MyAdapter extends DefaultAdapter<AppInfo> {
        public MyAdapter(List<AppInfo> mDatas, int layoutId) {
            super(mDatas, layoutId);
        }

        @Override
        protected void setHolder(BaseViewHolder holder, AppInfo data) {
            holder.setImage(R.id.item_icon, R.drawable.ic_action_search);
            holder.setText(R.id.item_title, data.getName());
            holder.setText(R.id.item_size,Formatter.formatFileSize(SecondActivity.this,data.getSize()));
            holder.setText(R.id.item_bottom, data.getDes());
        }
    }

代码瞬间从上百行,减少到十几行,而且 DefaultAdapter 和 BaseViewHolder 可以直接拷到项目中使用。

缺点

尽管这个框架功能强大,能大大减少代码量,但就像毕向东所说,凡事都有两面性,代码量是少了,扩展性也差了。遇到特别复杂的需求,这两个类就显得力不从心。以后,我会抽时间再写篇文章,讲述怎么修改这个适配器框架以适应不同需求,力求在”方便”和”灵活”之间找到一个平衡点,将代码优化到极致!

下一篇:ListView 优化篇:从 BaseViewHolder 到面向 Holder 的思想

附录

示例代码:https://github.com/heshiweij/BaseViewHolder

你可能感兴趣的:(优化,android,ListView,面向Holder编程)