@shark0017
2016-04-10T14:43:06.000000Z
字数 10091
阅读 4276
概要:使用Adapter的注意事项与优化方案
本文会不定期更新,推荐watch下项目。如果喜欢请star,如果觉得有纰漏请提交issue,如果你有更好的点子可以提交pull request。
本文的示例代码主要是基于CommonAdapter这个库编写的,若你有其他的技巧和方法可以参与进来一起完善这篇文章。
固定连接:https://github.com/tianzhijiexian/Android-Best-Practices
如果用继承BaseAdapter的思路来做多类型的item,代码就会变得很丑,而且难于阅读
无法定义adapter是属于哪个层的
item对于adapter来说应该是可插拔的,item的类型再多也不应该增加复杂度
写adapter的时候我的心情是不愉悦的,因为要考虑很多优化策略
adapter藏有模板式代码,我讨厌模板式代码
与适配器不太相关的需求:
1. 如果item中要加载网络或本地图片,请在线程中加载,加载好后切回主线程显示
2. 在快速滑动时不加载网络图片或停止gif图的播放
3. 判断item已经显示的数据和需要显示的新数据是否不同,如果不同就更新,否则不更新
4. 如果一个item过于复杂,可以将其拆分成多个小的item
5. 如果item中文本过多,可以采用textview的预渲染方案
6. 如果发现item因为measure任务过重,而出现掉帧,则需要通过自定义view来优化此item。这种方案适用于,某个item在应用中频繁使用的情形。
view肯定需要知道设置给自己的数据,adapter肯定要知道view和数据,但数据应该对其他的东西完全不知情。
数据的傻瓜化的好处有很多,如果这么做了,我们甚至可以把网络层和解析的model放入java项目中,利用java工程的特性进行网络层快速的单元测试。
为了说明,我建立了一个数据模型:
public class DemoModel {
public String content;
public String type;
}
它就是一个POJO,没有任何特别之处,它完全不知道其他对象的存在。
我现在如果想要让adapter变成一个仅仅用于data和view进行绑定的工具,那么它里面就不应该有不属于它的操作,它该像下面这样:
listView.setAdapter(new CommonAdapter<DemoModel>(data) {
@Override
public AdapterItem<DemoModel> createItem(Object type) {
return new TextItem();
}
});
当我们让adapter变成一个内部类的时候,剩下的问题就是adapter应该处于view层还是presenter或是model层。在实际的运用当中,我最终定义adapter是处于presenter层(mvp)或者model层(mvvm)。因为ui层面有可能出现复用的情况,而且adapter中还会出现和数据相关的一些操作,所以应该让其独立于ui层。
listView.setAdapter(new CommonAdapter<DemoModel>(data, 3) {
@Override
public Object getItemType(DemoModel demoModel) {
// 返回item的类型
return demoModel.type;
}
@Override
public AdapterItem<DemoModel> getItemView(Object type) {
switch ((String) type) {
case "text":
return new TextItem();
case "button":
return new ButtonItem();
case "image":
return new ImageItem();
}
}
});
现在如果来了新的需求,让你多支持一个item类型,你只需要在switch-case语句块中新增一个case就行,简单且安全。
在做这样的操作时,可以写上default
这个条件,以免出现不可预知的错误,毕竟来自服务器的数据也是不能完全相信的。
我们之前对adapter的优化经常是需要在getView中判断convertView是否为null,如果不为空就不new出新的view,这样来实现item复用。先来看看上面已经出现多次的AdapterItem
是个什么。
public interface AdapterItem<T> {
/**
* @return item布局文件的layoutId
*/
@LayoutRes
int getLayoutResId();
/**
* 初始化views
*/
void bindViews(final View root);
/**
* 设置view
*/
void setViews();
/**
* 根据数据来设置item的内部views
*
* @param model 数据list内部的model
* @param position 当前adapter调用item的位置
*/
void handleData(T model, int position);
}
方法 | 描述 | 做的工作 |
---|---|---|
getLayoutResId | 你这个item的布局文件是什么 | 返回一个R.layout.xxx |
bindViews | 在这里做findviewById的工作 | btn = findViewById(R.id.xx) |
setViews | 在这里初始化view各个参数 | setcolor ,setOnClickListener... |
handleData | 数据更新时会调用(类似getView) | button.setText(model.text) |
其实这里就是view的几个过程,首先初始化布局文件,然后绑定布局文件中的各个view,接着进行各个view的初始化操作,然后在数据更新时进行更新的工作。
原理:
分析完毕后,我去源码里面翻了一下,终于发现了这个库对item复用的优化:
LayoutInflater mInflater;
@Override
public View getView(int position, View convertView, ViewGroup parent) {
// 不重复创建inflater对象,无论你有多少item,我都仅仅创建一次
if (mInflater == null) {
mInflater = LayoutInflater.from(parent.getContext());
}
AdapterItem<T> item;
if (convertView == null) {
// 当convertView为null,说明没有复用的item,那么就new出来
item = getItemView(mType);
convertView = mInflater.inflate(item.getLayoutResId(), parent, false);
convertView.setTag(R.id.tag_item, item);
// 调用bindView进行view的findview。
// 可以看到仅仅是新new出来的view才会调用
item.onBindViews(convertView);
// findview后开始setView。将绑定和设置分离,方便整理代码结构
item.onSetViews();
} else {
// 如果这个item是可以复用的,那么直接返回
item = (AdapterItem<T>) convertView.getTag(R.id.tag_item);
}
// 无论你是不是复用的item,都会在getView时触发updateViews方法,更新数据
item.onUpdateViews(mDataList.get(position), position);
return convertView;
}
这个库最根本的方法就是这一段,所以你只需要明白这一段代码做的事情,即使你以后在使用这个库时遇到了什么问题,你都可以不必惊慌,因为你掌握了它的原理。
通过上述对源码的分析,现在只需要在bindViews中写findview即可让这个库自动实现优化。我喜欢用databinding,一行代码解决问题:
private DemoItemImageBinding b;
@Override
public void bindViews(View root) {
b = DataBindingUtil.bind(root);
}
传统做法:
TextView textView;
@Override
public void bindViews(View root) {
textView = (TextView) root.findViewById(R.id.textView);
}
假设你的item就是一个textView:
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>
现在只需要这么写:
public class TextItem implements AdapterItem<DemoModel> {
public int getLayoutResId() {
return R.layout.demo_item_text;
}
TextView textView;
public void bindViews(View root) {
textView = (TextView) root.findViewById(R.id.textView);
}
public void setViews() {}
public void handleData(DemoModel model, int position) {
textView.setText(model.content);
}
}
现在,你可以将它放入不同的界面,只需要给他同样的数据模型即可。当然,这种一个item被多个页面用的情形中还可以做更多的优化,比如在RecyclerView设置全局的缓存池等。
注意:
我强烈建议不要用itemOnListener做点击的判断,而是在每个item中做判断。这样的好处就是item自身知道自己的所有操作,而listview仅仅做个容器。现在RecyclerView的设计思路也是如此的,让item独立性增加。
我们之前会图省事在listview的getView中随便写监听器,以至于出现了监听器爆炸的现象。现在,我们在setViews中写上监听器就行。
public class ButtonItem implements AdapterItem<DemoModel> {
/**
* @tips
* 优化小技巧:在这里直接设置按钮的监听器。
* 因为这个方法仅仅在item建立时才调用,所以不会重复建立监听器。
*/
@Override
public void onSetViews() {
// 这个方法仅仅在item构建时才会触发,所以在这里也仅仅建立一次监听器,不会重复建立
b.button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// ...
}
});
}
@Override
public void onUpdateViews(DemoModel model, int position) {
// 在每次适配器getView的时候就会触发,这里避免做耗时的操作
}
}
这个功能在recyclerView中就已经提供了,我就不废话了。推荐直接使用recyclerView来做列表。在react-native的源码中我也看到了对recyclerView的支持。网上流传比较多的是用下面的代码做listview的单条刷新:
private void updateSingleRow(ListView listView, long id) {
if (listView != null) {
int start = listView.getFirstVisiblePosition();
for (int i = start, j = listView.getLastVisiblePosition(); i <= j; i++)
if (id == ((Messages) listView.getItemAtPosition(i)).getId()) {
View view = listView.getChildAt(i - start);
getView(i, view, listView);
break;
}
}
}
其实就是手动调用了对应position的item的getView方法,个人觉得不是很好,为何不直接使用recyclerView呢?
现在的commonadapter支持了ObservableList
对象,现在只需要操作这个list即可,所有的局部刷新全部会自动进行。
如今recyclerView大有接替listview的趋势,要知道listview的适配器和recyclerView的适配器的写法是不同的。
上面给出的例子都是listview的写法,我在这里在引用一下:
listView.setAdapter(new CommonAdapter<DemoModel>(data,1) {
@Override
public AdapterItem<DemoModel> getItemView(Object type) {
return new TextItem();
}
});
换成recyclerView的适配器应该需要很多步吧?不,一行代码早回家~
recyclerView.setAdapter(new CommonRcvAdapter<DemoModel>(data) {
public AdapterItem<DemoModel> getItemView(Object type) {
return new TextItem();
}
});
这里换了一个适配器的类名和容器名,其余的都没变。
CommonAdapter可以结合dataBinding中的ObservableList
进行数据的自动绑定操作。源码如下:
protected CommonRcvAdapter(@NonNull ObservableList<T> data) {
this((List<T>) data);
data.addOnListChangedCallback(new ObservableList.OnListChangedCallback<ObservableList<T>>() {
@Override
public void onChanged(ObservableList<T> sender) {
notifyDataSetChanged();
}
@Override
public void onItemRangeChanged(ObservableList<T> sender, int positionStart, int itemCount) {
notifyItemRangeChanged(positionStart, itemCount);
}
@Override
public void onItemRangeInserted(ObservableList<T> sender, int positionStart, int itemCount) {
notifyItemRangeInserted(positionStart, itemCount);
notifyItemRangeChanged(positionStart, itemCount);
}
@Override
public void onItemRangeRemoved(ObservableList<T> sender, int positionStart, int itemCount) {
notifyItemRangeRemoved(positionStart, itemCount);
notifyItemRangeChanged(positionStart, itemCount);
}
@Override
public void onItemRangeMoved(ObservableList<T> sender, int fromPosition, int toPosition, int itemCount) {
// Note:不支持一次性移动"多个"item的情况!!!!
notifyItemMoved(fromPosition, toPosition);
notifyDataSetChanged();
}
});
}
现在只要list变了,adapter就会自动去更新界面,简单方便。
1. 如果item中要加载图片,请在线程中加载,加载好了后切回主线程显示
有了RxAndroid这都不是事,当然一般的图片框架也会做这点。如果你使用的图片框架中没有做这样的处理,请务必加上。
2. 在快速滑动时不加载网络图片或停止gif图的播放
这个在QQ空间和微信朋友圈详情页中很常见,这个工作我仍旧希望交给图片加载框架做,而不是手动处理。因为手动处理对程序员的懒惰程度和知识水平有要求,所以还是交给库做放心。如果你的库没有做这样的处理,可以参考Android-Universal-Image-Loader中的实现方法。
核心代码:
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
switch (scrollState) {
case OnScrollListener.SCROLL_STATE_IDLE:
imageLoader.resume();
break;
case OnScrollListener.SCROLL_STATE_TOUCH_SCROLL:
if (pauseOnScroll) {
imageLoader.pause();
}
break;
case OnScrollListener.SCROLL_STATE_FLING:
if (pauseOnFling) {
imageLoader.pause();
}
break;
}
if (externalListener != null) {
externalListener.onScrollStateChanged(view, scrollState);
}
}
3. 判断item已有的数据和新数据是否不同
如果是加载图片,我还是希望你去看看你用的图片框架有没有做这样的优化,如果有就请放心,如果没有,那么请自己在框架中配置或者写工具类。
这里的情况不仅仅适用于图片也适用于其他的数据,如果你的item中文字很多,经常有几百个文字。那么也可以先判断要显示的文字和textview中已经有的文字是否一致,如果不一致再调用setText方法。下面是一个例子:
/**
* @tips 优化小技巧:对于图片这样的对象,我们先判断要加载的图片是不是之前的图片,如果是就不重复加载了
* 这里为了演示方便没从网络加图,所以url是用int标识的,一般情况下都是用string标识
*
* 这里仅仅是用图片做个说明,你完全可以在textview显示文字前判断一下要显示的文字和已经显示的文字是否不同
*/
@Override
public void updateViews(DemoModel model, int position) {
if (b.imageView.getTag() != null) {
mOldImageUrl = (int) b.imageView.getTag();
}
int imageUrl = Integer.parseInt(model.content);
if (mOldImageUrl == 0 && mOldImageUrl != imageUrl) {
Log.d(ImageItem.class.getSimpleName(), "update image--------->");
b.imageView.setTag(imageUrl);
b.imageView.setImageResource(imageUrl); // load local image
}
}
4. 如果一个item过于复杂,可以将其拆分成多个小的item
关于这点是facebook提出的优化技巧,后来我了解到ios本身就是这么做的。
如图所示,这个item很复杂,而且很大。当你的item占据三分之二屏幕的时候就可以考虑这样的优化方案了。右图说明了将一个整体的item变成多个小item的效果,在这种拆分后,你会发现原来拆分后的小的item可能在别的界面也用到了,可以在写其他需求的时候也用一下,这就出现了item模块化的思想,总之是一个挺有意思的优化思路。
详细的文章(中文)请参考:facebook新闻页ListView的优化方案,十分感谢作者的分享和翻译~
5. 如果item中文本过多,可以采用textview的预渲染方案
如果你是做bbs或者做新闻的,你会发现item中会有大量的文字。textview其实是一个很基本但不简单的view,里面做了大量的判断和处理。当你有心想要优化textview的时候,你会发现在我们知道这个item中textview的宽度和文字大小的情况下可以把初始化的配置做个缓存,每个textview只需要用这个配置好的东西进行文字的渲染即可。
Instagram(现已在facebook旗下)分享了他们是如何优化他们的TextView渲染的效率的,在国内有作者也专门写了一篇文章来说明实际的原理和最终的效果,文章短小精悍,值得一读。
我之后在github上问他后续的工作安排,他回答到准备做好一个优化textview的库并放出,希望到时候能帮助到大家。
下面是通过优化得到的结果:
这里测试的机器是MX3,左侧是直接使用StaticLayout的方案,右侧是系统的默认方案,Y轴是FPS,可以看出来,使用优化之后的方案,帧率提升了许多。
6. 通过自定义viewGroup来优化item,从而减少重复的measure
facebook的工程师讲解了他们对上面这个布局的优化策略,内容翔实,是个很好的分享。中文翻译版本:听FackBook工程师讲Custom ViewGroups
用不用一个第三方库我有下面的几点建议:
探索无止境,优化没底线,我还是希望能有库在库中做好很多的优化操作,降低对程序员的要求,最终希望谁都可以写代码。简单编程,快乐生活。本文的完成离不开朋友们的支持和帮助,感谢:MingleArch、豪哥的批评和建议。
示例代码下载:https://github.com/tianzhijiexian/CommonAdapter
developer_kale@foxmail.com
@天之界线2010