欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Android学习笔记之ListView复用机制详解

程序员文章站 2023-12-20 14:29:46
ps:满打满算,差不多三个月没写博客了...前一阵忙的不可开交...总算是可以抽出时间研究研究其他事情了... 1.listview的复用机制   listv...

ps:满打满算,差不多三个月没写博客了...前一阵忙的不可开交...总算是可以抽出时间研究研究其他事情了...

1.listview的复用机制

  listview是我们经常使用的一个控件,虽然说都会用,但是却并不一定完全清楚listview的复用机制,虽然在android 5.0版本之后提供了recycleview去替代listview和gridview,提供了一种插拔式的体验,也就是所谓的模块化。本篇主要针对listview的复用机制进行探讨,因此就 提recycleview。昨天看了一下郭霖大神的listview原理深度解析的一篇博客,因此学习了一段时间,自己也说一下自己的理解。

i.recyclebin的基本原理

  首先需要说一下recyclebin的基本原理,这个类也是实现复用的关键类。接着我们需要明确activeview的概念,activityview其实就是在ui屏幕上可见的视图(onscreenview),也是与用户进行交互的view,那么这些view会通过recyclebin直接存储到mactivityview数组当中,以便为了直接复用,那么当我们滑动listview的时候,有些view被滑动到屏幕之外(offscreen) view,那么这些view就成为了scrapview,也就是废弃的view,已经无法与用户进行交互了,这样在ui视图改变的时候就没有绘制这些无用视图的必要了。他将会被recyclebin存储到mscrapview数组当中,但是没有被销毁掉,目的是为了二次复用,也就是间接复用。当新的view需要显示的时候,先判断mactivityview中是否存在,如果存在那么我们就可以从mactivityview数组当中直接取出复用,也就是直接复用,否则的话从mscrapview数组当中进行判断,如果存在,那么二次复用当前的视图,如果不存在,那么就需要inflate view了。

Android学习笔记之ListView复用机制详解

这是一个总体的流程图,复用机制就是这样的。那么我们先来理解一下listview第一次加载的时候都做了哪些工作,首先会执行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方法会调用layoutchildren()方法,也就是对item进行布局的流程,layoutchildren()方法就不进行粘贴了,代码量过长我们只需要知道,这是对listview中的子view进行布局的一个方式就可以了,在我们第一次加载listview的时候,recyclebin中的数组都没有任何的数据,因此第一次加载都需要inflate view,也就是创建新的view。并且第一次加载的时候是自顶向下对数据进行加载的,因此在layoutchildren()会执行fillfromtop()方法。fillfromtop()会执行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:列表中的一个绘制的item在adapter数据源中对应的位置
 * @param nexttop:表示当前绘制的item在listview中的实际位置..
 */
private view filldown(int pos, int nexttop) {
  view selectedview = null;
  /**
   * end用来判断item是否已经将listview填充满
   */
  int end = (getbottom() - gettop()) - mlistpadding.bottom;
  while (nexttop < end && pos < mitemcount) {
     /**
     * nexttop < end确保了我们只要将新增的子view能够覆盖listview的界面就可以了
     *pos < mitemcount确保了我们新增的子view在adapter中都有对应的数据源item
     */
    // is this the selected item?
    boolean selected = pos == mselectedposition;
    view child = makeandaddview(pos, nexttop, true, mlistpadding.left, selected);
    /**
      *将最新child的bottom值作为下一个child的top值,存储在nexttop中
      */
    nexttop = child.getbottom() + mdividerheight;
    if (selected) {
      selectedview = child;
    }
    pos++;
  }
  return selectedview;
}

  • 在while循环中添加子view,我们先不看while循环的具体条件,先看一下循环体。在循环体中,将pos和nexttop传递给makeandaddview方法,该方法返回一个view作为child,该方法会创建view,并把该view作为child添加到listview的children数组中。
  • 然后执行nexttop = child.getbottom() + mdividerheight,child的bottom值表示的是该child的底部到listview顶部的距离,将该child的bottom作为下一个child的top,也就是说nexttop一直保存着下一个child的top值。
  • 最后调用pos++实现position指针下移。现在我们回过头来看一下while循环的条件while (nexttop < end && pos < mitemcount)。
  • nexttop < end确保了我们只要将新增的子view能够覆盖listview的界面就可以了,比如listview的高度最多显示10个子view,我们没必要向listview中加入11个子view。
  • pos < mitemcount确保了我们新增的子view在adapter中都有对应的数据源item,比如listview的高度最多显示10个子view,但是我们adapter中一共才有5条数据,这种情况下只能向listview中加入5个子view,从而不能填充满listview的全部高度。

