【Android】让HeaderView也参与回收机制 自我感觉是优雅的为 RecyclerView 添加 Header

首先给大家推荐一下我老师大神的人工智能教学网站。教学不仅零基础,通俗易懂,而且非常风趣幽默,还时不时有内涵黄段子!点这里可以跳转到网站

本文站在巨人的肩膀上 自我感觉又进了一步而成。

基于翔神的大作基础之上写的一个为RecyclerView添加HeaderView FooterView 的另一种解决方案, 

翔神链接文首镇楼:http://blog.csdn.net/lmj623565791/article/details/51854533 

上次翔神发表这篇文章时,我就提了个问题:说headerView和FooterView都是强引用在Adapter中的,这样即使他所属的ViewHolder被回收复用(后实践发现,就算设置了HeaderView的ViewHolder不缓存,但是始终有一个HeaderView的ViewHolder在被强引用),但是View本身的实例还是在被强引用,内存空间也无法释放的。 这样做虽然速度没任何问题,(甚至还有提升,但是HeaderView过大内存空间就会吃紧了吧) 因为我司项目大多HeaderView又臭又长,所以我想了好久 改写了一下,换了种思路,给RecyclerView提供数据和布局,并且可以让开发者动态配置headerView在RecyclerViewPool里的缓存数,将UI的创建 和 数据的绑定分开来做,都交由Adapter维护。

先给大家看一下我司app的某个界面设计稿:这种页面在我们的APP里有10+个

是的你没看错,底部还是个不断加载更多的列表~,对于这种又臭又长的HeaderView,我一想到它在内存里不能释放,我就浑身难受。

墙裂建议大家先阅读翔神文章后 再立刻阅读此文,威力翻倍。这样对本文使用到的一些吊炸天的东西就不会陌生了,例如通用的CommonAdapter和ViewHolder。

敲黑板,如果只是伸手党,建议直接看 【2 使用方法】,并直接到文末下载链接里的工程,拷贝recyclerview包下的几个文件即可使用。

工程里已经参考解决,HeaderView适配GridLayoutManager 和StaggeredGridLayoutManager。

========================================================================

【1 引言】

众所周知,RecyclerView已经是主流,ListView已经成为过去式,而两者之间有些许的不同,其中比较重要的一点就是ListView自带addHeaderView,addFooterView方法,而RecyclerView并没有提供。So,我们开发者要自己想办法实现这个功能。

市面上大多为RecyclerView添加HeaderView的方案,都是在使用RecyclerView的类中(Activity Fragment)里构建一个View,并绑定好数据,然后通过XXXAdapter提供的addHeaderView方法,将这个View set进Adapter里。

Adapter内部使用ArrayList、或者翔神使用的是SparseArray存储这个View,并为HeaderView FooterView分配不同的itemViewType,然后Adapter在onCreateViewHolder和onBindViewHolder方法里,根据ViewType的不同来判断这是HeaderView 还是普通item。

这种方法目前为止我只发现一个弊端(也是本文改进的地方),就是这个HeaderView由于在Adapter里是被ArrayList、SparseArray强引用的,就算其所属的RecyclerView.ViewHolder在RecyclerViewPool的缓存池里 被设置缓存数量为0,被回收了(后来经过实测,发现HeaderViewHolder数量多后,始终有一个ViewHolder在被引用 没有被释放,其余的被成功释放),但是这个View会因为被ArrayList等强引用着,依然停留在内存中。所以该HeaderView并没有被回收。而想一想普通的item都只有数据和layoutId传递给Adapter,并没有View的实例。

一般情况下 这并没有任何问题,因为普通项目的HeaderView也不大,但是若HeaderView过于庞大,(就像我司的项目,动辄HeaderView就三+个屏幕长度,三屏之后才是普通的item),在这个页面已经往下滑了很多距离,浏览了很多内容,HeaderView早已不可见,此时按照RecyclerView的思路,这个庞大的HeaderView所属的VIewHolder应该已经进入了RecyclerViewPool的缓存池中,如果设置该种viewType的缓存数量为0,即不缓存,ok,那么RecyclerView做了它该做的事,不再缓存这个HeaderView寄身的VIewHolder了,在GC垃圾回收触发后,虽然该种type的ViewHolder被回收了,可惜上文提到,此时HeaderView被强引用住,被回收的只是其所属的那个ViewHolder,这个庞大的VIew所占的内存空间依然没有被释放。

其实我们仔细想一想,RecyclerView Adapter里是不保存View对象的,它保存的只是数据和layout,而我们也应该遵循此原则 为其添加HeaderView(FooterView)。

(题外话,和ListView相比,RecyclerView更是进一步的 将 UI的创建 和数据的绑定 分成了两步,(oncreateViewHolder,onBindViewHolder))

敲黑板,本文就参考翔神的装饰者模式,为RecyclerView 添加 HeaderView(FooterView),

并且将HeaderView的UI创建,和数据绑定强制分开,提供配置每种headerView的缓存数量的方法,令HeaderView实例在Adapter中不再被强引用,让HeaderView和普通的ItemView没有两样~。

先上预览动图:

第二张图是为了测试headerViewHolder是否真的被回收特意选用4个ImageView组成的HeaderView看效果。

========================================================================

【2 使用方法】

//HeaderView使用方法小窥: 以下为Rv添加两个HeaderView
mHeaderAdapter = new HeaderRecyclerAndFooterWrapperAdapter(mAdapter) {
    @Override
    protected void onBindHeaderHolder(ViewHolder holder, int headerPos, int layoutId, Object o) {
        switch (layoutId) {
            case R.layout.item_header_1:
                TestHeader1 header1 = (TestHeader1) o;
                holder.setText(R.id.tv, header1.getText());
                break;
            case R.layout.item_header_2:
                TestHeader2 header2 = (TestHeader2) o;
                holder.setText(R.id.tv1, header2.getTxt1());
                holder.setText(R.id.tv2, header2.getTxt2());
                break;
            default:
                break;
        }
    }
};
mHeaderAdapter.addHeaderView(R.layout.item_header_1,new TestHeader1("第一个HeaderView"));
mHeaderAdapter.addHeaderView(R.layout.item_header_2,new TestHeader2("第二个","HeaderView"));
mRv.setAdapter(mHeaderAdapter);

以上每个HeaderView在缓存池RecyclerViewPool的数量都是默认的5个,

使用如下方法:

mHeaderAdapter.addHeaderView(R.layout.item_header_4, new TestHeader4(pics),0);

将该种类型的headerView的缓存数量配置为0个。

粗略这么一看,我擦 什么辣鸡,比翔神那个真是差十万八千里,人家只要4行代码就加一个HeaderView,而且还不用实现父类Adapter的方法,你这还要switch case 看起来就一坨好麻烦的样子,走了走了。

客官留步留步,如果客官有这种想法,先冷静一下,里听我港。

这个写法猛地看起来是略复杂了一些,但是它强制的让我们将UI的创建和数据的绑定分开了,我们重写的onBindHeaderHolder()方法,就是数据的绑定过程, 试想一下,基本上每个带HeaderView的页面都有下拉刷新功能,如果你使用传统方法添加HeaderView,那么你必须要持有HeaderView的引用才能在数据刷新时改变头部数据,而且那些烦人的set方法一样是要写一遍,你可能需要将 写在Activity(Fragment)里的 创建HeaderView时的set数据方法抽成一个函数,再调用一遍。所以工作量是一点没减少的。

而且重要的是,使用这种方法,如果将缓存数量设置为0,HeaderView在移出屏幕后,触发GC事件时,是可以被回收滴。文末给实验证明。

所以我们这种做法,你的工作量也是一点没增加滴!反而还是方便滴!优雅滴!

(躲开丢过来的鸡蛋)废话不多说,用法已经看到,下面看我们是怎么实现的。 如果伸手党看到这里觉得已经够了,那么就可以去文末直接下载源码copy使用了,里面使用的几个类版权大多归翔神所有。

========================================================================

【三,实现】

直接贴出核心代码:













public abstract class HeaderRecyclerAndFooterWrapperAdapter2 extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    private class HeaderBean {
        private final int DEFAULT_HEADER_VIEW_CACHE_SIZE = 5;//默认是5 和RecyclerViewPool的默认值一样
        private int layoutId;//viewType当做layoutId
        private Object data;//该viewType(LayoutId)对应的数据
        private int cacheSize;//该种viewType的HeaderView 在RecyclerViewPool的缓存池内的缓存数量

        public HeaderBean(int layoutId, Object data, int cacheSize) {
            this.layoutId = layoutId;
            this.data = data;
            this.cacheSize = cacheSize;
        }

        public HeaderBean(int layoutId, Object data) {
            this.layoutId = layoutId;
            this.data = data;
            this.cacheSize = DEFAULT_HEADER_VIEW_CACHE_SIZE;
        }

        public int getLayoutId() {
            return layoutId;
        }

        public void setLayoutId(int layoutId) {
            this.layoutId = layoutId;
        }

        public Object getData() {
            return data;
        }

        public void setData(Object data) {
            this.data = data;
        }

        public int getCacheSize() {
            return cacheSize;
        }

        public void setCacheSize(int cacheSize) {
            this.cacheSize = cacheSize;
        }

    }
