欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

记一次 ListView 性能优化过程

程序员文章站 2022-03-02 17:17:07
...
订阅

奇葩需求催生创新,记一次对 ListView 的性能优化。(当然,RecyclerView 也是一样)

对本文有任何问题,可加我的个人微信询问:kymjs123

引子

首先来看我们伟大的思想家设定的图:
记一次 ListView 性能优化过程

没什么特别的?那是因为这只是我截的手机淘宝的一个图。而我们所要实现的是:一个列表列出这个人所有的历史评价的评价情况,其中星星部分还是根据后台数据动态改变的,例如可能这件商品有描述相符、服务态度、物流服务;而下一件商品就只有描述相符、服务态度;另一件商品又可能来个帅气度、物流服务。
好吧,这的确不是什么奇葩的需求,我只是不爽这种数据传递格式。

首次实现

如果是你,你第一反应这种界面要怎么实现?ListView嵌套?擦,那岂不更是卡到死了,一不小心还造成个OOM的事儿来。我一上来直接就是在适配器里面,根据data返回来的那个评价项目数量来动态 new Layout。然后每个 Layout 就是一个 TextView 加一个 RatingBar 。

////此处省略一万行代码...

写完了,跑一下看看。嘿实现了,就是这效果。我草,等等,这怎么这么卡,我用了ViewHolder和布局复用啊。
记一次 ListView 性能优化过程
你他妈在逗我,动态new布局你跟我谈复用。

首次优化

仔细观察一下 item ,好像每个 item 都是至少2个评价选项,至多也就4个。 既然动态 new 会卡顿,那就减少动态 new 的次数呗。首先咱们先在 item 里面写三个布局项。然后在 adapter 的 getView()方法里面判断,如果评价项少于当前布局中的 layout(即 TextView + Ratingbar 组合)数量,就隐藏多出来的 layout;如果多于布局中 layout 数,再动态 new 布局。
如上方法应该是比较好的了,也是我之前在 KJBlog 的 仿微信公众号界面 中使用的方法,代码请见:KJFrame的演示项目

不够完美

记一次 ListView 性能优化过程
擦,一定是最近科技发布会看多了,一提到不完美,首先想到的是魅族那奇葩广告:“pro 5,轻易不说完美”。 尼玛,白老总,这是语病啊,你小学语文是数学老师上体育课教的吗?

吐槽完毕,说正事。

回想 ListView 实现原理,为什么 ListView 能够做到即使滚动浏览无数条数据时,程序所占用的内存竟然都不会跟着增长多少。
翻看 AbsListView 源码我们发现,其中有一个叫 RecycleBin 的内部类。 RecycleBin,中有多个 ArrayList<View> ,就是因为这些个 ArrayList<View> 才使得 ListView 的滚动能够不耗费更多内存。

ListView实现原理简介

我们在编写 ListViewAdapter 都知道要用 ViewHolder ,目的是为了布局复用。而这其中的原理很简单,就是当 ListView 滚动的时候,一个 item 滾出了屏幕,就会把这个滚出屏幕的 item 回收,而另一个 item 滚入屏幕的时候,就会去之前回收的 item 里面找,如果有回收过的 item 那就直接拿来复用了。
而这个回收与复用的过程,就跟上面的 RecycleBin 类中的那些集合类有关。
当一个 item 被移出屏幕的时候,首先会被加入到 ArrayList<View> 中,这样下次我们再使用的时候就不用再去new View (inflate()方法本质也是new对象),而是可以直接从 List 中去取一个 View 来复用了。
具体的细节,大家可以看郭霖的这篇博客ListView工作原理完全解析

再次优化

结合刚刚讲的,我们自己定义一个集合类用来复用View

public class RecyclerList extends LinkedList<View> {

    private Context cxt;
    private int layoutId;

    public RecyclerList(Context cxt, int id) {
        this.cxt = cxt;
        this.layoutId = id;
    }

    public View get() {
        View view;
        if (size() > 0) {
            view = getFirst();
            removeFirst();
        } else {
            view = View.inflate(cxt, layoutId, null);
        }
        return view;
    }
}

这里使用Linked是考虑到我实际项目中会频繁的添加删除多个item,想想 LinkedListArrayList 各自的优势~~

于是项目中的适配器就可以这么写了(只挑有用的部分写出来了)

private RecyclerList recycler;

public void function(){
    LinearLayout root = vh.getView(R.id.root);
    int count = root.getChildCount();
    int size = item.getList().size();
    if (count >= size) {
        for (int i = 0; i < count - size; i++) {
            recycler.add(root.getChildAt(1));
            root.removeViewAt(1);
        }
    } else {
        for (int i = 0; i < size - count; i++) {
            View view = recycler.get();
            root.addView(view, 1);
        }
    }
}

最后的优化

呵呵哒,在我写这篇文章的时候,界面又特么变了。
记一次 ListView 性能优化过程
算了,就当是自己总结了。
结合 初次优化 的效果,我们预先在item里面写几个layout,不过呢,这次我们不是直接去写,而是用 ViewStub ,ViewStub相当于一个占位符,可以在控件 onMeasure()onLayout() 的时候不去考虑内部的实现,而是在控件需要显示的时候才去通过代码调用加载到内存中。
利用这一特性,我们可以在 getView() 中判断,这个item是否需要显示,如果需要layout,则添加进来,而当不需要的时候,移除到 RecyclerList 中;下次再进来,如果需要layout,则从 RecyclerList 中拿。

由于代码我没有实现,这里就只简单的写一下 ViewStub 的相关代码。
首先是item的xml布局中使用ViewStub:

...
<ViewStub   
        android:id="@+id/view_stub"  
        android:layout="@layout/layout"  
        android:layout_width="match_parent"  
        android:layout_height="wrap_content" />  
...

之后是 getView()方法中的java代码

ViewStub viewStub = (ViewStub) findViewById(R.id.view_stub);  
    if (viewStub != null) {  
        View layout = viewStub.inflate();  

        // android:layout="@layout/layout"
        // layout就是xml中这句引入的View  
    }  

需要注意的是 findView 拿到 ViewStub 的时候一定要判空,因为如果我们上次已经调用了 viewStub.inflate() ,则 findview 就会返回 null 了。w