这里存在一个关键方法,也就是makeandaddview()方法,这是listview将item显示出来的核心部分,也是这个部分涉及到了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;
}

这里可以看到如果数据源没有变化的时候,会从mactivityview数组中判断是否存在可以直接复用的view,可能很多读者都不太明白直接复用到底是怎么个过程,举个例子,比如说我们listview一页可以显示10条数据,那么我们在这个时候滑动一个item的距离,也就是说把position = 0的item移除屏幕,将position = 10 的item移入屏幕,那么position = 1的item是不是就直接能够从mactivityview数组中拿到呢?这是可以的,我们在第一次加载item数据的时候,已经将position = 0~9的item加入到了mactivityview数组当中,那么在第二次加载的时候,由于position = 1 的item还是activityview,那么这里就可以直接从数组中获取,然后重新布局。这里也就表示的是item的直接复用。

  如果我们在mactivityview数组中获取不到position对应的view,那么就尝试从mscrapview废弃view数组中尝试去获取,还拿刚才的例子来说当position = 0的item被移除屏幕的时候,首先会detach让view和视图进行分离,清空children,然后将废弃view添加到mscrapview数组当中,当加载position = 10的item时,mactivityview数组肯定是没有的,也就无法获取到,同样mscrapview中也是不存在postion = 10与之对应的废弃view,说白了就是mscrapview数组只有mscrapview[0]这一项数据,肯定是没有mscrapview[10]这项数据的,那么我们就会这样想,肯定是从adapter中的getview方法获取新的数据喽,其实并不是这样,虽然mscrapview中虽然没有与之对应的废弃view,但是会返回最后一个缓存的view传递给convertview。那么也就是将mscrapview[0]对应的view返回。总体的流程就是这样。

Android学习笔记之ListView复用机制详解

  这里我们可以看到,listview始终只会在getview方法中inflate一页的item,也就是new view只会执行一页item的次数。后续的item通过直接复用和间接复用完成。

 注意一种情况:比如说还是一页的item,但是position = 0的item没有完全滑动出ui,position = 10的item没有完全进入到ui的时候,那么position = 0的item不会被detach掉,同样不会被加入到废弃view数组,这时mscrapview是空的,没有任何数据,那么position = 10的item即无法从mactivityview中直接复用view,因为是第一次加载。mactivityview[10]是不存在的,同时mscrapview是空的,因此position = 10的item只能重新生成view,也就是从getview方法中inflate。这里obtainview方法没有具体贴出,大家可以自己进去看看。obtainview其实就是判断能否从废弃view中获取到view,获取到了则执行:

if (scrapview != null) { 
  child = madapter.getview(position, scrapview, this);  
} 

这里是可以获取到,那么getview会传递scrapview。否则的话:

else { 
  child = madapter.getview(position, null, this); 
} 

 获取不到就传递null,这样就会执行我们定义的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;
}

至于向上滑动会执行其他的一些方法,也就是自底向上铺满listview,同样也会直接或者间接复用控件。理解了复用的机制才是关键,因此向上滑基本就不难理解了。补充一点,recyclebin中还存在一个方法,setviewtypecount()方法。这个是针对adapter中的getviewtypecount()设定的。针对每一种数据类型,setviewtypecount()会为每种数据类型开启一个单独的recyclebin回收机制。这里我们只需要知道就可以了。至于在郭神博客中看到listview会onlayout多次,这是肯定的,由于android view加载机制问题,子控件需要根据父控件的大小要重新测量大小,经过多次测量才能够显示在ui上。这是view测量多次的原因。至于listview在多次布局的问题我就不进行赘余了,总之无论几次测量,listview是不会多次执行重复的逻辑的,也就是说数据不会有多份,只会存在一份数据。

 这里也就是listview复用的基本原理和recyclebin的回收机制了。代码贴的很少,都是一些关键代码,没必要去一行一行的研究代码,毕竟和大神还差很大的一个档次。我们只需要知道这个执行过程和原理就可以了。

2.viewholder

 最后说一说viewholder这个东西,很多android学习者会把这个东西和listview的复用机制搞混。这里viewholder也是在复用的时候进行使用,但是和复用机制是没太大关系的。

@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;
}

在实现adapter的时候,我们一般会加上viewholder这个东西,viewholder和复用机制和原理是无关的,他的主要目的是持有item中控件的引用,从而减少findviewbyid()的次数,因为findviewbyid()方法也是会影响效率的,因此在复用的时候他起的作用是这个,减少方法执行次数增加效率。这里做个简单的提醒,别弄混就行。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持。

上一篇:

下一篇: