溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

深入淺析Android中的ListView復(fù)用機(jī)制

發(fā)布時(shí)間:2020-11-23 17:07:18 來源:億速云 閱讀:177 作者:Leah 欄目:移動(dòng)開發(fā)

這篇文章給大家介紹深入淺析Android中的ListView復(fù)用機(jī)制,內(nèi)容非常詳細(xì),感興趣的小伙伴們可以參考借鑒,希望對(duì)大家能有所幫助。

1.ListView的復(fù)用機(jī)制

  ListView是我們經(jīng)常使用的一個(gè)控件,雖然說都會(huì)用,但是卻并不一定完全清楚ListView的復(fù)用機(jī)制,雖然在Android 5.0版本之后提供了RecycleView去替代ListView和GridView,提供了一種插拔式的體驗(yàn),也就是所謂的模塊化。本篇主要針對(duì)ListView的復(fù)用機(jī)制進(jìn)行探討,因此就 提RecycleView。昨天看了一下郭霖大神的ListView原理深度解析的一篇博客,因此學(xué)習(xí)了一段時(shí)間,自己也說一下自己的理解。

i.RecycleBin的基本原理

  首先需要說一下RecycleBin的基本原理,這個(gè)類也是實(shí)現(xiàn)復(fù)用的關(guān)鍵類。接著我們需要明確ActiveView的概念,ActivityView其實(shí)就是在UI屏幕上可見的視圖(onScreenView),也是與用戶進(jìn)行交互的View,那么這些View會(huì)通過RecycleBin直接存儲(chǔ)到mActivityView數(shù)組當(dāng)中,以便為了直接復(fù)用,那么當(dāng)我們滑動(dòng)ListView的時(shí)候,有些View被滑動(dòng)到屏幕之外(offScreen) View,那么這些View就成為了ScrapView,也就是廢棄的View,已經(jīng)無法與用戶進(jìn)行交互了,這樣在UI視圖改變的時(shí)候就沒有繪制這些無用視圖的必要了。他將會(huì)被RecycleBin存儲(chǔ)到mScrapView數(shù)組當(dāng)中,但是沒有被銷毀掉,目的是為了二次復(fù)用,也就是間接復(fù)用。當(dāng)新的View需要顯示的時(shí)候,先判斷mActivityView中是否存在,如果存在那么我們就可以從mActivityView數(shù)組當(dāng)中直接取出復(fù)用,也就是直接復(fù)用,否則的話從mScrapView數(shù)組當(dāng)中進(jìn)行判斷,如果存在,那么二次復(fù)用當(dāng)前的視圖,如果不存在,那么就需要inflate View了。

深入淺析Android中的ListView復(fù)用機(jī)制

這是一個(gè)總體的流程圖,復(fù)用機(jī)制就是這樣的。那么我們先來理解一下ListView第一次加載的時(shí)候都做了哪些工作,首先會(huì)執(zhí)行onLayout方法。。

/**
 * Subclasses should NOT override this method but {@link #layoutChildren()}
 * instead.
 */
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
  super.onLayout(changed, l, t, r, b);
  mInLayout = true;
  if (changed) {
    int childCount = getChildCount();
    for (int i = 0; i < childCount; i++) {
      getChildAt(i).forceLayout();
    }
    mRecycler.markChildrenDirty();
  }
  layoutChildren();
  mInLayout = false;
}

這里可以看到onLayout方法會(huì)調(diào)用layoutChildren()方法,也就是對(duì)item進(jìn)行布局的流程,layoutChildren()方法就不進(jìn)行粘貼了,代碼量過長(zhǎng)我們只需要知道,這是對(duì)ListView中的子View進(jìn)行布局的一個(gè)方式就可以了,在我們第一次加載ListView的時(shí)候,RecycleBin中的數(shù)組都沒有任何的數(shù)據(jù),因此第一次加載都需要inflate View,也就是創(chuàng)建新的View。并且第一次加載的時(shí)候是自頂向下對(duì)數(shù)據(jù)進(jìn)行加載的,因此在layoutChildren()會(huì)執(zhí)行fillFromTop()方法。fillFromTop()會(huì)執(zhí)行filleDown()方法。

