ListView优化总结

  1. 1. 利用好 ConvertView
  2. 2. 利用好 ViewType
  3. 3. Item View 的 Layout 层次结构简单
  4. 4. 减少View绘制时间
    1. 4.1. 原因
    2. 4.2. 减短时间
    3. 4.3. 控件更新前先对比数据
    4. 4.4. Instagram 预渲染文本
  5. 5. ViewHolder
  6. 6. 尽量自定义布局
  7. 7. 尽量能保证 Adapter 的 hasStableIds() 返回 true
  8. 8. 每个 Item 不能太高
  9. 9. getView() 中要做尽量少的事情
    1. 9.1. 停下来再加载
    2. 9.2. 异步加载
  10. 10. 监听器设置一个就OK了
  11. 11. ListView 中元素避免半透明
  12. 12. 尽量开启硬件加速
  13. 13. AnimationCache
  14. 14. ScrollingCache
  15. 15. SmoothScrollbar
  16. 16. 参考

ListView 的优化总结,包括利用好 ConvertView、利用好 ViewType、Layout 层次结构、ViewHolder、使用自定义布局、保证 Adapter 的 hasStableIds() 返回 true、Item 不能太高、getView() 中要做尽量少的事情、ListView 中元素避免半透明、尽量开启硬件加速、 AnimationCache、 ScrollingCache 和 SmoothScrollbar。

利用好 ConvertView

尽可能的少去执行 Layout 的 Inflate 。即使 Layout 文件已经被高效的解析程序转换为了二进制代码,Layout 的 Inflate 仍然是巨消耗资源的。Infalte 操作依旧需要彻底包含整个 XML 代码树,而且还要实例化相应的View。

在初始显示的时候,每次显示一个 item 都调用一次 getview() ,但是每次调用的时候 covertView 为 null ,当显示完了之后。如果屏幕移动了之后,并且导致有些 item 的 View 移到屏幕外面,此时如果还有新的 item 需要产生,则这些 item 显示时调用的 getView() 方法,此时 getView() 中的 convertview 参数就不是 null ,而是那些移出屏幕的 View ,我们所要做的就是将需要显示的 item 填充到这些回收的 View 中去,最后注意 convertview 为 null 的不仅仅是初始显示的那些 item ,还要可能跟 ViewType 有关。

利用好 ViewType

如果ListView中要显示多种布局文件(类似聊天界面那种),可以使用 ViewType 。其中涉及到两个方法,分别是:

1
2
3
4
5
6
7
8
9
@Override
public int getItemViewType(int position) {
return super.getItemViewType(position);
}

@Override
public int getViewTypeCount() {
return super.getViewTypeCount();
}

第一个方法是告诉 ListView 当前 position 对应哪种 ViewType ,第二个方法是告诉 ListView 一共有多少中 ViewType

如果要实现聊天界面那种,这是一种可行方案,同样可行方案是使用同一个布局,但是在 getView() 中去判断是发送消息类型还是接收消息类型,然后去调用 View.setVisibility(View.VISIBLE)View.setVisibility(View.GONE)但是这样的话会导致 Layout 的层次结构复杂

同样,如果是使用 ViewType 的话,与上面那种相比可能会使 ListView 中的子 View 变多。

如果想动态向 ListView 中添加 HeaderView 或者 FooterView ,可以结合这两个方法。

Item View 的 Layout 层次结构简单

善用自定义 View,自定义 View 可以有效的减小 Layout 的层级。

减少View绘制时间

原因

Android系统每隔16.7ms发出一个渲染信号,通知ui线程进行界面的渲染。当一个ListView被添加到布局时 getView() 方法将会被回调。在16.7毫秒时间单内,ListView 中 item 将调用 getView() 方法,并且需要显示多少个 item 就会调用多少次 getView() 方法。在大多数情况下,由于其他绘图行为的存在,例如 measuredrawgetView() 实际分配到执行时间远低于16ms。一旦ListView包含复杂控件时,在16毫秒内不能完成渲染,用户只能看到上一帧的结果,这时就发生了掉帧。

减短时间

比如把 item 中控件高度尽量固定,比如固定值或者 match_parent 。慎用 layout_weight 类似属性,以便缩短 View 的 measure 时间。

控件更新前先对比数据

设置 View (如 TextView.setText() )之前先对比数据是否有改变。一般来说,比较两个数据的代价远小于 View 的重绘的代价

Instagram 预渲染文本

ins 中一条消息也很长,而且起初 measuredraw 时间都超过了16ms。ins 使用了 text.Layout 并且缓存了 text.Layout 实例。同时自定义View实现自己去控制 text.Layout 的绘制,这样有很多好处,比如可以使用 StaticLayout 而不是 DynamicLayout、避免从 SpannableStringBuilder 转换 String 的过程(前提是看文字中是否有links)、避免 TextView 中不需要的逻辑。比如监控文本信息的改变等。

ViewHolder

使用 ViewHolder 的原因是 findViewById() 方法耗时较大,如果控件个数过多,会严重影响性能,而使用ViewHolder主要是为了可以省去这个时间。注意 viewHolder 里面 item 方法重绘:如 invalidate , setVisibility , requestLayout 后,会调用 adapter 的 getView() 方法。

尽量自定义布局

自定义布局有个好处就是可以省略 ViewHolder。说出来可能你不会信, ViewHolder 首先会占用 setTag() ,其次每次取出后都需要转换一下类的类型。如果是自定义布局的话,findViewById() 这个过程可以在构造函数中进行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class ItemView extends RelativeLayout {
private TextView mTitleTextView;
private TextView mDescriptionTextView;
private ImageView mImageView;

public ItemView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
LayoutInflater.from(context).inflate(R.layout.item_view_children, this, true);
setupChildren();
}

public static ItemView inflate(ViewGroup parent) {
ItemView itemView = (ItemView)LayoutInflater.from(parent.getContext())
.inflate(R.layout.item_view, parent, false);
return itemView;
}

private void setupChildren() {
mTitleTextView = (TextView) findViewById(R.id.item_titleTextView);
mDescriptionTextView = (TextView) findViewById(R.id.item_descriptionTextView);
mImageView = (ImageView) findViewById(R.id.item_imageView);
}

public void setItem(Item item) {
mTitleTextView.setText(item.getTitle());
mDescriptionTextView.setText(item.getDescription());
// set up image URL blablabla...
}
}

这样操作的话,在 Adapter 的 getView() 中的操作就少了 ViewHolder

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ItemAdapter extends ArrayAdapter<Item> {

public ItemAdapter(Context c, List<Item> items) {
super(c, 0, items);
}

@Override
public View getView(int position, View convertView, ViewGroup parent) {
ItemView itemView = (ItemView)convertView;
if (null == itemView)
itemView = ItemView.inflate(parent);
itemView.setItem(getItem(position));
return itemView;
}

}

尽量能保证 Adapter 的 hasStableIds() 返回 true

Indicates whether the item ids are stable across changes to the underlying data.

这样在 notifyDataSetChanged() 的时候,如果 id 不变,ListView 将不会重新绘制这个 View,达到优化的目的;

这个在 RecyclerView 的时候得到了改善,多出了 notifyItemMoved() 等方法。毕竟 notifyDataSetChanged() 太暴力了。

每个 Item 不能太高

特别是不要超过屏幕的高度,最好在3/4以下,以便View的回收。这部分可以查看 facebook 的做法。

getView() 中要做尽量少的事情

为了保证 ListView 滑动的流畅性,不要有耗时的操作。很多时候,Android 应用在 ListView 每行中显示一些多媒体内容,比如图片等。在 Adapter 中的 getView() 使用应用内置的图片资源还是不会出什么问题的,因为可以存储在 Android 的高速缓存中。但当想显示来自本地磁盘或网络的内容时,例如缩略图,简历图片等,在这种情况下,可能不希望直接在 Adapter 中的 getView() 加载它们,因为IO进程会阻塞UI线程。如果这样做的话, ListView 就看起来非常卡顿。

停下来再加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
listView.setOnScrollListener(new AbsListView.OnScrollListener() {
@Override
public void onScrollStateChanged(AbsListView view, int scrollState) {
switch (scrollState){
case SCROLL_STATE_TOUCH_SCROLL:
case SCROLL_STATE_FLING:
break;
case SCROLL_STATE_IDLE:
int start = listView.getFirstVisiblePosition();
int end = listView.getLastVisiblePosition();
if(end >= listView.getCount()){
end = listView.getCount() - 1;
}
//展示start-end之间的图片 blablabla......
break;
}
}

@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {

}
});

异步加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public View getView(int position, View convertView,
ViewGroup parent)
{

ViewHolder holder;
///blablabla......
holder.position = position;
new ThumbnailTask(position, holder)
.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, null);
return convertView;
}

private static class ThumbnailTask extends AsyncTask {
private int mPosition;
private ViewHolder mHolder;

public ThumbnailTask(int position, ViewHolder holder) {
mPosition = position;
mHolder = holder;
}

@Override
protected Cursor doInBackground(Void... arg0) {
// Download bitmap here
}

@Override
protected void onPostExecute(Bitmap bitmap) {
if (mHolder.position == mPosition) {
mHolder.thumbnail.setImageBitmap(bitmap);
}
}
}

private static class ViewHolder {
public ImageView thumbnail;
public int position;
}

注意,在滑动屏幕时,如果你一味的在每一个 getView() 调用里面都去启动一个异步的操作,造成的结果就是你会浪费大量资源。因为行被频繁回收,造成大部分返回的结果会被丢弃

监听器设置一个就OK了

不要在 getView() 中不断的去设置监听器,因为 View 是复用的,当 convertView 不为 null 时是已经设置了监听器的。同时当 convertView 为 null 时也不要去 new 监听器,假设一个屏幕上有10个 item ,那么就要 new 出10个监听器来。最佳的做法是这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private View.OnClickListener mOnClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
//blablabla......
}
};

@Override
public View getView(int position, View convertView, ViewGroup parent) {
//blablabla......
if (convertView == null) {
//blablabla......
convertView.setOnClickListener(mOnClickListener);
}else{
//blablabla......
}
//blablabla......
}

这样做就不需要 new 出那么多监听器来了。

有人问,如果这样写,所有 button 只能通过 id 区分逻辑,无法传入每个 item 的数据,我们可以将数据通过 View 的 tag 带进来:

1
2
3
4
5
6
@Override
public View getView(int position, View convertView, ViewGroup parent) {
//blablabla......
convertView.setTag(key, getItem(position));
//blablabla......
}

然后在 mOnClickListener 中通过 v.getTag(key) 将数据取出。

ListView 中元素避免半透明

半透明绘制需要大量乘法计算,在滑动时不停重绘会造成大量的计算,在比较差的机子上会比较卡。在设计上能不半透明就不不半透明。实在要弄的话我个人是用个比较偷懒的方法,是在滑动的时候把半透明设置成不透明,滑动完再重新设置成半透明

尽量开启硬件加速

硬件加速提升巨大,避免使用一些不支持的函数导致含泪关闭某个地方的硬件加速。

AnimationCache

在Layout布局中添加 android:animationCache="false" ,或者调用方法 public void setAnimationCacheEnabled(boolean enabled)

Enables or disables the children’s drawing cache during a layout animation.
By default, the drawing cache is enabled but this will prevent nested layout animations from working. To nest animations, you must disable the cache.

在执行一个 Layout 动画时开启或关闭子控件的绘制缓存。默认情况下,绘制缓存是开启的,但是这将阻止嵌套 Layout 动画的正常执行。对于嵌套动画,你必须禁用这个缓存。

ScrollingCache

在Layout布局中添加 android:scrollingCache="false" ,或者调用方法 public void setScrollingCacheEnabled(boolean enabled)

Enables or disables the children’s drawing cache during a scroll.
By default, the drawing cache is enabled but this will use more memory.

When the scrolling cache is enabled, the caches are kept after the first scrolling. You can manually clear the cache by calling android.view.ViewGroup.setChildrenDrawingCacheEnabled(boolean).

在滚动期间执行子控件的绘制缓存。默认情况下,绘制缓存是开启的,但是这会导致使用更多的内存。当滑动缓存开启时,一开始滑动就保存了缓存,可以人工情况缓存。

SmoothScrollbar

item 大小不一致的时候设置一下 public void setSmoothScrollbarEnabled(boolean enabled)可以让ListView滑动更流畅。

When smooth scrollbar is enabled, the position and size of the scrollbar thumb is computed based on the number of visible pixels in the visible items. This however assumes that all list items have the same height. If you use a list in which items have different heights, the scrollbar will change appearance as the user scrolls through the list. To avoid this issue, you need to disable this property.

When smooth scrollbar is disabled, the position and size of the scrollbar thumb is based solely on the number of items in the adapter and the position of the visible items inside the adapter. This provides a stable scrollbar as the user navigates through a list of items with varying heights.

参考