[置顶] ListView 优化篇:从 BaseAdapter 到 BaseViewHolder
2016-03-16 10:53
567 查看
Goole 推荐 Android 程序员使用的 ConvertView + ViewHolder 的模式已经被众人所熟知,现已成为最基础的设计方式,能提升至少 70% 的性能。可以说,写 ListView 不用 ViewHolder 都不能算是合格的 Android 程序员。
但是,本文并不打算介绍传统的 ConvertView + ViewHolder 的模式,也不打算直接贴代码敷衍了事,而是站在代码优化的角度,从最基本的 BaseAdapter,教你如何一步步搭建一个通用的万能适配器,最终将引出 Android 中非常重要的”面向 Holder 的编程思想”。
本文全部示例代码,请移步 github
判断是否 convertView 为 null
创建 holder 对象
findViewBtId 和 setTag getTag
最后返回复用后的 view
传统的代码如下
看了上边一坨代码,是否感觉非常臃肿。好吧,下面我们开始优化之旅
getView() 方法:
ViewHolder:
将控件的赋值放到 ViewHolder 内单独的 setData 方法中,别忘了在 getView 方法调用 setData
为 ViewHolder 创建 getContentView,对外提供根布局
getView 方法返回的是 holder 中保存的布局
getView():
ViewHolder:
BaseViewHolder 类的主要作用是:
用泛型,抽象具体的实体类
供 initView、setData 给子类实现
BaseViewHolder:
ViewHolder:
这一步:
1. 定义了 SparseArray 对象,存储 View 对象
2. 创建 setImage、setText,以便为控件赋值
BaseViewHolder:
ViewHolder:
需要注意:
1. DefaultAdapter 应当添加泛型 T,以代表各种实体类
2. 数据集合通过构造方法传给 DefaultAdapter
3. getView 中的 holder 交给父类 BaseViewHolder 接收(多态)
DefaultAdapter:
MyAdapter:
这一步,主要做了:
1. 将布局的资源 id 通过 DefaultAdapter 的构造方法传入,并传递给 BaseViewHolder
DefaultAdapter:
代码瞬间从上百行,减少到十几行,而且 DefaultAdapter 和 BaseViewHolder 可以直接拷到项目中使用。
下一篇:ListView 优化篇:从 BaseViewHolder 到面向 Holder 的思想
但是,本文并不打算介绍传统的 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); } }
模块化实体的设置
这一步,主要是:将控件的赋值放到 ViewHolder 内单独的 setData 方法中,别忘了在 getView 方法调用 setData
为 ViewHolder 创建 getContentView,对外提供根布局
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 方法是固定的。那么就可以将这些共性抽取到 BaseViewHolderBaseViewHolder 类的主要作用是:
用泛型,抽象具体的实体类
供 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相关文章推荐
- snmptrap的使用方法
- SAP HR系统如何处理员工月中调动问题
- Android.mk中call all-subdir-makefiles和call all-makefiles-under,$(LOCAL_PATH)的区别
- 最近突然发现Myeclipse中New菜单项下常用选项都不见了 怎么调整?
- cocos2dx之HTTP开发
- 杭电ACM2043密码的问题已经AC
- Android——打招呼
- Eclipse 插件大全 (持续更新)
- TMP
- Jquery轮播图片
- (转)使用FFMPEG类库分离出多媒体文件中的H.264码流
- 微信授权步骤与详解 -- c#篇
- SAP-HR三大结构
- 直方图中bins应如何理解及处理
- C++设计模式编程之Flyweight享元模式结构详解
- ASP.NET 控件中AutoPostBack属性
- STM8S AWU与WWD配合
- javamail 实例
- 杭电ACM2041楼梯问题
- HTTP协议中PUT和POST使用区别