/**
 * Fills the list from pos down to the end of the list view.
 *
 * @param pos The first position to put in the list
 *
 * @param nextTop The location where the top of the item associated with pos
 *    should be drawn
 *
 * @return The view that is currently selected, if it happens to be in the
 *     range that we draw.
 * 
 * @param pos:列表中的一個(gè)繪制的Item在Adapter數(shù)據(jù)源中對(duì)應(yīng)的位置
 * @param nextTop:表示當(dāng)前繪制的Item在ListView中的實(shí)際位置..
 */
private View fillDown(int pos, int nextTop) {
  View selectedView = null;
  /**
   * end用來判斷Item是否已經(jīng)將ListView填充滿
   */
  int end = (getBottom() - getTop()) - mListPadding.bottom;
  while (nextTop < end && pos < mItemCount) {
     /**
     * nextTop < end確保了我們只要將新增的子View能夠覆蓋ListView的界面就可以了
     *pos < mItemCount確保了我們新增的子View在Adapter中都有對(duì)應(yīng)的數(shù)據(jù)源item
     */
    // is this the selected item&#63;
    boolean selected = pos == mSelectedPosition;
    View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);
    /**
      *將最新child的bottom值作為下一個(gè)child的top值,存儲(chǔ)在nextTop中
      */
    nextTop = child.getBottom() + mDividerHeight;
    if (selected) {
      selectedView = child;
    }
    pos++;
  }
  return selectedView;
}
  • 在while循環(huán)中添加子View,我們先不看while循環(huán)的具體條件,先看一下循環(huán)體。在循環(huán)體中,將pos和nextTop傳遞給makeAndAddView方法,該方法返回一個(gè)View作為child,該方法會(huì)創(chuàng)建View,并把該View作為child添加到ListView的children數(shù)組中。
  • 然后執(zhí)行nextTop = child.getBottom() + mDividerHeight,child的bottom值表示的是該child的底部到ListView頂部的距離,將該child的bottom作為下一個(gè)child的top,也就是說nextTop一直保存著下一個(gè)child的top值。
  • 最后調(diào)用pos++實(shí)現(xiàn)position指針下移?,F(xiàn)在我們回過頭來看一下while循環(huán)的條件while (nextTop < end && pos < mItemCount)。
  • nextTop < end確保了我們只要將新增的子View能夠覆蓋ListView的界面就可以了,比如ListView的高度最多顯示10個(gè)子View,我們沒必要向ListView中加入11個(gè)子View。
  • pos < mItemCount確保了我們新增的子View在Adapter中都有對(duì)應(yīng)的數(shù)據(jù)源item,比如ListView的高度最多顯示10個(gè)子View,但是我們Adapter中一共才有5條數(shù)據(jù),這種情況下只能向ListView中加入5個(gè)子View,從而不能填充滿ListView的全部高度。

這里存在一個(gè)關(guān)鍵方法,也就是makeAndAddView()方法,這是ListView將Item顯示出來的核心部分,也是這個(gè)部分涉及到了ListView的復(fù)用

