Android之ListView性能优化——一行代码绑定数据——万能适配器
如下图,加入现在有一个这样的需求图,你会怎么做?作为一个初学者,之前我都是直接用SimpleAdapter结合一个Item的布局来实现的,感觉这样实现起来很方便(基本上一行代码就可以实现),而且也没有觉得有什么不好的。直到最近在慕课网上看到鸿洋大神讲的“机器人小慕”和“万能适配器”两节课,才对BaseAdapter有所了解。看了鸿洋大神的课程之后,我又上网搜了几个博客,也看了一些源码和文档,于是打算写一个帖子来记录一下自己的学习历程。
在今天的帖子中,我们从一个最基本的实现BaseAdapter的适配器开始,先介绍ListView性能优化(convertView结合ViewHolder类),再将其封装起来,最后达到可以像SimpleAdapter一样可以一行代码搞定一个ListView的数据绑定。
总结一下,本帖子要实现的功能:
- 为一个继承自BaseAdapter的原始的Adapter添加ViewHolder,达到缓存的功能
- 将优化后的ListView进行封装,实现一行代码为ListView绑定数据
一、最原始的适配器类的实现
最原始的思想就是常见一个继承自BaseAdapter的适配器类,在getView()方法中找到子View的布局,获取到子View中的控件,再为其绑定数据。简略代码如下:
1 @Override 2 public View getView(int position, View convertView, ViewGroup parent) { 3 convertView = inflater.inflate(R.layout.sideworks_main_userlist_item, parent, false); 4 ImageView userPhoto = (ImageView) convertView.findViewById(R.id.find_listitem_photo); 5 TextView userName = (TextView) convertView.findViewById(R.id.find_listitem_name); 6 User user = userList.get(position); 7 userPhoto.setImageResource(user.getPhotoRes()); 8 userName.setText(user.getUserName()); 9 return convertView; 10 }
这样写代码理论上是没有问题的,但是,看过源码的人都知道,convertView这个参数指的是 The old view to reuse, if possible. 也就是说,convertView是一个以前用过的子View,如果它存在的话,就可以复用它,即只要ListView中存在一个布局和这个子View一样的子View,那么那个子View就可以复用这个convertView。这样就有了一个缓存的机制,也就是靠这个机制,我们可以达到ListView性能优化的目的。
二、convertView结合ViewHolder类实现ListView性能优化
先看代码。
1 @Override 2 public View getView(int position, View convertView, ViewGroup parent) { 3 ViewHolder holder = null; 4 if (convertView == null) { 5 convertView = inflater.inflate(R.layout.sideworks_main_userlist_item, parent, false); 6 holder = new ViewHolder(); 7 holder.userPhoto = (ImageView) convertView.findViewById(R.id.find_listitem_photo); 8 holder.userName = (TextView) convertView.findViewById(R.id.find_listitem_name); 9 convertView.setTag(holder); 10 } else { 11 holder = (ViewHolder) convertView.getTag(); 12 } 13 User user = userList.get(position); 14 holder.userPhoto.setImageResource(user.getPhotoRes()); 15 holder.userName.setText(user.getUserName()); 16 return convertView; 17 } 18 19 /** 20 * 存储以前用过的View中的控件的类 21 */ 22 class ViewHolder { 23 ImageView userPhoto; 24 TextView userName; 25 }
从代码中我们可以看到,和最开始的代码相比较,现在的代码多了一个内部类ViewHolder,这个类就是用来存储子View中的所有控件的。在Adapter的getView()方法中,我们首先判断convertView是否存在,如果不存在则说明这是第一次使用这个子View,此时我们就需要新建convertView和ViewHolder,找到ViewHolder中的所有控件,方便后面绑定数据,最后将ViewHolder存储到convertView缓存中;如果convertView存在,则说明以前已经用过一次这个子View,此时我们只需要从convertView的缓存中将ViewHolder去出来即可。得到ViewHolder实例之后,我们再根据List中的数据为子View中的所有控件绑定数据。
从理论上说,到此,ListView的优化已经结束,对于只有一个ListView的项目,这样已经是极限了。但是,如果一个项目中有100个ListView呢?如果有100个ListView,那么按照我们现在对ListView的优化程度,我们只能把Adapter复制粘贴100份,分别绑定100份数据,然后再绑到ListView上。这样是不是很愚蠢?可见,我们的“革命”尚未成功,仍需努力。下面,我们就来把这个Adapter封装起来。
三、封装适配器Adapter
我们的最终目的是要把ListView绑定数据的代码尽可能多的封装起来,尽可能达到一行代码为ListView绑定数据。要达到这个目标,我们需要克服以下几个困难:
- 对于不同的ListView,其ViewHolder不可能相同,因此我们需要一个可以适配所有布局的ViewHolder
- 对于不同的ListView,与之绑定的数据的类型不能确定,因此我们需要一个可以绑定所有数据类型的适配器
基于这两个问题,我们先给出我们解决问题的思路:先写出ViewHolder类,再写适配器类。
首先来看ViewHolder类。如上面所说的,我们需要一个可以适配所有布局的ViewHolder,因此,我们不能用TextView、ImageView等具体的控件类型,而是要用它们的父类View;其次,每个ListView只有一个ViewHolder,我们不可能对每个Item都用一个ViewHolder,因为这样不仅浪费了性能,而且也不合理,因此,我们使用单例模式来获得ViewHolder;另外,我们无法预测ViewHolder中都有几个控件,因此我们需要一个动态的数据结构来存储这些控件,我们选择的是SparseArray,它类似于HashMap,但其性能远远高于HashMap;最后,我们需要明确ViewHolder类中都干了什么:我们的ViewHolder类中主要进行对现在代码中getView()方法和ViewHolder类的封装。
最重要的一点,我们需要在ViewHolder类中开放若干个方法,一旦有新的控件类型,我们就需要在ViewHolder中为这种控件声明一个绑定数据的方法,如TextView对应setTextToTextView()方法,ImageView对应setResourceToImageView()、setBitmapToImageView()方法等。最后,我们可以用链式变成的思想,把这些方法的返回值设置成ViewHolder类型,这样,我们就可以通过 holder.setTextToTextView(?,?).setResourceToImageView(?,?)... 来实现数据的绑定,也就是一行代码搞定数据绑定。
说完了VeiwHolder类,让我们再来说说Adapter适配器类。因为是“万能适配器”,因此我为它起名为PowerfulAdapter。这个类中封装了BaseAdapter中的几个可以复用的方法,并且暴露出一个接口,让用户可以写具体的、每个Adapter都不同的代码。我们的思路是,将BaseAdapter中的getCount()、getItem()、getItemId()方法彻底封装起来,将getView()方法中获取ViewHolder的部分封装起来,把具体绑定数据的部分用一个抽象方法暴露给外界,让用户可以自定义绑定数据。
另外一个问题是关于数据源的类型问题。和ViewHolder类中的存放控件的集合一样,我们并不能事先预测到用户会设置什么数据,所以我们用一个通用泛型<T>来代替,这样我们就可以绑定随意类型的数据了。
这样一来,如果我们想要为一个ListView绑定数据,可以有两种方式:第一种,新建一个类继承PowerfulAdapter,并实现其中的抽象方法,为LsitView绑定数据;第二种,我们可以直接在Activity的onCreate()方法中实现绑定数据,直接用 listView.setAdapter(new PowerfulAdapter(){ ...... }); 来实现,“......”处写绑定数据的代码。
四、总结与代码
首先先来总结一下:在这个DEMO中,我们主要解决了一下几个问题:
- 为ListView绑定数据
- 使用convertView结合ViewHolder实现了数据缓存
- 对ViewHolder和Adapter进行了完美的封装,实现了一行代码为ListView绑定数据
以下是代码。在代码之前,国际惯例,让我们先看一看DEMO的文件结构,如下图所示:
以下是代码:
主界面布局 activity_main.xml 文件中的代码:
1 <?xml version="1.0" encoding="utf-8"?> 2 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 android:layout_width="match_parent" 4 android:layout_height="match_parent" 5 android:background="@android:color/white"> 6 7 <ListView 8 android:id="@+id/find_main_lv_userlist" 9 android:layout_width="match_parent" 10 android:layout_height="match_parent" 11 android:cacheColorHint="@android:color/transparent" 12 android:divider="@android:color/darker_gray" 13 android:dividerHeight="1.0dip" /> 14 15 </RelativeLayout>
主界面ListView的Item的布局 sideworks_main_userlist_item.xml 文件中的代码:
1 <?xml version="1.0" encoding="utf-8"?> 2 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" 3 android:layout_width="match_parent" 4 android:layout_height="wrap_content" 5 android:orientation="horizontal" 6 android:padding="6.0dip"> 7 8 <ImageView 9 android:id="@+id/find_listitem_photo" 10 android:layout_width="45.0dip" 11 android:layout_height="45.0dip" 12 android:src="@mipmap/ic_launcher" /> 13 14 <TextView 15 android:id="@+id/find_listitem_name" 16 android:layout_width="wrap_content" 17 android:layout_height="wrap_content" 18 android:layout_gravity="center_vertical" 19 android:layout_marginLeft="10.0dip" 20 android:textColor="@android:color/black" 21 android:textSize="18.0sp" 22 android:textStyle="bold" /> 23 24 </LinearLayout>
数据源实体类 User.java 文件中的代码:
1 package com.itgungnir.entity; 2 3 public class User { 4 private int photoRes; 5 private String userName; 6 7 public User(){ 8 9 } 10 11 public User(int photoRes, String userName) { 12 this.photoRes = photoRes; 13 this.userName = userName; 14 } 15 16 public int getPhotoRes() { 17 return photoRes; 18 } 19 20 public void setPhotoRes(int photoRes) { 21 this.photoRes = photoRes; 22 } 23 24 public String getUserName() { 25 return userName; 26 } 27 28 public void setUserName(String userName) { 29 this.userName = userName; 30 } 31 }
数据源生成类 DataGenerater.java 中的代码:
1 package com.itgungnir.tools; 2 3 import com.itgungnir.activity.R; 4 import com.itgungnir.entity.User; 5 6 import java.util.ArrayList; 7 import java.util.List; 8 9 /** 10 * 生成User列表数据的工具类 11 */ 12 public class DataGenerater { 13 /** 14 * 获取UserList列表数据 15 */ 16 public static List<User> getUserList() { 17 List<User> userList = new ArrayList<User>(); 18 for (int i = 0; i < 15; i++) { 19 userList.add(new User(R.mipmap.ic_launcher, "User Name " + (i + 1))); 20 } 21 return userList; 22 } 23 }
ListView的Item中控件的存放与处理类 ViewHolder.java 文件中的代码:
1 package com.itgungnir.tools; 2 3 import android.content.Context; 4 import android.util.SparseArray; 5 import android.view.LayoutInflater; 6 import android.view.View; 7 import android.view.ViewGroup; 8 import android.widget.ImageView; 9 import android.widget.TextView; 10 11 public class ViewHolder { 12 private SparseArray<View> views; // 存储ViewHolder中所有的控件(SparseArray类似于HashMap但性能强于HashMap) 13 private View convertView; // 以前用过的布局 14 15 /** 16 * 构造函数,因为ViewHolder之后一个,因此声明为private私有类型,通过下面的getInstance()方法获取 17 */ 18 private ViewHolder(Context context, int layoutId, ViewGroup parent) { 19 this.views = new SparseArray<View>(); // 初始化控件集合 20 this.convertView = LayoutInflater.from(context).inflate(layoutId, parent, false); // 第一次用布局 21 this.convertView.setTag(this); // 初始化布局之后将一个ViewHolder放入布局中作为缓存 22 } 23 24 /** 25 * 单例模式获取ViewHolder实例 26 */ 27 public static ViewHolder getInstance(View convertView, Context context, int layoutId, ViewGroup parent) { 28 if (convertView == null) { // 如果convertView不存在,则需要new一个ViewHolder并返回 29 return new ViewHolder(context, layoutId, parent); 30 } else { // 如果convertView存在,则只需要从convertView中将ViewHolder去出来返回即可 31 return (ViewHolder) convertView.getTag(); 32 } 33 } 34 35 /** 36 * 通过ID找到控件并返回 37 */ 38 private <T extends View> T getView(int viewId) { 39 View view = views.get(viewId); // 先尝试着从控件集合中取出控件 40 if (view == null) { // 如果view==null则说明控件没有加载到控件集合中,这时就需要手动查找控件并放到集合中 41 view = this.convertView.findViewById(viewId); 42 views.put(viewId, view); 43 } 44 return (T) view; 45 } 46 47 /** 48 * 设置一个为TextView设置文本的方法 49 */ 50 public ViewHolder setTextToTextView(int viewId,String text){ 51 TextView textView = getView(viewId); 52 textView.setText(text); 53 return this; 54 } 55 56 /** 57 * 设置一个为ImageView设置图片资源的方法 58 */ 59 public ViewHolder setResourceToImageView(int viewId,int resourceId){ 60 ImageView imageView = getView(viewId); 61 imageView.setImageResource(resourceId); 62 return this; 63 } 64 65 /** 66 * 开放一个返回convertView的方法,在适配器中会用到 67 */ 68 public View getConvertView() { 69 return this.convertView; 70 } 71 }
万能适配器类 PowerfulAdapter.java 文件中的代码:
1 package com.itgungnir.tools; 2 3 import android.content.Context; 4 import android.view.View; 5 import android.view.ViewGroup; 6 import android.widget.BaseAdapter; 7 8 import java.util.List; 9 10 public abstract class PowerfulAdapter<T> extends BaseAdapter { 11 private List<T> dataList; 12 private Context context; 13 private int layoutId; 14 15 public PowerfulAdapter(Context context, List<T> dataList, int layoutId) { 16 this.dataList = dataList; 17 this.context = context; 18 this.layoutId = layoutId; 19 } 20 21 @Override 22 public int getCount() { 23 return dataList.size(); 24 } 25 26 @Override 27 public T getItem(int position) { 28 return dataList.get(position); 29 } 30 31 @Override 32 public long getItemId(int position) { 33 return position; 34 } 35 36 @Override 37 public View getView(int position, View convertView, ViewGroup parent) { 38 ViewHolder holder = ViewHolder.getInstance(convertView, context, layoutId, parent); 39 convert(holder, dataList.get(position)); 40 return holder.getConvertView(); 41 } 42 43 public abstract void convert(ViewHolder holder, T t); 44 }
PowerfulAdapter类的子类MyAdapter.java文件中的代码,用于第一种绑定数据的方式:
1 package com.itgungnir.tools; 2 3 import android.content.Context; 4 5 import com.itgungnir.activity.R; 6 import com.itgungnir.entity.User; 7 8 import java.util.List; 9 10 public class MyAdapter extends PowerfulAdapter<User> { 11 12 public MyAdapter(Context context, List<User> dataList, int layoutId) { 13 super(context, dataList, layoutId); 14 } 15 16 @Override 17 public void convert(ViewHolder holder, User user) { 18 holder.setTextToTextView(R.id.find_listitem_name, user.getUserName()) 19 .setResourceToImageView(R.id.find_listitem_photo, user.getPhotoRes()); 20 } 21 }
主界面 MainActivity.java 中的代码:
1 package com.itgungnir.activity; 2 3 import android.app.Activity; 4 import android.os.Bundle; 5 import android.widget.ListView; 6 7 import com.itgungnir.entity.User; 8 import com.itgungnir.tools.DataGenerater; 9 import com.itgungnir.tools.MyAdapter; 10 import com.itgungnir.tools.PowerfulAdapter; 11 import com.itgungnir.tools.ViewHolder; 12 13 public class MainActivity extends Activity { 14 private ListView userList; 15 16 @Override 17 protected void onCreate(Bundle savedInstanceState) { 18 super.onCreate(savedInstanceState); 19 setContentView(R.layout.activity_main); 20 initView(); 21 } 22 23 private void initView() { 24 userList = (ListView) findViewById(R.id.find_main_lv_userlist); 25 /** 26 * 绑定数据:第一种方法,新建一个继承自PowerfulAdapter的Adapter类,在其中实现convert()方法 27 */ 28 userList.setAdapter(new MyAdapter(MainActivity.this, DataGenerater.getUserList(), R.layout.sideworks_main_userlist_item)); 29 /** 30 * 绑定数据:第二种方法,直接使用PowerfulAdapter,并现场实现其convert()方法 31 */ 32 // userList.setAdapter(new PowerfulAdapter<User>(MainActivity.this, DataGenerater.getUserList(), R.layout.sideworks_main_userlist_item) { 33 // @Override 34 // public void convert(ViewHolder holder, User user) { 35 // holder.setTextToTextView(R.id.find_listitem_name, user.getUserName()) 36 // .setResourceToImageView(R.id.find_listitem_photo, user.getPhotoRes()); 37 // } 38 // }); 39 } 40 }
最后奉上LZ的Git,欢迎各位大神批评指正:https://github.com/ITGungnir/TestPowerfulAdapter