[关闭]
@shark0017 2016-04-10T06:43:06.000000Z 字数 10091 阅读 4153

Adapter最佳实践

概要:使用Adapter的注意事项与优化方案
本文会不定期更新,推荐watch下项目。如果喜欢请star,如果觉得有纰漏请提交issue,如果你有更好的点子可以提交pull request。

本文的示例代码主要是基于CommonAdapter这个库编写的,若你有其他的技巧和方法可以参与进来一起完善这篇文章。
固定连接:https://github.com/tianzhijiexian/Android-Best-Practices

一、需求背景

如果用继承BaseAdapter的思路来做多类型的item,代码就会变得很丑,而且难于阅读
无法定义adapter是属于哪个层的
item对于adapter来说应该是可插拔的,item的类型再多也不应该增加复杂度
写adapter的时候我的心情是不愉悦的,因为要考虑很多优化策略
adapter藏有模板式代码,我讨厌模板式代码

二、需求

  1. 数据不应知道adapter和view的存在
  2. adapter不应是一个独立的类,它更合适作内部类
  3. 作为内部类的adapter应该有明确的层级位置
  4. adapter能支持多种item类型,能仅改动两行代码即可添加一个新的item
  5. adapter能对自身的item进行自动复用,无需手动判断
  6. adapter对findviewById方法应有自动的优化策略,类似于ViewHolder
  7. item能独立的处理自身的逻辑和点击事件,其自身应具有极高的独立性和可维护性
  8. item自身的setListener应仅设置一次,不用在getView时被重复设置
  9. adapter应提供item的局部刷新功能
  10. Listview的adapter应在稍微修改后支持recyclerView,方便过渡
  11. 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工程的特性进行网络层快速的单元测试。
为了说明,我建立了一个数据模型:

  1. public class DemoModel {
  2. public String content;
  3. public String type;
  4. }

它就是一个POJO,没有任何特别之处,它完全不知道其他对象的存在。

将Adapter变成内部类

我现在如果想要让adapter变成一个仅仅用于data和view进行绑定的工具,那么它里面就不应该有不属于它的操作,它该像下面这样:

  1. listView.setAdapter(new CommonAdapter<DemoModel>(data) {
  2. @Override
  3. public AdapterItem<DemoModel> createItem(Object type) {
  4. return new TextItem();
  5. }
  6. });

Adapter不属于UI层

当我们让adapter变成一个内部类的时候,剩下的问题就是adapter应该处于view层还是presenter或是model层。在实际的运用当中,我最终定义adapter是处于presenter层(mvp)或者model层(mvvm)。因为ui层面有可能出现复用的情况,而且adapter中还会出现和数据相关的一些操作,所以应该让其独立于ui层。

支持多种item类型

  1. listView.setAdapter(new CommonAdapter<DemoModel>(data, 3) {
  2. @Override
  3. public Object getItemType(DemoModel demoModel) {
  4. // 返回item的类型
  5. return demoModel.type;
  6. }
  7. @Override
  8. public AdapterItem<DemoModel> getItemView(Object type) {
  9. switch ((String) type) {
  10. case "text":
  11. return new TextItem();
  12. case "button":
  13. return new ButtonItem();
  14. case "image":
  15. return new ImageItem();
  16. }
  17. }
  18. });

现在如果来了新的需求,让你多支持一个item类型,你只需要在switch-case语句块中新增一个case就行,简单且安全。
在做这样的操作时,可以写上default这个条件,以免出现不可预知的错误,毕竟来自服务器的数据也是不能完全相信的。

自动复用内部的item

我们之前对adapter的优化经常是需要在getView中判断convertView是否为null,如果不为空就不new出新的view,这样来实现item复用。先来看看上面已经出现多次的AdapterItem是个什么。

  1. public interface AdapterItem<T> {
  2. /**
  3. * @return item布局文件的layoutId
  4. */
  5. @LayoutRes
  6. int getLayoutResId();
  7. /**
  8. * 初始化views
  9. */
  10. void bindViews(final View root);
  11. /**
  12. * 设置view
  13. */
  14. void setViews();
  15. /**
  16. * 根据数据来设置item的内部views
  17. *
  18. * @param model 数据list内部的model
  19. * @param position 当前adapter调用item的位置
  20. */
  21. void handleData(T model, int position);
  22. }