private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
    boolean selected) {
  View child;
  //判斷數(shù)據(jù)源是否發(fā)生了變化.
  if (!mDataChanged) {
    // Try to use an exsiting view for this position
    //如果mActivityView[]數(shù)組中存在可以直接復(fù)用的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[]數(shù)組中沒有可用的View,那么嘗試從mScrapView數(shù)組中讀取.然后重新布局.
   *如果可以從mScrapView數(shù)組中可以獲取到,那么直接返回調(diào)用mAdapter.getView(position,scrapView,this);
   *如果獲取不到那么執(zhí)行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;
}

這里可以看到如果數(shù)據(jù)源沒有變化的時(shí)候,會(huì)從mActivityView數(shù)組中判斷是否存在可以直接復(fù)用的View,可能很多讀者都不太明白直接復(fù)用到底是怎么個(gè)過程,舉個(gè)例子,比如說我們ListView一頁可以顯示10條數(shù)據(jù),那么我們?cè)谶@個(gè)時(shí)候滑動(dòng)一個(gè)Item的距離,也就是說把position = 0的Item移除屏幕,將position = 10 的Item移入屏幕,那么position = 1的Item是不是就直接能夠從mActivityView數(shù)組中拿到呢?這是可以的,我們?cè)诘谝淮渭虞dItem數(shù)據(jù)的時(shí)候,已經(jīng)將position = 0~9的Item加入到了mActivityView數(shù)組當(dāng)中,那么在第二次加載的時(shí)候,由于position = 1 的Item還是ActivityView,那么這里就可以直接從數(shù)組中獲取,然后重新布局。這里也就表示的是Item的直接復(fù)用。

  如果我們?cè)趍ActivityView數(shù)組中獲取不到position對(duì)應(yīng)的View,那么就嘗試從mScrapView廢棄View數(shù)組中嘗試去獲取,還拿剛才的例子來說當(dāng)position = 0的Item被移除屏幕的時(shí)候,首先會(huì)Detach讓View和視圖進(jìn)行分離,清空children,然后將廢棄View添加到mScrapView數(shù)組當(dāng)中,當(dāng)加載position = 10的Item時(shí),mActivityView數(shù)組肯定是沒有的,也就無法獲取到,同樣mScrapView中也是不存在postion = 10與之對(duì)應(yīng)的廢棄View,說白了就是mScrapView數(shù)組只有mScrapView[0]這一項(xiàng)數(shù)據(jù),肯定是沒有mScrapView[10]這項(xiàng)數(shù)據(jù)的,那么我們就會(huì)這樣想,肯定是從Adapter中的getView方法獲取新的數(shù)據(jù)嘍,其實(shí)并不是這樣,雖然mScrapView中雖然沒有與之對(duì)應(yīng)的廢棄View,但是會(huì)返回最后一個(gè)緩存的View傳遞給convertview。那么也就是將mScrapView[0]對(duì)應(yīng)的View返回??傮w的流程就是這樣。

深入淺析Android中的ListView復(fù)用機(jī)制

  這里我們可以看到,ListView始終只會(huì)在getView方法中inflate一頁的Item,也就是new View只會(huì)執(zhí)行一頁Item的次數(shù)。后續(xù)的Item通過直接復(fù)用和間接復(fù)用完成。

 注意一種情況:比如說還是一頁的Item,但是position = 0的Item沒有完全滑動(dòng)出UI,position = 10的Item沒有完全進(jìn)入到UI的時(shí)候,那么position = 0的Item不會(huì)被detach掉,同樣不會(huì)被加入到廢棄View數(shù)組,這時(shí)mScrapView是空的,沒有任何數(shù)據(jù),那么position = 10的Item即無法從mActivityView中直接復(fù)用View,因?yàn)槭堑谝淮渭虞d。mActivityView[10]是不存在的,同時(shí)mScrapView是空的,因此position = 10的Item只能重新生成View,也就是從getView方法中inflate。這里obtainView方法沒有具體貼出,大家可以自己進(jìn)去看看。obtainView其實(shí)就是判斷能否從廢棄View中獲取到View,獲取到了則執(zhí)行:

if (scrapView != null) { 
  child = mAdapter.getView(position, scrapView, this);  
} 

這里是可以獲取到,那么getView會(huì)傳遞scrapView。否則的話:

else { 
  child = mAdapter.getView(position, null, this); 
} 

 獲取不到就傳遞null,這樣就會(huì)執(zhí)行我們定義的Adapter中的方法。