//按照add顺序存放HeaderView的bean,bean包括layoutId,数据Data,和缓存数量cacheSize。
// 在createViewHOlder里根据layoutId创建UI,在onbindViewHOlder里依据这个data渲染UI,
// 在onAttachedToRecyclerView 为每种layoutId(同时也是viewType)的headerView设置缓存数量
private ArrayList<HeaderBean> mHeaderDatas = new ArrayList<HeaderBean>();
@Override
public int getItemViewType(int position) {
    if (isHeaderViewPos(position)) {
        return mHeaderDatas.get(position).getLayoutId();
//HeaderView的layoutId就是viewType

} return super.getItemViewType(position – getHeaderViewCount());}

@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {

    if (mHeaderDatas != null && !mHeaderDatas.isEmpty()) {//不为空,说明有headerview
        for (HeaderBean HeaderBean : mHeaderDatas) {
            if (HeaderBean.getLayoutId() == viewType) {//匹配上了说明是headerView
                return ViewHolderHeader3.get(parent.getContext(), null, parent, viewType, -1);
            }
        }
    }
protected abstract void onBindHeaderHolder(ViewHolderHeader3 holder, int headerPos, int layoutId, Object o);//多回传一个layoutId出去,用于判断是第几个headerview

@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
    if (isHeaderViewPos(position)) {
        onBindHeaderHolder((ViewHolderHeader3) holder, position, mHeaderDatas.get(position).getLayoutId(), mHeaderDatas.get(position).getData());
        return;
/**
 * 添加HeaderView
 *
 * @param layoutId headerView 的LayoutId
 * @param data     headerView 的data(可能多种不同类型的header 只能用Object了)
 */
public void addHeaderView(int layoutId, Object data) {
    mHeaderDatas.add(new HeaderBean(layoutId, data));
}

/**
 * 添加HeaderView
 *
 * @param layoutId  headerView 的LayoutId
 * @param data      headerView 的data(可能多种不同类型的header 只能用Object了)
 * @param cacheSize 该种headerView在缓存池中的缓存个数
 */
public void addHeaderView(int layoutId, Object data, int cacheSize) {
    mHeaderDatas.add(new HeaderBean(layoutId, data, cacheSize));
}

定义一个HeaderBean,存放HeaderView 的布局id,需要绑定的数据data,以及该种HeaderView在RecyclerViewPool缓存池中的缓存数量。

将layoutId作为viewType。

定义个ArrayList<HeaderBean>,按照add进来的顺序 存放HeaderBean。

首先需要重写的就是getItemViewType()方法,在这个方法里根据postion判断是否在headerView的范围内,如果是返回layoutId作为viewType。

然后重写onCreateViewHolder()方法,如果headerData不为空,说明有HeaderView,那么遍历headerData,比较当前要创建的这个ViewHolder的type和headerData里的type,如果一样,说明则是要创建一个HeaderViewHolder。

在onBindViewHolder()方法中,先根据postion判断是否是HeaderView,如果是,那么我们便根据postion从headerDatas里取出相应的layoutId和data,回调一个 abstract  的 onBindHeaderHolder()的方法,将这些参数都传入,交由子类去自由处理。 子类在这个方法里 完成数据的绑定即可。

addHeaderView()方法比较简单,就是new 一个HeaderBean 然后add进HeaderDatas里即可。

下面,重点来了,我们将在onAttachedToRecyclerView()方法里,设置headerView的在RecyclerViewPool里的缓存数量。

@Override
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
    mInnerAdapter.onAttachedToRecyclerView(recyclerView);
    //设置HeaderView的ViewHolder的缓存数量
    if (null != mHeaderDatas && !mHeaderDatas.isEmpty()) {
        for (HeaderBean HeaderBean : mHeaderDatas) {
            recyclerView.getRecycledViewPool().setMaxRecycledViews(HeaderBean.getLayoutId(), HeaderBean.getCacheSize());
        }
    }

判断如果headerdatas不为空,则遍历其中HeaderBean,并调用recyclerView.getRecycledViewPool()获取RecyclerViewPool对象,再调用它的setMaxRecycledViews()方法,传入viewType 和相应viewType的缓存数量。

====================================================================================================

【4 RecyclerViewPool 源码浅析】

关于RecyclerViewPool,可能还有很多人不是很了解(我也是最近才开始了解),这里简单说下我的理解,后续要深入研究一下RecyclerView相关的知识。

RecyclerViewPool是一个为RecyclerView缓存ViewHolder的缓存池,它默认会为每种viewType缓存5个ViewHolder。

源码为证:

public static class RecycledViewPool {
    private SparseArray<ArrayList<ViewHolder>> mScrap =
            new SparseArray<ArrayList<ViewHolder>>();
    private SparseIntArray mMaxScrap = new SparseIntArray();
    private int mAttachCount = 0;

    private static final int DEFAULT_MAX_SCRAP = 5;

这里的DEFAULT_MAX_SCRAP 就是每种viewType的默认缓存数量,本文所定义的默认数量和它保持一致,为5.

mScrap 以viewType为key,value是一个ArrayList,里面存放的就是该种ViewType的ViewHolder啦。

mMaxScrap 也以viewType为key,value就是每种viewType的最大缓存数量。

mAttachCount 是用来计数的,每当RecyclerViewPool与一个adapter绑定、解绑,回调onAdapterChanged()方法,在其中便会+1 -1,当它为0时,就会清空这个RecyclerViewPool缓存池里的所有ViewHolder。这便是缓存池中ViewHolder被清空的时刻了。

源码如下:

public void clear() {
    mScrap.clear();
}
void attach(Adapter adapter) {
    mAttachCount++;
}

void detach() {
    mAttachCount--;
}


/**
 * Detaches the old adapter and attaches the new one.
 * <p>
* RecycledViewPool will clear its cache if it has only one adapter attached and the new
 * adapter uses a different ViewHolder than the oldAdapter.
 *
 * @param oldAdapter The previous adapter instance. Will be detached.
 * @param newAdapter The new adapter instance. Will be attached.
 * @param compatibleWithPrevious True if both oldAdapter and newAdapter are using the same
 *                               ViewHolder and view types.
 */
void onAdapterChanged(Adapter oldAdapter, Adapter newAdapter,
        boolean compatibleWithPrevious) {
    if (oldAdapter != null) {
        detach();
    }
    if (!compatibleWithPrevious && mAttachCount == 0) {
        clear();
    }
    if (newAdapter != null) {
        attach(newAdapter);
    }
}

我们在onAttachedtoRecyclerVie()方法里调用的recyclerView.getRecycledViewPool() 方法的源码如下:很简单 就是通过mRecycler获取RecyclerViewPool对象。

/**
 * Retrieve this RecyclerView's {@link RecycledViewPool}. This method will never return null;
 * if no pool is set for this view a new one will be created. See
 * {@link #setRecycledViewPool(RecycledViewPool) setRecycledViewPool} for more information.
 *
 * @return The pool used to store recycled item views for reuse.
 * @see #setRecycledViewPool(RecycledViewPool)
 */
public RecycledViewPool getRecycledViewPool() {
    return mRecycler.getRecycledViewPool();
}

紧接着调用的 设置缓存数量的方法源码如下:

public void setMaxRecycledViews(int viewType, int max) {
    mMaxScrap.put(viewType, max);
    final ArrayList<ViewHolder> scrapHeap = mScrap.get(viewType);
    if (scrapHeap != null) {
        while (scrapHeap.size() > max) {
            scrapHeap.remove(scrapHeap.size() - 1);
        }
    }
}

这里除了将mMaxScrap里的value改变以外,还从mScrap里取出了该种ViewType的缓存队列list,并且判断size,如果超过最大值,会remove掉相应ViewHolder。

RecyclerViewPool类还有几个其他的方法:

获取缓存ViewHolder

public ViewHolder getRecycledView(int viewType) {
    final ArrayList<ViewHolder> scrapHeap = mScrap.get(viewType);
    if (scrapHeap != null && !scrapHeap.isEmpty()) {
        final int index = scrapHeap.size() - 1;
        final ViewHolder scrap = scrapHeap.get(index);
        scrapHeap.remove(index);
        return scrap;
    }
    return null;
}

该方法通过viewType获取缓存的ViewHolder,可以看出,是从缓存list的尾部逐个取出ViewHolder的。

存入ViewHolder

public void putRecycledView(ViewHolder scrap) {
    final int viewType = scrap.getItemViewType();
    final ArrayList scrapHeap = getScrapHeapForType(viewType);
    if (mMaxScrap.get(viewType) <= scrapHeap.size()) {
        return;
    }
    if (DEBUG && scrapHeap.contains(scrap)) {
        throw new IllegalArgumentException("this scrap item already exists");
    }
    scrap.resetInternal();
    scrapHeap.add(scrap);
}

存入时,通过viewType获取对应ViewType的最大缓存数量 以及 对应viewType的缓存list,如果list.size大于等于最大值,则不缓存。 所以我们设置为0永远>=0,即没有缓存。(废话) 如果缓存list未满,则将该ViewHolder add进去,并且调用

void resetInternal() {
    mFlags = 0;
    mPosition = NO_POSITION;
    mOldPosition = NO_POSITION;
    mItemId = NO_ID;
    mPreLayoutPosition = NO_POSITION;
    mIsRecyclableCount = 0;
    mShadowedHolder = null;
    mShadowingHolder = null;
    clearPayload();
    mWasImportantForAccessibilityBeforeHidden = ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_AUTO;
}

为这个ViewHolder恢复一些初始值,可以看到,都是一些flag itemId,postion等。

RecyclerViewPool缓存池一共缓存的ViewHolder数量:是所有viewType的ViewHolder数量之和。

int size() {
    int count = 0;
    for (int i = 0; i < mScrap.size(); i ++) {
        ArrayList<ViewHolder> viewHolders = mScrap.valueAt(i);
        if (viewHolders != null) {
            count += viewHolders.size();
        }
    }
    return count;
}

最后一个方法是根据viewType获取相应缓存list,很简单,有就取出,没有就new一个,new的时候,要将自己put进mScrap中,如果从mMaxScrap中没有找到该种viewType对应的缓存数量上限,那么就使用默认值。

如果我们已经调用过了setMaxRecycledViews()方法,设置过缓存上限,那么mMaxScrap就能找到该种viewType对应的index,所以就不会设置为默认值5.

private ArrayList<ViewHolder> getScrapHeapForType(int viewType) {
    ArrayList<ViewHolder> scrap = mScrap.get(viewType);
    if (scrap == null) {
        scrap = new ArrayList<>();
        mScrap.put(viewType, scrap);
        if (mMaxScrap.indexOfKey(viewType) < 0) {
            mMaxScrap.put(viewType, DEFAULT_MAX_SCRAP);
        }
    }
    return scrap;
}

至此,所有的RecyclerViewPool的源码都已经看完啦,这个类还是比较简单的。

========================================================================

【五,完整代码】

这份代码FooterView并没有用此方法实现,是“强引用VIew方法实现的”。

理由:

1 因为FooterView往往是一个LoadMore相关的提示控件,内存占用很有限。

2 LoadMore相关提示的控件 是需要强引用在Fragment Activity 或者相关类中,即使我在Adapter类里将其引用释放,这个View在内存的空间依然是无法被释放的。

3 两种实现方法都放上来,大家可以根据本文描述的方法,自行尝试将FooterView也改写,可以和我讨论,稍后我也会附加上我修改的版本。





/**
 * 介绍:一个给RecyclerView添加HeaderView FooterView的装饰Adapter类
 * 重点哦~ RecyclerView的HeaderView将可以被系统回收,不像老版的HeaderView是一个强引用在内存里
 * 作者:zhangxutong
 * 邮箱:zhangxutong@imcoming.com
 * 时间: 2016/8/2.
 */
public abstract class HeaderRecyclerAndFooterWrapperAdapter2 extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    private class HeaderBean {
        private final int DEFAULT_HEADER_VIEW_CACHE_SIZE = 5;//默认是5 和RecyclerViewPool的默认值一样
        private int layoutId;//viewType当做layoutId
        private Object data;//该viewType(LayoutId)对应的数据
        private int cacheSize;//该种viewType的HeaderView 在RecyclerViewPool的缓存池内的缓存数量

        public HeaderBean(int layoutId, Object data, int cacheSize) {
            this.layoutId = layoutId;
            this.data = data;
            this.cacheSize = cacheSize;
        }

        public HeaderBean(int layoutId, Object data) {
            this.layoutId = layoutId;
            this.data = data;
            this.cacheSize = DEFAULT_HEADER_VIEW_CACHE_SIZE;
        }

        public int getLayoutId() {
            return layoutId;
        }

        public void setLayoutId(int layoutId) {
            this.layoutId = layoutId;
        }

        public Object getData() {
            return data;
        }

        public void setData(Object data) {
            this.data = data;
        }

        public int getCacheSize() {
            return cacheSize;
        }

        public void setCacheSize(int cacheSize) {
            this.cacheSize = cacheSize;
        }

    }

    private static final int BASE_ITEM_TYPE_FOOTER = 2000000;//footerView的ViewType基准值

    //按照add顺序存放HeaderView的bean,bean包括layoutId,数据Data,和缓存数量cacheSize。
    // 在createViewHOlder里根据layoutId创建UI,在onbindViewHOlder里依据这个data渲染UI,
    // 在onAttachedToRecyclerView 为每种layoutId(同时也是viewType)的headerView设置缓存数量
    private ArrayList<HeaderBean> mHeaderDatas = new ArrayList<HeaderBean>();
    private SparseArrayCompat<View> mFooterViews = new SparseArrayCompat<>();//存放FooterViews,key是viewType

    protected RecyclerView.Adapter mInnerAdapter;//内部的的普通Adapter

    public HeaderRecyclerAndFooterWrapperAdapter2(RecyclerView.Adapter mInnerAdapter) {
        this.mInnerAdapter = mInnerAdapter;
    }

    public int getHeaderViewCount() {
        return mHeaderDatas.size();
    }

    public int getFooterViewCount() {
        return mFooterViews.size();
    }

    private int getInnerItemCount() {
        return mInnerAdapter != null ? mInnerAdapter.getItemCount() : 0;
    }

    /**
     * 传入position 判断是否是headerview
     *
     * @param position
* @return
*/
public boolean isHeaderViewPos(int position) {// 举例, 2 个头,pos 0 1,true, 2+ false
        return getHeaderViewCount() > position;
    }

    /**
     * 传入postion判断是否是footerview
     *
     * @param position
* @return
*/
public boolean isFooterViewPos(int position) {//举例, 2个头,2个inner,pos 0 1 2 3 ,false,4+true
        return position >= getHeaderViewCount() + getInnerItemCount();
    }

    /**
     * 添加HeaderView
     *
     * @param layoutId headerView 的LayoutId
     * @param data     headerView 的data(可能多种不同类型的header 只能用Object了)
     */
public void addHeaderView(int layoutId, Object data) {
        //mHeaderViews.put(mHeaderViews.size() + BASE_ITEM_TYPE_HEADER, v);

/*        SparseArrayCompat headerContainer = new SparseArrayCompat();
        headerContainer.put(layoutId, data);
        mHeaderDatas.put(mHeaderDatas.size() + BASE_ITEM_TYPE_HEADER, headerContainer);*/

        mHeaderDatas.add(new HeaderBean(layoutId, data));
    }

    /**
     * 添加HeaderView
     *
     * @param layoutId  headerView 的LayoutId
     * @param data      headerView 的data(可能多种不同类型的header 只能用Object了)
     * @param cacheSize 该种headerView在缓存池中的缓存个数
     */
public void addHeaderView(int layoutId, Object data, int cacheSize) {
        //mHeaderViews.put(mHeaderViews.size() + BASE_ITEM_TYPE_HEADER, v);

/*        SparseArrayCompat headerContainer = new SparseArrayCompat();
        headerContainer.put(layoutId, data);
        mHeaderDatas.put(mHeaderDatas.size() + BASE_ITEM_TYPE_HEADER, headerContainer);*/

        mHeaderDatas.add(new HeaderBean(layoutId, data, cacheSize));
    }

    /**
     * 设置某个位置的HeaderView
     *
     * @param headerPos 从0开始,如果pos过大 就是addHeaderview
     * @param layoutId
* @param data
*/
public void setHeaderView(int headerPos, int layoutId, Object data) {
        if (mHeaderDatas.size() > headerPos) {
/*            SparseArrayCompat headerContainer = new SparseArrayCompat();
            headerContainer.put(layoutId, data);
            mHeaderDatas.setValueAt(headerPos, headerContainer);*/
            mHeaderDatas.get(headerPos).setLayoutId(layoutId);
            mHeaderDatas.get(headerPos).setData(data);
        } else if (mHeaderDatas.size() == headerPos) {//调用addHeaderView
            addHeaderView(layoutId, data);
        } else {
            //
            addHeaderView(layoutId, data);
        }
    }

    /**
     * 设置某个位置的HeaderView
     *
     * @param headerPos 从0开始,如果pos过大 就是addHeaderview
     * @param layoutId
* @param data
* @param cacheSize 该种headerView在缓存池中的缓存个数
     */
public void setHeaderView(int headerPos, int layoutId, Object data, int cacheSize) {
        if (mHeaderDatas.size() > headerPos) {
/*            SparseArrayCompat headerContainer = new SparseArrayCompat();
            headerContainer.put(layoutId, data);
            mHeaderDatas.setValueAt(headerPos, headerContainer);*/
            mHeaderDatas.get(headerPos).setLayoutId(layoutId);
            mHeaderDatas.get(headerPos).setData(data);
            mHeaderDatas.get(headerPos).setCacheSize(cacheSize);
        } else if (mHeaderDatas.size() == headerPos) {//调用addHeaderView
            addHeaderView(layoutId, data, cacheSize);
        } else {
            //
            addHeaderView(layoutId, data, cacheSize);
        }
    }

    /**
     * 添加FooterView
     *
     * @param v
*/
public void addFooterView(View v) {
        mFooterViews.put(mFooterViews.size() + BASE_ITEM_TYPE_FOOTER, v);
    }

    /**
     * 清空HeaderView数据
     */
public void clearHeaderView() {
        mHeaderDatas.clear();
    }

    public void clearFooterView() {
        mFooterViews.clear();
    }


    public void setFooterView(View v) {
        clearFooterView();
        addFooterView(v);
    }

    @Override
    public int getItemViewType(int position) {
        if (isHeaderViewPos(position)) {
            return mHeaderDatas.get(position).getLayoutId();//HeaderView的layoutId就是viewType
        } else if (isFooterViewPos(position)) {//举例:header 2, innter 2, 0123都不是,4才是,4-2-2 = 0,ok。
            return mFooterViews.keyAt(position - getHeaderViewCount() - getInnerItemCount());
        }
        return super.getItemViewType(position - getHeaderViewCount());
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {

        if (mHeaderDatas != null && !mHeaderDatas.isEmpty()) {//不为空,说明有headerview
            for (HeaderBean HeaderBean : mHeaderDatas) {
                if (HeaderBean.getLayoutId() == viewType) {//匹配上了说明是headerView
                    return ViewHolderHeader3.get(parent.getContext(), null, parent, viewType, -1);
                }
            }
        }
        if (mFooterViews.get(viewType) != null) {//不为空,说明是footerview
            return new ViewHolder(parent.getContext(), mFooterViews.get(viewType));
        }
        return mInnerAdapter.onCreateViewHolder(parent, viewType);
    }

    //protected abstract RecyclerView.ViewHolder createHeader(ViewGroup parent, int headerPos);

    protected abstract void onBindHeaderHolder(ViewHolderHeader3 holder, int headerPos, int layoutId, Object o);//多回传一个layoutId出去,用于判断是第几个headerview

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        if (isHeaderViewPos(position)) {
            onBindHeaderHolder((ViewHolderHeader3) holder, position, mHeaderDatas.get(position).getLayoutId(), mHeaderDatas.get(position).getData());
            return;
        } else if (isFooterViewPos(position)) {
            return;
        }
        //举例子,2个header,0 1是头,2是开始,2-2 = 0
        mInnerAdapter.onBindViewHolder(holder, position - getHeaderViewCount());
    }


    @Override
    public int getItemCount() {
        return getInnerItemCount() + getHeaderViewCount() + getFooterViewCount();
    }

    @Override
    public void onAttachedToRecyclerView(RecyclerView recyclerView) {
        mInnerAdapter.onAttachedToRecyclerView(recyclerView);
        //设置HeaderView的ViewHolder的缓存数量
        if (null != mHeaderDatas && !mHeaderDatas.isEmpty()) {
            for (HeaderBean HeaderBean : mHeaderDatas) {
                recyclerView.getRecycledViewPool().setMaxRecycledViews(HeaderBean.getLayoutId(), HeaderBean.getCacheSize());
            }
        }
        //为了兼容GridLayout
        RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
        if (layoutManager instanceof GridLayoutManager) {
            final GridLayoutManager gridLayoutManager = (GridLayoutManager) layoutManager;
            final GridLayoutManager.SpanSizeLookup spanSizeLookup = gridLayoutManager.getSpanSizeLookup();

            gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
                @Override
                public int getSpanSize(int position) {
                    int viewType = getItemViewType(position);
                    if (isHeaderViewPos(position)) {
                        return gridLayoutManager.getSpanCount();
                    } else if (mFooterViews.get(viewType) != null) {
                        return gridLayoutManager.getSpanCount();
                    }
                    if (spanSizeLookup != null)
                        return spanSizeLookup.getSpanSize(position);
                    return 1;
                }
            });
            gridLayoutManager.setSpanCount(gridLayoutManager.getSpanCount());
        }

    }

    @Override
    public void onViewAttachedToWindow(RecyclerView.ViewHolder holder) {
        mInnerAdapter.onViewAttachedToWindow(holder);
        int position = holder.getLayoutPosition();
        if (isHeaderViewPos(position) || isFooterViewPos(position)) {
            ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();

            if (lp != null
                    && lp instanceof StaggeredGridLayoutManager.LayoutParams) {

                StaggeredGridLayoutManager.LayoutParams p =
                        (StaggeredGridLayoutManager.LayoutParams) lp;

                p.setFullSpan(true);
            }
        }
    }
}

