天天看點

ListView原理及優化

1.工作原理

     ListView 針對每個item,要求 adapter “傳回一個視圖” (getView),也就是說ListView在開始繪制的時候,系統首先調用getCount()函數,根據他的傳回值得到ListView的長度,然後根據這個長度,調用getView()一行一行的繪制ListView的每一項。如果你的getCount()傳回值是0的話,清單一行都不會顯示,如果傳回1,就隻顯示一行。傳回幾則顯示幾行。如果我們有幾千幾萬甚至更多的item要顯示怎麼辦?為每個Item建立一個新的View?不會,實際上Android早已經緩存了這些視圖,大家可以看下下面這個截圖來了解下,這個圖是解釋ListView工作原理的最經典的圖了大家可以收藏下,不懂的時候拿來看看,加深了解,其實Android中有個叫做Recycler的構件,順帶列舉下與Recycler相關的已經由Google做過N多優化過的東東比如:AbsListView.RecyclerListener、ViewDebug.RecyclerTraceType等等,要了解的朋友自己查下,不難了解,下圖是ListView加載資料的工作原理(原理圖看不清楚的點選後看大圖):

ListView原理及優化

    1).如果你有幾千幾萬甚至更多的選項(item)時,其中隻有可見的項目存在記憶體(記憶體記憶體哦,說的優化就是說在記憶體中的優化!!!)中,其他的在Recycler中

    2).ListView先請求一個type1視圖(getView)然後請求其他可見的項目。convertView在getView中是空(null)的

    3).當item1滾出螢幕,并且一個新的項目從螢幕低端上來時,ListView再請求一個type1視圖。convertView此時不是空值了,它的值是item1。你隻需設定新的資料然後傳回convertView,不必重新建立一個視圖

public class MultipleItemsList extends ListActivity {
 
    private MyCustomAdapter mAdapter;
 
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mAdapter = new MyCustomAdapter();
        for (int i = 0; i < 50; i++) {
            mAdapter.addItem("item " + i);
        }
        setListAdapter(mAdapter);
    }
 
    private class MyCustomAdapter extends BaseAdapter {
 
        private ArrayList mData = new ArrayList();
        private LayoutInflater mInflater;
 
        public MyCustomAdapter() {
            mInflater = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        }
 
        public void addItem(final String item) {
            mData.add(item);
            notifyDataSetChanged();
        }
 
        @Override
        public int getCount() {
            return mData.size();
        }
 
        @Override
        public String getItem(int position) {
            return mData.get(position);
        }
 
        @Override
        public long getItemId(int position) {
            return position;
        }
 
        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            System.out.println("getView " + position + " " + convertView);
            ViewHolder holder = null;
            if (convertView == null) {
                convertView = mInflater.inflate(R.layout.item1, null);
                holder = new ViewHolder();
                holder.textView = (TextView)convertView.findViewById(R.id.text);
                convertView.setTag(holder);
            } else {
                holder = (ViewHolder)convertView.getTag();
            }
            holder.textView.setText(mData.get(position));
            return convertView;
        }
 
    }
 
    public static class ViewHolder {
        public TextView textView;
    }
}
           

檢視日志:

ListView原理及優化
ListView原理及優化

getView 被調用 9 次 ,convertView 對于所有的可見項目是空值.

然後稍微向下滾動List,直到item10出現:

ListView原理及優化

convertView仍然是空值,因為recycler中沒有視圖(item1的邊緣仍然可見,在頂端)再滾動清單,繼續滾動:

ListView原理及優化

convertView不是空值了!item1離開螢幕到Recycler中去了,然後item11被建立,再滾動下:

ListView原理及優化

此時的convertView非空了,在item11離開螢幕之後,它的視圖(…0f8)作為convertView容納item12了

2. ListView優化

1)複用convertView

Android系統本身為我們考慮了ListView的優化問題,在複寫的Adapter的類中,比較重要的兩個方法是getCount()和getView()。界面上有多少個條顯示,就會調用多少次的getView()方法;是以如果在每次調用的時候,如果不進行優化,每次都會使用View.inflate(….)的方法,都要将xml檔案解析,并顯示到界面上,這是非常消耗資源的:因為有新的内容産生就會有舊的内容銷毀,是以,可以複用舊的内容。

優化:

在getView()方法中,系統就為我們提供了一個複用view的曆史緩存對象convertView,當顯示第一屏的時候,每一個item都會新建立一個view對象,這些view都是可以被複用的;如果每次顯示一個view都要建立一個,是非常耗費記憶體的;是以為了節約記憶體,可以在convertView不為null的時候,對其進行複用

2) ViewHold緩存item條目的引用

findViewById()這個方法是比較耗性能的操作,因為這個方法要找到指定的布局檔案,進行不斷地解析每個節點:從最頂端的節點進行一層一層的解析查詢,找到後在一層一層的傳回,如果在左邊沒找到,就會接着解析右邊,并進行相應的查詢,直到找到位置(如圖)。是以可以對findViewById進行優化處理,需要注意的是:

》》》》特點:xml檔案被解析的時候,隻要被建立出來了,其孩子的id就不會改變了。根據這個特點,可以将孩子id存入到指定的集合中,每次就可以直接取出集合中對應的元素就可以了。

優化:

在建立view對象的時候,減少布局檔案轉化成view對象的次數;即在建立view對象的時候,把所有孩子全部找到,并把孩子的引用給存起來

①定義存儲控件引用的類ViewHolder

這裡的ViewHolder類需要不需要定義成static,根據實際情況而定,如果item不是很多的話,可以使用,這樣在初始化的時候,隻加載一次,可以稍微得到一些優化

不過,如果item過多的話,建議不要使用。因為static是Java中的一個關鍵字,當用它來修飾成員變量時,那麼該變量就屬于該類,而不是該類的執行個體。是以用static修飾的變量,它的生命周期是很長的,如果用它來引用一些資源耗費過多的執行個體(比如Context的情況最多),這時就要盡量避免使用了。

  class ViewHolder{

              //定義item中相應的控件

        }

②建立自定義的類:ViewHolder holder = null;

③将子view添加到holder中:

在建立新的listView的時候,建立新的ViewHolder,把所有孩子全部找到,并把孩子的引用給存起來

通過view.setTag(holder)将引用設定到view中

通過holder,将孩子view設定到此holder中,進而減少以後查詢的次數

④在複用listView中的條目的時候,通過view.getTag(),将view對象轉化為holder,即轉化成相應的引用,友善在下次使用的時候存入集合。

通過view.getTag(holder)擷取引用(需要強轉)

3)分批分頁加載資料

需求:ListView有一萬條資料,如何顯示;如果将十萬條資料加載到記憶體,很消耗記憶體

解決辦法:

優化查詢的資料:先擷取幾條資料顯示到界面上

進行分批處理---優化了使用者體驗

進行分頁處理---優化了記憶體空間

說明:

一般資料都是從資料庫中擷取的,實作分批(分頁)加載資料,就需要在對應的DAO中有相應的分批(分頁)擷取資料的方法,如findPartDatas ()

1、準備資料:

  在dao中添加分批加載資料的方法:findPartDatas ()

  在适配資料的時候,先加載第一批的資料,需要加載第二批的時候,設定監聽檢測何時加載第二批

2、設定ListView的滾動監聽器:setOnScrollListener(new OnScrollListener{….})

①、在監聽器中有兩個方法:滾動狀态發生變化的方法(onScrollStateChanged)和listView被滾動時調用的方法(onScroll)

②、在滾動狀态發生改變的方法中,有三種狀态:

手指按下移動的狀态:               SCROLL_STATE_TOUCH_SCROLL: // 觸摸滑動

慣性滾動(滑翔(flgin)狀态):    SCROLL_STATE_FLING: // 滑翔

靜止狀态:                       SCROLL_STATE_IDLE: // 靜止

3、對不同的狀态進行處理:

分批加載資料,隻關心靜止狀态:關心最後一個可見的條目,如果最後一個可見條目就是資料擴充卡(集合)裡的最後一個,此時可加載更多的資料。在每次加載的時候,計算出滾動的數量,當滾動的數量大于等于總數量的時候,可以提示使用者無更多資料了。

4)滑動的時候不加載圖檔

如果你的ListView中需要顯示從網絡上下載下傳的圖檔的話,我們不要在ListView滑動的時候加載圖檔,那樣會使ListView變得卡頓,是以我們需要再監聽器裡面監聽ListView的狀态,如果滑動的時候,停止加載圖檔,如果沒有滑動,則開始加載圖檔

listView.setOnScrollListener(new OnScrollListener() {

            @Override
            public void onScrollStateChanged(AbsListView listView, int scrollState) {
                    //停止加載圖檔 
                    if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_FLING) {
                            imageLoader.stopProcessingQueue();
                    } else {
                    //開始加載圖檔
                            imageLoader.startProcessingQueue();
                    }
            }

            @Override
            public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
                    // TODO Auto-generated method stub

            }
    });
           

5)将ListView的scrollingCache和animateCache設定為false

scrollingCache: scrollingCache本質上是drawing cache,你可以讓一個View将他自己的drawing儲存在cache中(儲存為一個bitmap),這樣下次再顯示View的時候就不用重畫了,而是從cache中取出。預設情況下drawing cahce是禁用的,因為它太耗記憶體了,但是它确實比重畫來的更加平滑。而在ListView中,scrollingCache是預設開啟的,我們可以手動将它關閉。

animateCache: ListView預設開啟了animateCache,這會消耗大量的記憶體,是以會頻繁調用GC,我們可以手動将它關閉掉

<ListView
        android:id="@android:id/list"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:divider="@color/list_background_color"
        android:dividerHeight="0dp"
        android:listSelector="#00000000"
        android:scrollingCache="false"
        android:animationCache="false"
        android:smoothScrollbar="true"
        android:visibility="gone" />
           

6)盡量減少布局深度

7)

 盡量避免在BaseAdapter中使用static 來定義全局靜态變量,我以為這個沒影響 ,這個影響很大,static是Java中的一個關鍵字,當用它來修飾成員變量時,那麼該變量就屬于該類,而不是該類的執行個體。是以用static修飾的變量,它的生命周期是很長的,如果用它來引用一些資源耗費過多的執行個體(比如Context的情況最多),這時就要盡量避免使用了

8)如果為了滿足需求下必須使用Context的話:Context盡量使用Application Context,因為Application的Context的生命周期比較長,引用它不會出現記憶體洩露的問題

9)盡量避免在ListView擴充卡中使用線程,因為線程産生記憶體洩露的主要原因在于線程生命周期的不可控制