方法 描述 做的工作
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复用的优化:

  1. LayoutInflater mInflater;
  2. @Override
  3. public View getView(int position, View convertView, ViewGroup parent) {
  4. // 不重复创建inflater对象,无论你有多少item,我都仅仅创建一次
  5. if (mInflater == null) {
  6. mInflater = LayoutInflater.from(parent.getContext());
  7. }
  8. AdapterItem<T> item;
  9. if (convertView == null) {
  10. // 当convertView为null,说明没有复用的item,那么就new出来
  11. item = getItemView(mType);
  12. convertView = mInflater.inflate(item.getLayoutResId(), parent, false);
  13. convertView.setTag(R.id.tag_item, item);
  14. // 调用bindView进行view的findview。
  15. // 可以看到仅仅是新new出来的view才会调用
  16. item.onBindViews(convertView);
  17. // findview后开始setView。将绑定和设置分离,方便整理代码结构
  18. item.onSetViews();
  19. } else {
  20. // 如果这个item是可以复用的,那么直接返回
  21. item = (AdapterItem<T>) convertView.getTag(R.id.tag_item);
  22. }
  23. // 无论你是不是复用的item,都会在getView时触发updateViews方法,更新数据
  24. item.onUpdateViews(mDataList.get(position), position);
  25. return convertView;
  26. }

这个库最根本的方法就是这一段,所以你只需要明白这一段代码做的事情,即使你以后在使用这个库时遇到了什么问题,你都可以不必惊慌,因为你掌握了它的原理。

对findviewById方法的优化

通过上述对源码的分析,现在只需要在bindViews中写findview即可让这个库自动实现优化。我喜欢用databinding,一行代码解决问题:

  1. private DemoItemImageBinding b;
  2. @Override
  3. public void bindViews(View root) {
  4. b = DataBindingUtil.bind(root);
  5. }

传统做法:

  1. TextView textView;
  2. @Override
  3. public void bindViews(View root) {
  4. textView = (TextView) root.findViewById(R.id.textView);
  5. }

item能独立的处理自身的逻辑和事件

假设你的item就是一个textView:

  1. <?xml version="1.0" encoding="utf-8"?>
  2. <TextView xmlns:android="http://schemas.android.com/apk/res/android"
  3. android:id="@+id/textView"
  4. android:layout_width="match_parent"
  5. android:layout_height="wrap_content"
  6. />

现在只需要这么写:

  1. public class TextItem implements AdapterItem<DemoModel> {
  2. public int getLayoutResId() {
  3. return R.layout.demo_item_text;
  4. }
  5. TextView textView;
  6. public void bindViews(View root) {
  7. textView = (TextView) root.findViewById(R.id.textView);
  8. }
  9. public void setViews() {}
  10. public void handleData(DemoModel model, int position) {
  11. textView.setText(model.content);
  12. }
  13. }

现在,你可以将它放入不同的界面,只需要给他同样的数据模型即可。当然,这种一个item被多个页面用的情形中还可以做更多的优化,比如在RecyclerView设置全局的缓存池等。

注意:

我强烈建议不要用itemOnListener做点击的判断,而是在每个item中做判断。这样的好处就是item自身知道自己的所有操作,而listview仅仅做个容器。现在RecyclerView的设计思路也是如此的,让item独立性增加。

item自身的setListener应仅设置一次

我们之前会图省事在listview的getView中随便写监听器,以至于出现了监听器爆炸的现象。现在,我们在setViews中写上监听器就行。

  1. public class ButtonItem implements AdapterItem<DemoModel> {
  2. /**
  3. * @tips
  4. * 优化小技巧:在这里直接设置按钮的监听器。
  5. * 因为这个方法仅仅在item建立时才调用,所以不会重复建立监听器。
  6. */
  7. @Override
  8. public void onSetViews() {
  9. // 这个方法仅仅在item构建时才会触发,所以在这里也仅仅建立一次监听器,不会重复建立
  10. b.button.setOnClickListener(new View.OnClickListener() {
  11. @Override
  12. public void onClick(View v) {
  13. // ...
  14. }
  15. });
  16. }
  17. @Override
  18. public void onUpdateViews(DemoModel model, int position) {
  19. // 在每次适配器getView的时候就会触发,这里避免做耗时的操作
  20. }
  21. }

提供局部刷新功能

这个功能在recyclerView中就已经提供了,我就不废话了。推荐直接使用recyclerView来做列表。在react-native的源码中我也看到了对recyclerView的支持。网上流传比较多的是用下面的代码做listview的单条刷新:

  1. private void updateSingleRow(ListView listView, long id) {
  2. if (listView != null) {
  3. int start = listView.getFirstVisiblePosition();
  4. for (int i = start, j = listView.getLastVisiblePosition(); i <= j; i++)
  5. if (id == ((Messages) listView.getItemAtPosition(i)).getId()) {
  6. View view = listView.getChildAt(i - start);
  7. getView(i, view, listView);
  8. break;
  9. }
  10. }
  11. }

