浅析 ListView 优化策略

前记

ListView,作为曾经 Android 开发中最重要的控件,风光无两之后颓势渐显,如今正被大红大紫的 RecyclerView 慢慢取代,而且 Google 官方也建议我们使用后者代替前者。但即便如此,ListView 中依然有一些优秀的设计思想值得我们好好研读学习,也为后续我们更好地理解 RecyclerView 的强大打好基础。ListView 源码有三千多行,RecyclerView 有一万多行。这篇文章并没有打算从源码入手,只是在分析优化时引用一些源码中必要的代码及分析。对源码有兴趣的同学可以参考郭神的这篇博客《Android ListView工作原理完全解析,带你从源码的角度彻底理解》

OK,下面进入正题~~

ListView 继承结构

ListView 中有部分代码逻辑是继承父类的,所以有必要了解一下它的继承结构。官方文档中 ListView 的类继承结构如下:

http://7xsosy.com1.z0.glb.clouddn.com/Screen%20Shot%202016-08-12%20at%2015.40.50.png

需要注意的是:与 ListView 处于同一级别的是 CardView,对于 ListView 的优化策略对于 CardView 也同样适用,但是现在用到 CardView 的地方并不是很多。ListView 还有一个子类 ExpandableListView,从名字上可以看出来是 ListView 的一个增强版,但现在有了 RecyclerViewListView 的豪华增强版),估计也不怎么用了吧,有兴趣的同学可以自己研究一下。

ListView 优化策略

ListView 中用到的设计模式主要有下面三种:

  • 单例设计模式:在 LayoutInflater 类中,获取的系统核心服务以单例形式存在。
  • 适配器模式ListView负责加载视图,而 Adapter负责绑定并展示数据。
  • 观察者模式:当数据源发生变化时,会通知 ListView 更新并重新绘制 ItemView

下面介绍几个具体的优化策略:

使用 convertView 参数

我们在自定义 Adapter 时,都会重写 getView()方法,该方法签名如下:

@override
public view getView(int position, View convertView, ViewGroup parent)

其中,第三个参数parent表示该 Item View 的父视图,对于 ListView 来说这个 parent 就代表 ListView 本身。第二个参数 convertView很重要,严格来讲它并不是必须的,但使用它会提升 ListView 的运行效率。说它是优化策略似乎并不十分准确,因为这个参数本来就写好在getView()方法中了,不用的话,只能说代码写的不够优雅。但考虑到性能,我们现在也都用上这个参数了,不用的话反而感觉有些另类了。

完整getView()方法如下:

public View getView(int positioin, View convertView, ViewGroup parent) {
    ViewHolder holder = null;
    if (convertView == null) {
        convertView = LayoutInflater.from(getContext()).inflate(R.layout.listview_item, null);
        holder = new ViewHolder();
        holder.textview = (TextView) convertView.findViewById(R.id.tv_text);
        convertView.setTag(holder);
    } else {
        holder = (ViewHolder) convertView.getTag();
    }
    holder.textView.setText(mData.get(position)); // 数据绑定
    return convertView; // 返回视图
}

使用 convertView 的原因就是为了避免 LayoutInflater 类的多次使用,因为类 LayoutInflater加载布局比较耗资源

这里 convertView 的复用涉及到 RecycleBin 机制。如下图:

http://7xsosy.com1.z0.glb.clouddn.com/Screen%20Shot%202016-08-12%20at%2018.19.08.png

总结来说,RecycleBin中有两个重要的View数组,分别是mActiveViewsmScrapViews。这两个数组中所存储的View都是用来复用的,只不过mActiveViews中存储的是OnScreenView,这些View很有可能被直接复用;而mScrapViews中存储的是OffScreenView,这些View主要是用来间接复用的。对 RecycleBin 机制感兴趣的同学可以参考这篇文章《List 中的 RecycleBin 机制》

使用 ViewHolder

使用 ViewHolder 非常简单,如下:

public class ViewHolder {
    public TextView textView;
    // ItemView 中其他控件……
}

convertView 的使用是为了把 View 外层复用避免重复加载布局,而 ViewHolder 则是把 View 的内层 View 内容进行缓存,避免 findViewById()方法的多次调用,同样可以提升性能。

异步加载图片

优化列表的卡顿现象提高滑动的流畅度的出发点就是不要在主线程中做太耗时的操作。而我们都知道加载图片是一个比较耗时的操作,所以尽可能采用异步加载的方式,同时尽量用小图或缩略图。示例代码如下:

// Using an AsyncTask to load the slow images in a background thread
new AsyncTask<ViewHolder, Void, Bitmap>() {
    private ViewHolder v;

    @Override
    protected Bitmap doInBackground(ViewHolder... params) {
        v = params[0];
    return mFakeImageLoader.getImage();
    }

    @Override
    protected void onPostExecute(Bitmap result) {
        super.onPostExecute(result);
        if (v.position == position) {
        // If this item hasn't been recycled already, hide the
        // progress and set and show the image
            v.progress.setVisibility(View.GONE);
            v.icon.setVisibility(View.VISIBLE);
            v.icon.setImageBitmap(result);
        }
    }
}.execute(holder);

控制异步加载频率

有一种情况是用户有意无意的频繁滑动 ListView,这会造成短时间内主线程要进行大量的 UI 更新操作,肯定会造成一定程度上的卡顿。解决问题的思路是判断用户的操作手势,当快速滑动时不加载里面的图片。即使加载了速度很快时也看不清而且会卡,所以索性不加载了等停下来再加载也不迟。

具体实现时,可以给 ListView 或者 GridView 设置 setOnScrollListener,并在 OnScrollListeneronScrollStateChanged() 方法中判断列表是否处于滑动状态,如果是就停止加载图片,代码如下:

public void onScrollStateChanged(AbsListView view, int scrollState) {
    if (scrollState == OnScrollListener.SCROLL_STATE_IDLE) {
        mIsGridViewIdle = true;
        mImageAdapter.notifyDataSetChanged();
    } else {
        mIsGridViewIdle = false;
    }
}

然后在 getView()方法中,仅当列表静止时才能加载图片,如下:

if (mIsGridViewIdle && mCanGetBitmapFromNetWork) {
    imageView.setTag(uri);
    mImageLoader.bindBitmap(uri, imageView, mImageWidth, mImageWidth);
}

不太常用的几个技巧

  • 对数据显示进行分页加载
  • Item布局层级越少越好,可通过 Android Studio 中的 ADM 工具查看,SDK 中的 HierarchyViewer 也可以。
  • 开启硬件加速,通过设置:android:hardwareAccelerated=”true”。

后记

ListView 的分析暂时告一段落,以后就全面拥抱 RecyclerView 了。