一、ListView优化
ListView、Adapter以及Data source之间的关系
ListView的显示缓存机制原理图:需要才显示, 显示完就回收,节省系统资源
如下图所示,一屏幕显示了5个Item,通过Logcat也能看出来只有5个Log,分别打印的是convertView.toString()。
当滑动时(这里演示的是第三种getView的写法),滑动到第6个第7个时,Logcat的显示
全部Item加载完毕,也就是convertView全部不为空,可以看到,item7就是复用的item1的convertView。为了更明显的观察,将Item的个数增加到11个。
从编号为6开始,就是在复用0-5的convertView,这个个数是屏幕一屏能显示5个,而且会再预先加载1个,因此前6个是不重复的convertView,像下图也是类似
下面介绍三种写法由无优化到谷歌推荐用法。
第一种getView方法,效率最低下,完全没有用到ListView的缓存机制,每次都是新建一个View去绑定itemLayout,每次都进行一个findViewByID,都去遍历视图树寻找id,这个步骤是很消耗系统资源的,因此强烈不推荐。
public View getView(int position, View convertView, ViewGroup parent) {
View view = mInflater.inflate(R.layout.item_layout, null);
TextView a, b, c, d;
a = (TextView) view.findViewById(R.id.title);
b = (TextView) view.findViewById(R.id.desc);
c = (TextView) view.findViewById(R.id.time);
d = (TextView) view.findViewById(R.id.phone);
a.setText(mDatas.get(position).getTitle());
b.setText(mDatas.get(position).getDesc());
c.setText(mDatas.get(position).getTime());
d.setText(mDatas.get(position).getPhone());
return view;
}
第二种,则用到了getView中的convertView参数,当判断到convertView时null时,则会去创建,当判断非空时,表示已经缓存过了,那么直接从缓存中取出这个convertView即可,避免重复创建。
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
convertView = mInflater.inflate(R.layout.item_layout, null);
}
TextView a, b, c, d;
a = (TextView) convertView.findViewById(R.id.title);
b = (TextView) convertView.findViewById(R.id.desc);
c = (TextView) convertView.findViewById(R.id.time);
d = (TextView) convertView.findViewById(R.id.phone);
a.setText(mDatas.get(position).getTitle());
b.setText(mDatas.get(position).getDesc());
c.setText(mDatas.get(position).getTime());
d.setText(mDatas.get(position).getPhone());
return convertView;
}
第三种,也是Google推荐也是效率最高的实现方式,此方式的优化是在findViewByID上,通过ViewHolder来避免重复的findViewByID。首先创建一个ViewHolder内部类,内部定义的是ItemLayout中所包含的各个控件。这样只会在第一次初始化时关联控件,此后,都是从ViewHolder中取出这个关联关系。
以下是我自己的语言叙述,可能不太准确。
其实就是将view和layout的通道给用ViewHolder给保存下来了,如果convertView没有被创建过则先创建,并把保存路径的ViewHolder给设置成convertView的Tag,
当发现convertView已经存在,则只需要取出Tag中保存的ViewHolder就能找到路径,
有了这个所谓的路径,就可以将内容轻松设置到控件上。
效率高,用到了ListView提供的缓存机制,避免重复创建,避免重复寻找控件。
private class ViewHolder{
TextView mTitle, mDesc, mTime, mPhone;
}
public View getView(int position, View convertView, ViewGroup parent) {
ViewHolder viewHolder = null;
if (convertView == null) {
viewHolder = new ViewHolder();
convertView = mInflater.inflate(R.layout.item_layout, null);
viewHolder.mTitle = (TextView) convertView.findViewById(R.id.title);
viewHolder.mDesc = (TextView) convertView.findViewById(R.id.desc);
viewHolder.mTime = (TextView) convertView.findViewById(R.id.time);
viewHolder.mPhone = (TextView) convertView.findViewById(R.id.phone);
convertView.setTag(viewHolder);
}else{
viewHolder = (ViewHolder) convertView.getTag();
}
Bean bean = mDatas.get(position);
viewHolder.mTitle.setText(bean.getTitle());
viewHolder.mDesc.setText(bean.getDesc());
viewHolder.mTime.setText(bean.getTime());
viewHolder.mPhone.setText(bean.getPhone());
return convertView;
}
下面是itemLayout布局
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="10dp" >
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:singleLine="true"
android:text="Android新技能"
android:textColor="#444"
android:textSize="16sp" />
<TextView
android:id="@+id/desc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/title"
android:layout_marginTop="10dp"
android:maxLines="2"
android:minLines="1"
android:text="Android打造万能ListView和GridView适配器"
android:textColor="#898989"
android:textSize="16sp" />
<TextView
android:id="@+id/time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/desc"
android:layout_marginTop="10dp"
android:text="2016-7-11"
android:textColor="#898989"
android:textSize="12sp" />
<TextView
android:id="@+id/phone"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_below="@id/desc"
android:layout_marginTop="10dp"
android:background="#2ED667"
android:drawableLeft="@drawable/stat_sys_phone_call"
android:drawablePadding="5dp"
android:padding="3dp"
android:text="188****1028"
android:textColor="#fff"
android:textSize="12sp" />
</RelativeLayout>
接下来是完整的自定义Adapter
package com.example.commonadapter;
import java.util.List;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.TextView;
public class MyAdapter extends BaseAdapter {
private LayoutInflater mInflater;
private List<Bean> mDatas;
public MyAdapter(Context context, List<Bean> data){
mInflater = LayoutInflater.from(context);
this.mDatas = data;
}
@Override
public int getCount() {
return 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 viewHolder = null;
if (convertView == null) {
//其实就是将view和layout的通道给用ViewHolder给保存下来了
//如果convertView没有被创建过则先创建,并把保存路径的ViewHolder给设置成convertView的Tag
//当发现convertView已经存在,则只需要取出Tag中保存的ViewHolder就能找到路径
viewHolder = new ViewHolder();
convertView = mInflater.inflate(R.layout.item_layout, null);
viewHolder.mTitle = (TextView) convertView.findViewById(R.id.title);
viewHolder.mDesc = (TextView) convertView.findViewById(R.id.desc);
viewHolder.mTime = (TextView) convertView.findViewById(R.id.time);
viewHolder.mPhone = (TextView) convertView.findViewById(R.id.phone);
convertView.setTag(viewHolder);
}else{
viewHolder = (ViewHolder) convertView.getTag();
}
Bean bean = mDatas.get(position);
viewHolder.mTitle.setText(bean.getTitle());
viewHolder.mDesc.setText(bean.getDesc());
viewHolder.mTime.setText(bean.getTime());
viewHolder.mPhone.setText(bean.getPhone());
return convertView;
}
private class ViewHolder{
TextView mTitle, mDesc, mTime, mPhone;
}
}
最后,可以看出只有赋值操作是必须重复写的,其他的代码则可以进行抽取,这也是万能适配器的原理。
二、万能适配器
-
ViewHolder的优化抽取
跟普通的优化没有两样,只是将ViewHolder类抽取出来,并在此类中完成任意控件的获取,以及convertView的复用。而不是简单的定义Item控件变量,以及存储Item控件的引用。
因为这是个通用类,所以不能确定每个ItemLayout中包含哪种控件,所以不使用以前的方法在ViewHolder中定义控件类型为成员变量。取而代之的是Map,key值是控件ID,value值是确定的view,针对这个特性,最终决定使用SparseArray,也是一个特殊的Map,不过key值指定了为int,效率要比Map高。value值为View,但是事先并不知道View是哪种类型,所以才会使用以泛型为返回值的getView方法。
public class MyViewHolder {
private SparseArray<View> mViews;
private Context mContext;
private int mPosition;
private View mConvertView;
private MyViewHolder(Context context, int layoutId, ViewGroup parent, int position){
this.mPosition = position;
this.mViews = new SparseArray<View>();
mConvertView = LayoutInflater.from(context).inflate(layoutId, parent, false);;
mConvertView.setTag(this);
}
public static MyViewHolder get(Context context, View convertView, int layoutId, ViewGroup parent, int position){
if (convertView == null) {
return new MyViewHolder(context, layoutId, parent, position);
}else{
MyViewHolder holder = (MyViewHolder) convertView.getTag();
holder.mPosition = position;
return holder;
}
}
/**
* 通过viewId获取控件
* @param viewId
* @return
*/
public <T extends View> T getView(int viewId){
View view = mViews.get(viewId);
if (view == null) {
view = mConvertView.findViewById(viewId);
mViews.put(viewId, view);
}
return (T) view;
}
public View getConvertView(){
return mConvertView;
}
}
可以看到这个类将构造方法私有化,并提供静态方法像以前的一样来初始化和复用convertView,当为空时就new一个ViewHolder并同样用LayoutInflater来绑定ItemLayout,然后setTag。当不为空时,同样的步骤通过getTag方法取出viewHolder。最后position虽然没有用到,但还是应该让其保存正确的数据。
再看getView方法,利用了泛型,通过传入viewId来将ViewHolder和具体控件之间建立引用关系。以前为了避免重复的findViewByID是将holder的参数赋值为这种引用,此时却不行,上面的一段话也说过,因为不明确ItemLayout中到底包含了什么控件,所以用容器来代替以前的方法。getView时先在容器中查找,查找不到则findViewByID后添加到容器中,当再需要getView的时候不需要遍历视图树。
在adapter中也摆脱了繁杂的对每一个控件的findViewByID,只留下对具体控件设置的具体内容,因为这必须是自定义的。
@Override
public View getView(int position, View convertView, ViewGroup parent) {
MyViewHolder holder = MyViewHolder.get(mContext, convertView, R.layout.item_layout, parent, position);
Bean bean = mDatas.get(position);
((TextView)holder.getView(R.id.title)).setText(bean.getTitle());
((TextView)holder.getView(R.id.desc)).setText(bean.getDesc());
((TextView)holder.getView(R.id.time)).setText(bean.getTime());
((TextView)holder.getView(R.id.phone)).setText(bean.getPhone());
return holder.getConvertView();
}
-
CommonAdapter的优化抽取
分析发现每一个Adapter都要实现的方法getCount,getItem,getItemId以及getView方法,因此想自建一个CommonAdapter,在这里实现大同小异的方法。而用户只需要实现getView方法就行。当然因为要继承此类,所以又要用泛型来代替具体的List元素。
public abstract class CommonAdapter<T> extends BaseAdapter {
protected Context mContext;
protected List<T> mDatas;
protected LayoutInflater mInflater;
public CommonAdapter(Context context, List<T> datas){
this.mContext = context;
this.mDatas = datas;
mInflater = LayoutInflater.from(context);
}
@Override
public int getCount() {
return mDatas.size();
}
@Override
public T getItem(int position) {
return mDatas.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public abstract View getView(int position, View convertView, ViewGroup parent);
}
public class MyAdapterCommon extends CommonAdapter<Bean> {
public MyAdapterCommon(Context context, List<Bean> datas){
super(context, datas);
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
MyViewHolder holder = MyViewHolder.get(mContext, convertView, R.layout.item_layout, parent, position);
Bean bean = mDatas.get(position);
((TextView)holder.getView(R.id.title)).setText(bean.getTitle());
((TextView)holder.getView(R.id.desc)).setText(bean.getDesc());
((TextView)holder.getView(R.id.time)).setText(bean.getTime());
((TextView)holder.getView(R.id.phone)).setText(bean.getPhone());
return holder.getConvertView();
}
}
可以发现实现代码已经少了许多。
接下来还可以继续提取优化。在CommonAdapter构造方法中加入ItemLayout的id,方便更好的适配,而不是仅支持此一个的item。
public CommonAdapter(Context context, List<T> datas, int layoutId){
this.mContext = context;
this.mDatas = datas;
this.mInflater = LayoutInflater.from(context);
this.mLayoutId = layoutId;
}
同时取消getView方法的抽象,就在CommonAdapter中实现,具体的实现跟在子类中的实现是相同的。
@Override
public View getView(int position, View convertView, ViewGroup parent){
MyViewHolder holder = MyViewHolder.get(mContext, convertView, mLayoutId, parent, position);
convert(holder, getItem(position));
return holder.getConvertView();
}
再分析,还是只有TextView的设置文本是自己动手写的,于是抽象一个方法,用来设置控件的具体内容。
public abstract void convert(MyViewHolder holder, T t);
@Override
public void convert(MyViewHolder holder, Bean bean) {
((TextView)holder.getView(R.id.title)).setText(bean.getTitle());
((TextView)holder.getView(R.id.desc)).setText(bean.getDesc());
((TextView)holder.getView(R.id.time)).setText(bean.getTime());
((TextView)holder.getView(R.id.phone)).setText(bean.getPhone());
}
越来越精简的MyAdapter。还可以更加的简化。在ViewHolder中定义各个控件的主要内容设置的方法。比如说TextView的setText。
public MyViewHolder setText(int viewId, String text){
TextView tv = getView(viewId);
tv.setText(text);
return this;
}
为了使用链式编程,所以返回值类型就是MyViewHolder。设置文本的语句也变得更加简便,只有一句,所以没有必要再新建一个类继承CommonAdapter了,只需要在MainActivity中写一个匿名内部类就解决了。
holder.setText(R.id.title, bean.getTitle())
.setText(R.id.desc, bean.getDesc())
.setText(R.id.time, bean.getTime())
.setText(R.id.phone, bean.getPhone());
mListView.setAdapter(new CommonAdapter<Bean>(MainActivity.this, mDatas, R.layout.item_layout) {
@Override
public void convert(MyViewHolder holder, Bean bean) {
holder.setText(R.id.title, bean.getTitle())
.setText(R.id.desc, bean.getDesc())
.setText(R.id.time, bean.getTime())
.setText(R.id.phone, bean.getPhone());
}
});
虽然说匿名内部类更简单,但是如果有些控件含有各种事件,点击事件等,还是新建个类继承CommonAdapter,在新的类中去实现。
当然如果ItemLayout中包含ImageView,则需要在ViewHolder中添加如下方法
public MyViewHolder setImageResource(int viewId, int resId){
ImageView img = getView(viewId);
img.setImageResource(resId);
return this;
}
当然,可以添加很多很多的辅助方法,来简化用户使用ViewHolder和设置控件内容,当每一次使用发现没有这个辅助方法时,就添加进去,久而久之,这个ViewHolder类就会越来越完善。