========================================================================

【6 实验】

是否使用了我这种方法,设置了headerView的缓存数量为0后,该HeaderView不在屏幕显示时,触发GC,内存空间真的能被回收呢,?那么就实验见真相。

这里我们借助 Android Studio 的Android Monitor里的dump java heap,它可以打印出当前内存里的对象的情况,还有旁边的小卡车按钮 Initiate GC,可以触发GC事件。

我的猜想,设置HeaderView的缓存数量为0,当向下滑动一段距离,HeaderView已不可见时,打印内存对象情况,HeaderViewHolder应该是一个“”野对象“”,没有任何引用,触发GC后,它将被回收,内存降下降。

而使用传统方法添加的HeaderView,即使设置缓存数量为0,由于View被强引用,内存空间也无法释放。

(剧透一点,我原本想的是HeaderViewHolder应该被释放,而HeaderView不会被释放,但实际上内存空间真的没有减少,但是HeaderViewHolder也被强引用住了,如有知情者 望不吝赐教 )使用本方法:添加6个HeaderView,5个不设置缓存数量,1个设置缓存数量(加载四张图片的View),滑动到屏幕低端,Dump Java Heap,

可以看到有一个HeaderView的ViewHolder的Depth为空,说明没设置缓存数量的ViewHolder的确会被回收掉,这时候我们点击黄色小卡车 强制GC,

内存降了一半,这里面就有被回收掉的ViewHolder的功劳,ViewHolder被顺利回收,它里面引用的图片也可以被系统回收 腾出空间。

此时再Dump Java Heap 查看一下,果然被回收掉了一个ViewHolder对象。
再实验一下,只添加一个HeaderView(内含4个ImageView),然后滑动出屏幕后,HeaderViewHolder的数量,以及GC后的情况。对应的图片如下:

此时就一个ViewHolderHeader的野对象,

GC后,它顺利被回收,内存下降(四张图片被回收的功效。)

这时,打开使用传统方式Adapter的Activity,也已经将这个RecyclerView的HeaderView的缓存设置为0和上次操作一样,进入后,滑动界面到HeaderView移除屏幕,Dump Java Heap,

发现ViewHolderHeader3 的Depth是9,说明其正在被引用着,是无法被释放的,这时我们就算再怎么点击Initiate GC,内存也不会下降很明显,GC后再查看Java Heap,ViewHolderHeader3的对象依然存在,说明其确实被强引用着没有被系统回收。

这里有个疑问,想问问各位,@翔神,在我的理解里,HeaderView实例由于在Adapter中强引用不会被回收,可是HeaderViewHolder 并没有被强引用住啊。这里看它确实被强引用了。这是一个疑问点,

其实本文到这里,可以结束了,我已经证明了本文的HeaderAdapter 确实可以将其ViewHolder回收,如果此时触发GC,将释放该部分内存空间,可是那个HeaderViewHolder的强引用的问题 我依然没有答案。我又做了一些实验,

不停的上下滑动,令这个HeaderView 一会滑出屏幕 一会进入屏幕,我的猜想中,应该会create N个 HeaderViewHolder。

查看java heap,果然如下图:

可以看到由于没有缓存,我们new了 9个ViewHolder出来,但是其中8个是野对象,可以被GC回收,此时屏幕上HeaderView已经不可见了,不明白为何还会有一个强引用。

此问题我将继续研究下去。。

========================================================================

【7 总结:】

以本文的方法添加的HeaderView以layoutId做itemType。

可以添加多种HeaderView。

可以设置每种HeaderView的ViewHolder在RecyclerViewPool缓存池中的数量。
设置为0则不缓存,当HeaderView不在屏幕可见时,触发GC的话, 它的ViewHolder将被回收,因为ViewHolder里的ItemView(HeaderView)本身也没有被其他强引用,所以它的内存空间将被释放。

就算不设置cacheSize,默认缺省值为5(和RecyclerViewPool的缺省值一致)。

同一种类型(相同的layoutId,viewType)的HeaderView也将被RecyclerVIew管理,不像之前那种方法 永远存于内存中。

