Android学习笔记之ListView复用机制

PS:满打满算,差很少三个月没写博客了...前一阵忙的不可开交...总算是能够抽出时间研究研究其余事情了...数组

 

学习内容:缓存

1.ListView的复用机制app

2.ViewHolder的概念ide

 

1.ListView的复用机制模块化

  ListView是咱们常用的一个控件,虽说都会用,可是却并不必定彻底清楚ListView的复用机制,虽然在Android 5.0版本以后提供了RecycleView去替代ListView和GridView,提供了一种插拔式的体验,也就是所谓的模块化。本篇主要针对ListView的复用机制进行探讨,所以就 提RecycleView。昨天看了一下郭霖大神的ListView原理深度解析的一篇博客,所以学习了一段时间,本身也说一下本身的理解。布局

i.RecycleBin的基本原理post

  首先须要说一下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了。学习

  这是一个整体的流程图,复用机制就是这样的。那么咱们先来理解一下ListView第一次加载的时候都作了哪些工做,首先会执行onLayout方法。。this

/**
 * 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()方法。spa

/**
 * 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返回。整体的流程就是这样。

  这里咱们能够看到,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()方法也是会影响效率的,所以在复用的时候他起的做用是这个,减小方法执行次数增长效率。这里作个简单的提醒,别弄混就行。

相关文章
相关标签/搜索