Android ListView 重難點解析

一、基本使用


 我們先來看看 ListView 的 Adapter 一般是怎麼寫的:

public class FruitAdapter extends ArrayAdapter<Fruit> {

    private int resourceId;

    public FruitAdapter(Context context, int textViewResourceId, List<Fruit> objects) {
        super(context, textViewResourceId, objects);
        resourceId = textViewResourceId;
    }

    /*  由系統調用,獲取一個View對象,作爲ListView的條目,屏幕上能顯示多少個條目,getView方法就會被調用多少次
     *  position:代表該條目在整個ListView中所處的位置,從0開始
     */
    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        //重寫適配器的getItem()方法
        Fruit fruit = getItem(position);
        View view;
        ViewHolder viewHolder;
        if (convertView == null) { //若沒有緩存佈局,則加載
            //首先獲取佈局填充器,然後使用佈局填充器填充佈局文件
            view = LayoutInflater.from(getContext()).inflate(resourceId, null);
            viewHolder = new ViewHolder();
            //存儲子項佈局中子控件對象
            viewHolder.fruitImage = (ImageView) view.findViewById(R.id.fruit_image);
            viewHolder.fruitName = (TextView) view.findViewById(R.id.fruit_name);
            // 將內部類對象存儲到View對象中
            view.setTag(viewHolder);
        } else { //若有緩存佈局,則直接用緩存(利用的是緩存的佈局,利用的不是緩存佈局中的數據)
            view = convertView;
            viewHolder = (ViewHolder) view.getTag();
        }
        viewHolder.fruitImage.setImageResource(fruit.getImageId());
        viewHolder.fruitName.setText(fruit.getName());
        return view;
    }
    
    //內部類,用於存儲ListView子項佈局中的控件對象
    class ViewHolder {
        ImageView fruitImage;
        TextView fruitName;        
    }
}

如上所示,在實現 Adapter 的時候,我們一般會加上 ViewHolder 這個東西。ViewHolder 和複用機制的原理是無關的,他的主要目的是持有 Item 中控件的引用,從而減少 findViewById() 的次數,因爲 findViewById() 方法也是會影響效率的,因此在複用的時候他起的作用是這個,減少方法執行次數增加效率。

點擊事件代碼:

listView.setOnItemClickListener(new OnItemClickListener() {  
    @Override                                                
    public void onItemClick(AdapterView<?> parent, View view, int position, long id) {                         
        Fruit fruit = fruitList.get(position);               
        // 加入邏輯代碼 
    }                                                        
});  

ListView 對象使用 setAdapter(ListAdapter la) 方法給 ListView 控件設置適配器。

 

二、ListView 的緩存機制


 ListView 的緩存主要是在 ListView 的內部類 RecycleBin 中,它包含了兩級緩存(Active View、Scrap View):

Active View、Scrap View 分別是什麼呢,我們繼續看下面這張圖:

可以看到,Active View 其實就是在屏幕上可見的視圖,也是與用戶進行交互的 View,那麼這些 View 會通過 RecycleBin 直接存儲到 mActiveView 數組當中,以便爲了直接複用

那麼當我們滑動 ListView 的時候,有些 View 被滑動到屏幕之外 ,那麼這些 View 就成爲了 Scrap View,也就是廢棄的 View,已經無法與用戶進行交互了,這樣在 UI 視圖改變的時候就沒有繪製這些無用視圖的必要了。它將會被 RecycleBin 存儲到 mScrapView 數組當中,目的是爲了二次複用,也就是間接複用

當新的 View 需要顯示的時候,先判斷 mActiveView 中是否存在,如果存在那麼我們就可以從 mActiveView 數組當中直接取出複用,也就是直接複用,否則的話從 mScrapView 數組當中進行判斷,如果存在則間接複用當前的視圖然後調用 getView 方法,如果不存在,那麼就需要創建新的 View 了。

它們都是在 RecycleBin 類中的:

class RecycleBin {
    private RecyclerListener mRecyclerListener;
    //第一個可見的View存儲的位置
    private int mFirstActivePosition;
    //可見的View數組
    private View[] mActiveViews = new View[0];
    //不可見的的View數組,是一個集合數組,每一種type的item都有一個集合來緩存
    private ArrayList<View>[] mScrapViews;
    //View的Type的數量
    private int mViewTypeCount;
    //viewType爲1的集合或者說mScrapViews的第一個元素
    private ArrayList<View> mCurrentScrap;
}

可以看到 mScrapViews 是一個 ArrayList 的數組,不知道大家有沒有這樣的一個疑惑,負責緩存的 mScrapViews 數組的容量是誰來確定的?當我們需要 ListView 支持多類型複用時,往往要覆蓋這兩個方法:

  • getViewTypeCount() 就決定了 mScrapViews 數組的長度
  • getItemViewType() 就決定了相同類型的 View 投放到哪個座標下。這句話的意思就是相同類型的 View 需要返回相同的值,並且它的值必須是從 0 開始依次遞增的。

當我們使用同類型加載數據的ListView時,這兩個方法我們不必去理會。

ListView 將 Item 顯示出來的核心部分也就是 makeAndAddView() 方法,這個部分涉及到了 ListView 的複用:

private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
        boolean selected) {
    View child;
    //判斷數據源是否發生了變化.
    if (!mDataChanged) {
        // Try to use an exsiting view for this position
        //如果mActivityView[]數組中存在可以直接複用的View,那麼直接獲取,然後重新佈局.
        child = mRecycler.getActiveView(position);
        if (child != null) {
            // Found it -- we're using an existing child
            // This just needs to be positioned
            setupChild(child, position, y, flow, childrenLeft, selected, true);
            return child;
        }
    }
    // Make a new view for this position, or convert an unused view if possible
    /**
      *如果mActivityView[]數組中沒有可用的View,那麼嘗試從mScrapView數組中讀取.然後重新佈局.
      *如果可以從mScrapView數組中可以獲取到,那麼直接返回調用mAdapter.getView(position,scrapView,this);
      *如果獲取不到那麼執行mAdapter.getView(position,null,this)方法.
      */
    child = obtainView(position, mIsScrap);
    // This needs to be positioned and measured
    setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
    return child;
}

這裏可以看到如果數據源沒有變化的時候,會從 mActiveView 數組中判斷是否存在可以直接複用的 View,可能很多讀者都不太明白直接複用到底是怎麼個過程,舉個例子,比如說我們 ListView 一頁可以顯示 10 條數據,那麼我們在這個時候滑動一個 Item 的距離,也就是說把 position = 0 的 Item 移除屏幕,將 position = 10 的 Item 移入屏幕,那麼 position = 1 的 Item 是不是就直接能夠從 mActiveView 數組中拿到呢?這是可以的,我們在第一次加載 Item 數據的時候,已經將 position = 0~9 的 Item 加入到了 mActiveView 數組當中,那麼在第二次加載的時候,由於 position = 1 的 Item 還是 ActiveView,那麼這裏就可以直接從數組中獲取。這裏也就表示的是 Item 的直接複用。

接下來跟進去看看 obtainView 方法:

View obtainView(int position, boolean[] isScrap) {  
    isScrap[0] = false;  
    View scrapView; 
    // 根據position調用getItemViewType(position)方法可以獲得View在緩存池的位置
    scrapView = mRecycler.getScrapView(position);  
    View child;  
    if (scrapView != null) {  
        // 如果不爲null,我們就可以利用convertView進行復用操作
        child = mAdapter.getView(position, scrapView, this);  
        if (child != scrapView) {
            // 如果返回的View和我們從緩存池中拿出的View不同,則把它重新存進去
            mRecycler.addScrapView(scrapView);  
            if (mCacheColorHint != 0) {  
                child.setDrawingCacheBackgroundColor(mCacheColorHint);  
            }  
        } else {  
            isScrap[0] = true;  
            dispatchFinishTemporaryDetach(child);  
        }  
    } else {  
        // 當緩存池中沒有時,傳遞convertView爲null
        child = mAdapter.getView(position, null, this);  
        if (mCacheColorHint != 0) {  
            child.setDrawingCacheBackgroundColor(mCacheColorHint);  
        }  
    }  
    return child;  
} 

這也是 ListView 中最核心的方法,用來獲取 scrap view 緩存,我將難點都用註釋標註了應該很好理解。

 

三、總結


與 RecyclerView 緩存 RecyclerView.ViewHolder 不同,ListView 緩存的是 View。

  是否需要回調createView 是否需要回調bindView 生命週期 備註
mActiveViews onLayout函數週期內 用於屏幕內ItemView快速重用。容量爲一屏能展示的ItemView的個數
mScrapViews 與mAdapter一致,當mAdapter被更換時它被清空 容量不限

 

Android ListView工作原理完全解析,帶你從源碼的角度徹底理解

ListView緩存原理剖析

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