例如,有两种item1类型的HeaderView,设置缓存数量为1,那么 RecyclerViewPool里会缓存 一份item1类型的ViewHolder,有一份View的空间可以被系统回收。

缓存本身是一种用空间换时间的技术,我们这么做,将更加灵活,headerView本身简单时,可以用默认配置,被RecyclerViewPool缓存住无法释放内存也无伤大雅,如果HeaderView本身很臃肿,占内存,滑出页面后,希望内存空间可以被回收,那么可以配置缓存数量,为0 则不缓存,但与此同时,我们获取了空间,就要付出时间的代价,每次滚回屏幕时,它的ViewHolder已经不存在,所以会重走onCreateViewHolder方法,这需要一定的时间。
所以说,这种方法只是让开发者多一种选择,万一出现特殊情况,想要释放HeaderView,也有计可施,实际开发中,大部分情况不需要设置缓存个数,

如果考虑多个RecyclerView共用同一个RecyclerViewPool,可为HeaderView设置1-2个缓存数。

RecyclerView涉及的东西还很多,多个RecyclerView共用一个RecyclerViewPool 我稍后将研究一下,争取也研究出一点小心得分享给大家。即使没有什么浏览量,多一个人看到也是好事。说实话,设置缓存数量,95%的情况下用不到,但是假设一个场景,(我司app就如此),首页5个Tab,每个页面HeaderView长度2-3个屏幕,上面各种图片,5个Framgent切换是用hide show做的,所以5个页面全开时,将全部存于内存中,更可怕的是,每个页面头部再分2-3个子tab,每个子tab点开又是一坨。。。每个页面的风格都像h5。

而不用replace是希望用户每次切换tab回来后还能停留在上次浏览的地方,这就需要我们做一个抉择,很多时候还要和产品商量(不过他们既然设计出这种界面,我已经放弃和他们商量)。

如果依然要流畅的速度,还要这种又臭又长的页面,还不想内存OOMcrash,那么我只能选择!狗带~

========================================================================

本文工具类已经被收入该库:https://github.com/mcxtzhang/all-base-adapter

该库还包括了N多的好用的Adapter。

========================================================================

源码链接:http://download.csdn.net/detail/zxt0601/9609911

========================================================================

点这里可以跳转到人工智能网站

发表评论