@Override
public View getView(int position, View convertView, ViewGroup parent) {
  if(convertView == null){
    convertView = View.inflate(context, R.layout.list_item_layout, null);
  }
  return convertView;
}

至于向上滑動(dòng)會(huì)執(zhí)行其他的一些方法,也就是自底向上鋪滿ListView,同樣也會(huì)直接或者間接復(fù)用控件。理解了復(fù)用的機(jī)制才是關(guān)鍵,因此向上滑基本就不難理解了。補(bǔ)充一點(diǎn),RecycleBin中還存在一個(gè)方法,setViewTypeCount()方法。這個(gè)是針對(duì)Adapter中的getViewTypeCount()設(shè)定的。針對(duì)每一種數(shù)據(jù)類型,setViewTypeCount()會(huì)為每種數(shù)據(jù)類型開啟一個(gè)單獨(dú)的RecycleBin回收機(jī)制。這里我們只需要知道就可以了。至于在郭神博客中看到ListView會(huì)onLayout多次,這是肯定的,由于Android View加載機(jī)制問題,子控件需要根據(jù)父控件的大小要重新測(cè)量大小,經(jīng)過多次測(cè)量才能夠顯示在UI上。這是View測(cè)量多次的原因。至于ListView在多次布局的問題我就不進(jìn)行贅余了,總之無論幾次測(cè)量,ListView是不會(huì)多次執(zhí)行重復(fù)的邏輯的,也就是說數(shù)據(jù)不會(huì)有多份,只會(huì)存在一份數(shù)據(jù)。

 這里也就是ListView復(fù)用的基本原理和RecycleBin的回收機(jī)制了。代碼貼的很少,都是一些關(guān)鍵代碼,沒必要去一行一行的研究代碼,畢竟和大神還差很大的一個(gè)檔次。我們只需要知道這個(gè)執(zhí)行過程和原理就可以了。

2.ViewHolder

 最后說一說ViewHolder這個(gè)東西,很多Android學(xué)習(xí)者會(huì)把這個(gè)東西和ListView的復(fù)用機(jī)制搞混。這里ViewHolder也是在復(fù)用的時(shí)候進(jìn)行使用,但是和復(fù)用機(jī)制是沒太大關(guān)系的。

@Override
public View getView(int position, View convertView, ViewGroup parent) {
    final ViewHolder holder;
    ListViewItem itemData = items.get(position);
    if(convertView == null){
      convertView = View.inflate(context, R.layout.list_item_layout, null);
      holder = new ViewHolder();
      holder.userImg = (ImageView) convertView.findViewById(R.id.user_header_img);
      holder.userName = (TextView) convertView.findViewById(R.id.user_name);
      holder.userComment = (TextView) convertView.findViewById(R.id.user_coomment);
      convertView.setTag(holder);
    }else{
      holder = (ViewHolder) convertView.getTag();
    }
    holder.userImg.setImageResource(itemData.getUserImg());
    holder.userName.setText(itemData.getUserName());
    holder.userComment.setText(itemData.getUserComment());
    return convertView;
}

static class ViewHolder{
    ImageView userImg;
    TextView userName;
    TextView userComment;
}

在實(shí)現(xiàn)Adapter的時(shí)候,我們一般會(huì)加上ViewHolder這個(gè)東西,ViewHolder和復(fù)用機(jī)制和原理是無關(guān)的,他的主要目的是持有Item中控件的引用,從而減少findViewById()的次數(shù),因?yàn)閒indViewById()方法也是會(huì)影響效率的,因此在復(fù)用的時(shí)候他起的作用是這個(gè),減少方法執(zhí)行次數(shù)增加效率。這里做個(gè)簡(jiǎn)單的提醒,別弄混就行。

關(guān)于深入淺析Android中的ListView復(fù)用機(jī)制就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,可以學(xué)到更多知識(shí)。如果覺得文章不錯(cuò),可以把它分享出去讓更多的人看到。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI