您的位置:首页 > 其它

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

2016-03-16 10:53 567 查看
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);
}
}


模块化实体的设置

这一步,主要是:

将控件的赋值放到 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 方法是固定的。那么就可以将这些共性抽取到 BaseViewHolder

BaseViewHolder 类的主要作用是:

用泛型,抽象具体的实体类

供 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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: