Android深入学习listview的优化

1、ListView是怎么工作的

ListView的设计考虑了可拓展性和性能,从本质上来说,这意味着:

(1)尽量少的inflate操作

(2)只绘制或展示屏幕上可见(或者即将可见)的子控件

 

第(1)条的理由很简单:对XML布局文件的inflate操作开销大,尽管为了更高效的解析,布局文件被编译成了二进制形式,然而inflate()方法会调用rlnflate()从根视图递归地遍历一整棵由XML块(包装编译后的XML文件)组成的树,并且实例化每个子视图。 ListView通过重复利用被称为"SrapesViews"的非可见视图来缓解这个问题,这意味着开发者只需更新被重复利用的视图的内容,而不用对每一行做inflate操作。

ListView为了实现第(2)条,当列表滚动的时候,ListView通过视图回收器从回收池中取出视图添加到屏幕中,并且把滑出屏幕范围的视图缓存回回收器中,这样,即使你有成百上千行的数据,ListView只需在内存中保持一定(少数)的行视图对象(包括显示中的和回收器里面的),它会以不同的方式把每一行填充到列表中,从上方往下或者从下方往上,这取决于列表的如何滚动的。

2、视图回收利用

每次ListView需要在屏幕上增加新的一行,它便会通过适配器调用getView()方法,正如你所知道的那样,getView()方法有三个参数:行位置position;行视图对象convertView;父控件ViewGroup。参数convertView就是之前提到的“ScrapView”,当回收器中有缓存时会返回一个非空值,因此,如果convertView非空的时候,你只需要更新它的内容,而不是inflate一个新的行布局视图,加入回收利用机制的getView()的代码如下:

public View getView(int position, View convertView, ViewGroup parent) {
    if (convertView == null) {
        convertView = mInflater.inflate(R.layout.your_layout, null);
    }
 
    TextView text = (TextView) convertView.findViewById(R.id.text);
    text.setText("Position " + position);
 
    return convertView;
}

3、ViewHolder模式

从一个被inflate出来的布局中寻找内部视图是Android开发中的常见操作之一,通常通过findViewByld()视图方法实现,该方法会递归遍历视图树寻找给定ID对应的子视图。在静态UI布局上使用findViewById()很好,但是随着列表的滚动,ListView频繁地调用适配器的getView()方法.而findViewById()对滚动的性能有着较大的影响,特别是当你的行布局比较复杂的时候。ViewHolder模式的核心是减少适配器getView()方法中调用findViewById()方法的次数,实际上,HoleView是保存着行视图的内部子视图的直接引用的轻量级内部类,在对行视图进行inflate操作之后,可以把它作为一个标记(TAG)保存在行视图(convertView)中,这样你只需要在创建视图布局的时候调用findViewById()方法。下面是加入了ViewHolder模式之后的代码示例:

public View getView(int position, View convertView, ViewGroup parent) {
    ViewHolder holder;
 
    if (convertView == null) {
        convertView = mInflater.inflate(R.layout.your_layout, null);
 
        holder = new ViewHolder();
        holder.text = (TextView) convertView.findViewById(R.id.text);
 
        convertView.setTag(holder);
    } else {
        holder = convertView.getTag();
    }
 
    holder.text.setText("Position " + position);
 
    return convertView;
}
 
private static class ViewHolder {
    public TextView text;
}

4、异步加载

Android应用通常会在ListView中展示复杂的内容,例如图片。在适配器getView()方法中使用drawable资源是一种很好的做法,因为Android在内部对它们做了缓存。但是有时候你可能希望展示动态内容(来自本地磁盘或者网络),例如短文或者资料图片等。这种情况下,你应该不希望直接在getView()方法中加载它们,因为任何情况下都不应该让IO操作阻塞UI线程,这样做会影响列表滚动的流畅性。你希望的是在独立的线程中异步处理每一行的IO或者给CPU带来很大负担的操作,这里的技巧在于这样处理的同时遵循ListView的回收利用行为。例如,如果你在适配器getView()方法中执行异步任务加载图片,在异步任务完成之前,该图片对应的视图可能会被回收并且被另一个位置利用,于是乎你需要一种机制检验在异步任务完成的时候视图是否已经被回收。一种简单的检验方法是再视图上附加信息标记哪一行与它关联,然后你可以检验当异步任务结束时与视图关联的目标行是否仍然相同。有很多种方法可以实现这种机制,下面提供一种简化的实现:

