[关闭]
@linux1s1s 2019-02-14T10:31:07.000000Z 字数 5204 阅读 2565

Android List 优化

AndroidWidget 2015-04


在讨论这个优化问题之前,请先阅读 Android View解析初步 这篇博文。
理解上面这篇博文,我们就会建立一个比较初级的印象:
通过布局xml文件在创建View并显示是很耗费时间和资源的操作。尽管布局文件已经编译打包成了二进制形式以便于更高效的语法解析,但是创建View仍然需要通过一个特殊的XML树,并实例化所有需要相应的View。

所以为了减少这些耗费时间和资源的操作,很有必要减少创建View的操作的次数。

ListView是如何运作的?

ListView通过回收一些不可见的Views,通常在Android源码中称为“ScrapView(废弃的View)”来解决这个问题。这及意味着开发者只需要简单的更新每行的内容而不需要针对每个单独的行的布局来创建View,所以ListView在运作的时候已经帮我们很好的提供了一种重用机制,重用在某种意义上说已经很好的减少了创建View的操作次数

创建View只是View完整显示在设备上这个复杂流程的一个前提,接下来还有onMeasure()、onLayout()、onDraw()等等这些更耗费时间和资源的操作,所以接下来,我们需要把优化的焦点放在布局和绘制上。

如何在布局和绘制上提高效率,一个很自然的想法就是 只绘制和布局在屏幕上可见的子View,那么如何做到这点呢?

在我们滑动屏幕时,ListView通过使用View回收器来增加低于或者高于当当前窗口的Views,并当前活动的Views移动到一个可回收池中。这样的话,ListView只需要在内存中保持足够多的Views去填充分配空间中的布局和一些额外的可回收Views,即使当你的Adapter有上百个items的适合。它会使用不同的方法去填充行之间的空间,从顶部或者底部等等,具体取决于窗口是如何变化的。下面这个图很直观的展示了当你按下ListView的情景:

此处输入图片的描述

代码实现

了解了ListView的运作方式,接下来一起看看代码如何实现

我们先来看看一个普通的页面的显示逻辑:Fragment里面全部放UI的显示逻辑,关于MVC的module这一块,我们称之为DataProvider,所以DataProvider这些在Worker线程里面。而Fragment关于UI部分在UI线程里操作。

  1. public abstract class ShowVideosFragmentBase extends Fragment
  2. {
  3. protected ShowVideosAdapter mAdapter;
  4. protected PullToRefreshListView mListView;
  5. @Override
  6. public void onActivityCreated(Bundle savedInstanceState)
  7. {
  8. initContentView();
  9. fetchShowVideosListViewData(...);
  10. ...
  11. }
  12. ...
  13. }

initContentView()方法初始化需要相应的View,其中比较重要的是ListView的初始化,而在fetchShowVideosListViewData(...)这个方法里面会启用worker线程拿去远端数据,在完成远端时候以后,会通知UI线程完成数据的更新显示,接下来初步的看一下代码:

  1. protected void fetchShowVideosListViewData(String formatId, int pageNum, boolean needLoading)
  2. {
  3. final ShowVideosFragmentVideosDP dp = DataProviderFactory.createProvider(getActivity(), this, ShowVideosFragmentVideosDP.class);
  4. mShowVideosDPListViewCallback = new ShowVideosDPListViewCallback(needLoading, pageNum);
  5. dp.loadVideosDP(mShowVideosDPListViewCallback, formatId, pageNum);
  6. }

简单说一下上面的代码片段,ShowVideosFragmentVideosDP这个类是完成远端数据的提供类,很显然这个类持有UI线程的回调引用,然后重点是开启worker线程获取远端数据并解析,这些都在类似于AsyncTask的doInBackground()方法中进行。

而mShowVideosDPListViewCallback这个引用就是上面所说的UI线程的回调。这里类似于AsyncTask的onPreExecute(...)、onPostExecute(...)、方法,重点来看一下onPostExecute(...)这个方法:

  1. @Override
  2. public void onPostExecute(...)
  3. {
  4. if (mAdapter == null)
  5. {
  6. mAdapter = new ShowVideosAdapter(mPrograms, true);
  7. if (mListView != null) mListView.setAdapter(mAdapter);
  8. }
  9. else
  10. {
  11. mAdapter.update(mPrograms, true);
  12. mAdapter.notifyDataSetChanged();
  13. }
  14. }
  15. }