其实就是手动调用了对应position的item的getView方法,个人觉得不是很好,为何不直接使用recyclerView呢?
现在的commonadapter支持了ObservableList对象,现在只需要操作这个list即可,所有的局部刷新全部会自动进行。

Listview无痛迁移至recyclerView

如今recyclerView大有接替listview的趋势,要知道listview的适配器和recyclerView的适配器的写法是不同的。
上面给出的例子都是listview的写法,我在这里在引用一下:

  1. listView.setAdapter(new CommonAdapter<DemoModel>(data1) {
  2. @Override
  3. public AdapterItem<DemoModel> getItemView(Object type) {
  4. return new TextItem();
  5. }
  6. });

换成recyclerView的适配器应该需要很多步吧?不,一行代码早回家~

  1. recyclerView.setAdapter(new CommonRcvAdapter<DemoModel>(data) {
  2. public AdapterItem<DemoModel> getItemView(Object type) {
  3. return new TextItem();
  4. }
  5. });

这里换了一个适配器的类名和容器名,其余的都没变。

Adapter支持MVVM的数据绑定

CommonAdapter可以结合dataBinding中的ObservableList进行数据的自动绑定操作。源码如下:

  1. protected CommonRcvAdapter(@NonNull ObservableList<T> data) {
  2. this((List<T>) data);
  3. data.addOnListChangedCallback(new ObservableList.OnListChangedCallback<ObservableList<T>>() {
  4. @Override
  5. public void onChanged(ObservableList<T> sender) {
  6. notifyDataSetChanged();
  7. }
  8. @Override
  9. public void onItemRangeChanged(ObservableList<T> sender, int positionStart, int itemCount) {
  10. notifyItemRangeChanged(positionStart, itemCount);
  11. }
  12. @Override
  13. public void onItemRangeInserted(ObservableList<T> sender, int positionStart, int itemCount) {
  14. notifyItemRangeInserted(positionStart, itemCount);
  15. notifyItemRangeChanged(positionStart, itemCount);
  16. }
  17. @Override
  18. public void onItemRangeRemoved(ObservableList<T> sender, int positionStart, int itemCount) {
  19. notifyItemRangeRemoved(positionStart, itemCount);
  20. notifyItemRangeChanged(positionStart, itemCount);
  21. }
  22. @Override
  23. public void onItemRangeMoved(ObservableList<T> sender, int fromPosition, int toPosition, int itemCount) {
  24. // Note:不支持一次性移动"多个"item的情况!!!!
  25. notifyItemMoved(fromPosition, toPosition);
  26. notifyDataSetChanged();
  27. }
  28. });
  29. }

现在只要list变了,adapter就会自动去更新界面,简单方便。

和Adapter不太相关的需求

1. 如果item中要加载图片,请在线程中加载,加载好了后切回主线程显示

有了RxAndroid这都不是事,当然一般的图片框架也会做这点。如果你使用的图片框架中没有做这样的处理,请务必加上。

2. 在快速滑动时不加载网络图片或停止gif图的播放

这个在QQ空间和微信朋友圈详情页中很常见,这个工作我仍旧希望交给图片加载框架做,而不是手动处理。因为手动处理对程序员的懒惰程度和知识水平有要求,所以还是交给库做放心。如果你的库没有做这样的处理,可以参考Android-Universal-Image-Loader中的实现方法。
核心代码:

  1. @Override
  2. public void onScrollStateChanged(AbsListView view, int scrollState) {
  3. switch (scrollState) {
  4. case OnScrollListener.SCROLL_STATE_IDLE:
  5. imageLoader.resume();
  6. break;
  7. case OnScrollListener.SCROLL_STATE_TOUCH_SCROLL:
  8. if (pauseOnScroll) {
  9. imageLoader.pause();
  10. }
  11. break;
  12. case OnScrollListener.SCROLL_STATE_FLING:
  13. if (pauseOnFling) {
  14. imageLoader.pause();
  15. }
  16. break;
  17. }
  18. if (externalListener != null) {
  19. externalListener.onScrollStateChanged(view, scrollState);
  20. }
  21. }

3. 判断item已有的数据和新数据是否不同

如果是加载图片,我还是希望你去看看你用的图片框架有没有做这样的优化,如果有就请放心,如果没有,那么请自己在框架中配置或者写工具类。

这里的情况不仅仅适用于图片也适用于其他的数据,如果你的item中文字很多,经常有几百个文字。那么也可以先判断要显示的文字和textview中已经有的文字是否一致,如果不一致再调用setText方法。下面是一个例子:

  1. /**
  2. * @tips 优化小技巧:对于图片这样的对象,我们先判断要加载的图片是不是之前的图片,如果是就不重复加载了
  3. * 这里为了演示方便没从网络加图,所以url是用int标识的,一般情况下都是用string标识
  4. *
  5. * 这里仅仅是用图片做个说明,你完全可以在textview显示文字前判断一下要显示的文字和已经显示的文字是否不同
  6. */
  7. @Override
  8. public void updateViews(DemoModel model, int position) {
  9. if (b.imageView.getTag() != null) {
  10. mOldImageUrl = (int) b.imageView.getTag();
  11. }
  12. int imageUrl = Integer.parseInt(model.content);
  13. if (mOldImageUrl == 0 && mOldImageUrl != imageUrl) {
  14. Log.d(ImageItem.class.getSimpleName(), "update image--------->");
  15. b.imageView.setTag(imageUrl);
  16. b.imageView.setImageResource(imageUrl); // load local image
  17. }
  18. }

4. 如果一个item过于复杂,可以将其拆分成多个小的item

关于这点是facebook提出的优化技巧,后来我了解到ios本身就是这么做的。

item.png-224.2kB

如图所示,这个item很复杂,而且很大。当你的item占据三分之二屏幕的时候就可以考虑这样的优化方案了。右图说明了将一个整体的item变成多个小item的效果,在这种拆分后,你会发现原来拆分后的小的item可能在别的界面也用到了,可以在写其他需求的时候也用一下,这就出现了item模块化的思想,总之是一个挺有意思的优化思路。

详细的文章(中文)请参考:facebook新闻页ListView的优化方案,十分感谢作者的分享和翻译~

5. 如果item中文本过多,可以采用textview的预渲染方案
如果你是做bbs或者做新闻的,你会发现item中会有大量的文字。textview其实是一个很基本但不简单的view,里面做了大量的判断和处理。当你有心想要优化textview的时候,你会发现在我们知道这个item中textview的宽度和文字大小的情况下可以把初始化的配置做个缓存,每个textview只需要用这个配置好的东西进行文字的渲染即可。

Instagram(现已在facebook旗下)分享了他们是如何优化他们的TextView渲染的效率的,在国内有作者也专门写了一篇文章来说明实际的原理和最终的效果,文章短小精悍,值得一读
我之后在github上问他后续的工作安排,他回答到准备做好一个优化textview的库并放出,希望到时候能帮助到大家。

下面是通过优化得到的结果:

staticLayout.png-37.5kB

这里测试的机器是MX3,左侧是直接使用StaticLayout的方案,右侧是系统的默认方案,Y轴是FPS,可以看出来,使用优化之后的方案,帧率提升了许多。

6. 通过自定义viewGroup来优化item,从而减少重复的measure

viewgroup.png-57.7kB

facebook的工程师讲解了他们对上面这个布局的优化策略,内容翔实,是个很好的分享。中文翻译版本:听FackBook工程师讲Custom ViewGroups

四、后记

用不用一个第三方库我有下面的几点建议:

  1. 如果你不了解其内部的实现,那么尽可能少用。因为出了问题无从查找
  2. 如果你遇到一个很好的库,不妨看下内部的实现,既能学到东西,又可以在以后出问题的时候快速定位问题
  3. 如果遇到复杂的库,比如网络和图片库。全部知道其原理是很难的,也需要成本,而你自己写也是不现实的,所以需要挑选很有名气的库来用。这样即使遇到了问题,也会有很多资料可以搜到
  4. 不要抵触国人的库,国人的库更加接地气,说不定还更好,还可以更加方便的提出issue。

探索无止境,优化没底线,我还是希望能有库在库中做好很多的优化操作,降低对程序员的要求,最终希望谁都可以写代码。简单编程,快乐生活。本文的完成离不开朋友们的支持和帮助,感谢:MingleArch、豪哥的批评和建议。

示例代码下载:https://github.com/tianzhijiexian/CommonAdapter

作者

avatar.png-40.4kB
developer_kale@foxmail.com
@天之界线2010

参考文章:
http://www.cnblogs.com/tianzhijiexian/p/4278546.html

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注