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可以檢測是否滾到最後一行且停止滾動然後執行加載

 

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