然后我们把重点放在这个Adapter上面:

  1. protected class ShowVideosAdapter extends BaseAdapter
  2. {
  3. @Override
  4. public View getView(int position, View convertView, ViewGroup parent)
  5. {
  6. ShowViewHolder holder;
  7. if (convertView == null)
  8. {
  9. convertView = getActivity().getLayoutInflater().inflate(
  10. isListView ? R.layout.item_show_videos : R.layout.item_show_videos_grid_view, parent, false);
  11. holder = new ShowViewHolder(convertView, getImageTaskContext());
  12. convertView.setTag(holder);
  13. }
  14. else
  15. {
  16. holder = (ShowViewHolder) convertView.getTag();
  17. }
  18. final UIProgram d = getItem(position);
  19. if (DeviceTypeUtil.isPhone(getActivity()))
  20. {
  21. holder.refreshUI(d, null);
  22. }
  23. else
  24. {
  25. holder.refreshUI(d, mCurProgram);
  26. }
  27. return convertView;
  28. }
  29. }

从上面代码片段能清晰的看到View重用的实现,convertView其实只是第一次初始化的时候被inflate了一次,这个消耗时间和资源的操作在初始化完成以后就不必要再每次inflate了。参数convertView说穿来就是之前讲述的ScrapView。当ListView要求更新一行的布局时,convertView是一个非空值。因此,当convertView值非空时,你仅仅需要更新内容即可,而不需要重新一个新行的布局。

接下来还有个可以进一步优化的操作,在代码片段中已经有所体现,那就是这个所谓的ViewHolder。

Android很常见的一个操作就是在布局文件中找到一个内部的View。通常是使用一个findViewById()的View方法来实现的。这个findViewById()方法在View树中,根据一个View ID,会递归的被调用来找到其子树。虽然在静态UI布局中使用findViewById()是完全正常的。但是,在滑动时,ListView调用其Adapter中的getView()是非常频繁的。findViewById()可能会影响ListView滑动时的性能,尤其是你的行布局是很复杂的时候。

ViewHolder的模式就是减少在Adapter中getView()方法中调用findViewById()次数。实际上,View Holder是一个轻量级的内部类,在创建View之后,你可以把每行的View存储为一个Tag。然后当滑动到该行的时候通过这个Tag把这个轻量级的内部类Holder取出来,通过这种方法,只需要在初次创建布局的时候调用findViewById()。所以有必要看看这个Holder如何实现。

  1. public class ShowViewHolder
  2. {
  3. private TextView mTime;
  4. private TextView mDesc;
  5. public ShowViewHolder(View convertView, ImageTaskContext context)
  6. {
  7. this.mContext = context;
  8. this.mPoster = (ImageView) convertView.findViewById(R.id.item_show_videos_poster);
  9. this.mDirector = (ImageView) convertView.findViewById(R.id.item_show_videos_director);
  10. this.mTime = (TextView) convertView.findViewById(R.id.item_show_videos_time);
  11. this.mDesc = (TextView) convertView.findViewById(R.id.item_show_videos_des);
  12. if (convertView instanceof CheckableLinearLayout)
  13. {
  14. this.view = (CheckableLinearLayout) convertView;
  15. }
  16. }
  17. public void refreshUI(UIProgram d)
  18. {
  19. refreshUI(d, null);
  20. }
  21. public void refreshUI(UIProgram d, UIProgram cur)
  22. {
  23. if (mTime != null) mTime.setText(d.getItemTime());
  24. if (mDesc != null) mDesc.setText(d.getTitle());
  25. //TODO more
  26. }
  27. }

以上是所以ListView都会使用到的重用机制,小结起来就是做到一下两点:

  1. 减少创建View的操作的次数
  2. 只绘制和布局在屏幕上可见的子View

当然还有一些看起来比较不错,尚未用于实践的优化方法,可以稍作了解

更为激进的优化方法

核心思想:

  1. AbsListView.OnScrollListener onScrollListener = new AbsListView.OnScrollListener() {
  2. public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
  3. }
  4. public void onScrollStateChanged(AbsListView view, int scrollState) {
  5. switch (scrollState) {
  6. case AbsListView.OnScrollListener.SCROLL_STATE_FLING:// 滑动状态
  7. threadFlag = false;
  8. break;
  9. case AbsListView.OnScrollListener.SCROLL_STATE_IDLE:// 停止
  10. threadFlag = true;
  11. startThread();//开启新线程,加载数据
  12. break;
  13. case AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL:// 触摸listView
  14. threadFlag = false;
  15. break;
  16. default:
  17. // Toast.makeText(contextt, "default",
  18. // Toast.LENGTH_SHORT).show();
  19. break;
  20. }
  21. }
  22. }

简单说明一下:

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