public View getView(int position, View convertView,
        ViewGroup parent) {
    ViewHolder holder;
 
    ...
 
    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;
}

5、交互意识(Interaction awareness)

对每一行异步加载重量级资源是提高ListView性能的重要步骤,但是如果你在滚动时盲目地为每一个getView()操作开启异步任务,将会浪费大量的资源,因为由于行视图经常被回收导致很多结果都被丢弃。我们需要为ListView适配器添加交互意识,使得它再某些操作之后不会对每一行都执行异步任务,例如在ListView上的甩动动作(也就是快速滑动,这时候对每一行执行异步任务是没有意义的),当滑动停止,或者将要停止时,就是为每一行实际展示重量级内容的时候。由于相关代码比较长,我就不展示出来了,Romain Guy 经典的 Shelves app是一个很好的例子,其中展示了当GridView停止滚动的时候,就会触发书本封面的加载。还可以平衡内存缓存与交互意识之间的使用,在滚动的时候展示缓存内容。

 

ListView怎么和ScrollView兼容?
我们知道,有些时候我们需要在ListView外层嵌套一层ScrollView,代码如下:

    <ScrollView
        android:id="@+id/scrollview"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <ListView
            android:id="@+id/listview"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"></ListView>
    </ScrollView>
只要稍微有点经验的人都知道这是会出现什么问题,没错,就是“Listview不能显示正常的条目,只显示一条或二条”,这是怎么回事呢?这是因为:由于listView在scrollView中无法正确计算它的大小, 故只显示一行。 
当目前为止,我知道的针对这一问题的解决办法有:

1. 方法一:重写ListView, 覆盖onMeasure()方法
activity_list_view_scroll_view_test.xml:
<merge xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.art.demo.ListViewScrollViewTestActivity">
    <ScrollView
        android:id="@+id/scrollview"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <com.art.demo.WrapperListView
            android:id="@+id/listview"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"/>
    </ScrollView>
</merge>

WrapperListView.java:
public class WrapperListView extends ListView {
    public WrapperListView(Context context) {
        super(context);
    }
    public WrapperListView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }
    public WrapperListView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }
    public WrapperListView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }
    /**
     * 重写该方法,达到使ListView适应ScrollView的效果
     */
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
        super.onMeasure(widthMeasureSpec, expandSpec);
    }
}

ListViewScrollViewTestActivity.java:
public class ListViewScrollViewTestActivity extends AppCompatActivity {

    private ScrollView scrollView;
    private WrapperListView listView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_list_view_scroll_view_test);
        scrollView = (ScrollView) findViewById(R.id.scrollView);
        listView = (WrapperListView) findViewById(R.id.listview);
        initListVeiw();
    }

    private void initListVeiw() {
        List<String> list = new ArrayList<>();
        for (int i = 0; i < 20; i++) {
            list.add("第 " + i + " 条");
        }
        listView.setAdapter(new ArrayAdapter<String>(this,
                android.R.layout.simple_list_item_1, list));
    }
}

2. 方法二:动态设置listview的高度,不需要重写ListView
只需要在setAdapter之后调用如下方法即可:

public void setListViewHeightBasedOnChildren(ListView listView) {
        // 获取ListView对应的Adapter
        ListAdapter listAdapter = listView.getAdapter();
        if (listAdapter == null) {
            return;
        }
        int totalHeight = 0;
        for (int i = 0, len = listAdapter.getCount(); i < len; i++) {
            // listAdapter.getCount()返回数据项的数目
            View listItem = listAdapter.getView(i, null, listView);
            // 计算子项View 的宽高
            listItem.measure(0, 0);
            // 统计所有子项的总高度
            totalHeight += listItem.getMeasuredHeight();
        }
        ViewGroup.LayoutParams params = listView.getLayoutParams();
        params.height = totalHeight + (listView.getDividerHeight() * (listAdapter.getCount() - 1));
        // listView.getDividerHeight()获取子项间分隔符占用的高度
        // params.height最后得到整个ListView完整显示需要的高度
        listView.setLayoutParams(params);
    }

另外,这时,这时最好给ListView之外嵌套一层LinearLayout,不然有时候这种方法会失效,如下:

<merge xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.art.demo.ListViewScrollViewTestActivity">
    <ScrollView
        android:id="@+id/scrollview"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">

            <ListView
                android:id="@+id/listview"
                android:layout_width="fill_parent"
                android:layout_height="match_parent"
                android:background="#FFF4F4F4"
                android:dividerHeight="0.0dip"
                android:fadingEdge="vertical" />
        </LinearLayout>
    </ScrollView>
</merge>

3. 方法三:在xml文件中,直接将Listview的高度写死
可以确定的是:这种方式可以改变ListView的高度,但是,还有一个严重的问题就是listview的数据是可变动的,除非你能正确的写出listview的高度,否则这种方式就是个鸡肋。 
如下:

<ScrollView
        android:id="@+id/scrollview"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent">
            <ListView
                android:id="@+id/listview"
                android:layout_width="fill_parent"
                android:layout_height="300dip"
                android:background="#FFF4F4F4"
                android:dividerHeight="0.0dip"
                android:fadingEdge="vertical" />
        </LinearLayout>
    </ScrollView>

4. 方法四:
某些情况下,其实我们可以完全避免ScrollView嵌套Listview,比如使用listview的addHeader() 函数来实现预期效果或者利用布局的特性达到预期效果,当然,具体怎么用,只有在开发中慢慢琢磨,慢慢总结了.

至此,关于“ListView怎么和ScrollView兼容”这个问题就算是回答完了,如果有不明白的地方可以问我,同样,那里有错误也欢迎大家指出,真的不胜感激。

接下来要说的就是!!!!!

listview怎么优化?
关于Listview的优化,只要面试过的人,我相信都对这个题很熟悉,不管有没有人问过你这个题,我想你自己也一定准备过,否则,嘿嘿!!!!!而且网上也一搜一大把这里就简单提几个主要的: 
1)、convertView复用,对convetView进行判空,当convertView不为空时重复使用,为空则初始化,从而减少了很多不必要的View的创建 
2)定义一个ViewHolder,封装Listview Item条目中所有的组件,将convetView的tag设置为ViewHolder,不为空时通过ViewHolder的属性获取对应组件即可 
3)、当ListView加载数据量较大时可以采用分页加载和图片异步加载(关于Listview分页加载和图片异步加载思路请看接下来的文章内容)

下面就是关于Listview的一些相关拓展

1. 打开套有 ListVew的 ScrollView的页面布局 默认 起始位置不是最顶部?
解决办法有两种: 
方法一:把套在里面的ListVew 不让获取焦点即可。listview.setFocusable(false);注意:在xml布局里面设置android:focusable=“false”不生效 
方法二:myScrollView.smoothScrollTo(0,0);

2. 上拉加载和下拉刷新怎么实现?
实现OnScrollListener 接口重写onScrollStateChanged 和onScroll方法, 
使用onscroll方法实现”滑动“后处理检查是否还有新的记录,如果有,调用 addFooterView,添加记录到adapter, adapter调notifyDataSetChanged 更新数据;如果没有记录了,把自定义的mFooterView去掉。使用onScrollStateChanged可以检测是否滚到最后一行且停止滚动然后执行加载

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章