RecyclerView源码解析(三):RecyclerView的缓存机制

news2024/11/25 11:46:31

RecyclerView源码解析(三):RecyclerView的缓存机制

一只猫在公交车站等待,动漫卡通

导言

上两篇文章我们结合LinearLayoutManager对RecyclerView整体是如何绘制的有了大致的了解,不过RecyclerView的重头戏并不是简单显示列表,而是它通过缓存机制实现的高性能,本篇文章我们继续对源码进行分析来解析RecyclerView最重要的缓存机制。

关于viewHolder的状态

在RecyclerView中,ViewHolder作为视图的持有者会处于各种不同的状态,下面是可能出现的状态:

  1. Attached(附加):ViewHolder 附加到 RecyclerView 中的父视图,它是可见的并可以响应用户输入事件。在这个状态下,ViewHolder 可以被看到,而且可以与用户进行交互。
  2. Detached(分离):ViewHolder 不再附加到 RecyclerView 中的父视图,它不再可见。在这个状态下,ViewHolder 可能已经超出了屏幕可见范围,或者它已经从 RecyclerView 中分离,但尚未被回收。
  3. Recycled(回收):ViewHolder 被回收,通常是因为它不再可见或不再需要。在这个状态下,ViewHolder 的视图资源被释放,但ViewHolder 对象本身可能会被保留在 RecyclerView 的回收池中,以便稍后重用。
  4. Invalid(无效):ViewHolder 被标记为无效,通常表示该 ViewHolder 对应的数据项已经发生了变化。在这个状态下,ViewHolder 可能需要重新绑定以反映最新的数据。
  5. Removed(移除):ViewHolder 被标记为已移除,通常表示该 ViewHolder 对应的数据项已从适配器数据集中删除。这个状态通常会伴随着一些动画效果,直到 ViewHolder 完全离开屏幕。
  6. Update(更新):ViewHolder 被标记为需要更新,通常表示 ViewHolder 所对应的数据发生了部分更改,需要进行局部更新而不是完全的重新绑定。

而源文件中也有各种各样的FLAG标志位的定义和注释,这里介绍一些经常会出现的标志位:

  • FLAG_BOUND标志: 表示 ViewHolder 已经绑定到了一个数据位置上,ViewHolder 的 mPositionmItemIdmItemViewType 都已经设置为有效值。

  • FLAG_UPDATE标志:表示 ViewHolder 的视图所反映的数据已经过时,需要由适配器重新绑定。ViewHolder 的 mPositionmItemId 仍然保持有效,但是需要重新执行数据绑定操作以更新视图的内容。(区别于完全重新绑定,viewType还是没变)

  • FLAG_INVALID标志: 表示 ViewHolder 的数据无效。ViewHolder 的 mPositionmItemId 不再可信,而且可能不再匹配项视图的类型。这意味着 ViewHolder 需要完全重新绑定到不同的数据项,通常是因为与之前的数据不再匹配。这个标志位用于通知 RecyclerView,特定的 ViewHolder 需要进行完全的重新绑定。

  • FLAG_REMOVED标志:表示 ViewHolder 对应的数据项曾经从数据集中移除。但是,它的视图可能仍然可以用于执行一些操作,例如进行退出动画。这个标志位用于标记 ViewHolder 对应的数据项已被移除,但 RecyclerView 可能仍然需要对其执行一些动画等操作。

  • FLAG_NOT_RECYCLABLE标志:表示 ViewHolder 不应该被回收。通常,RecyclerView 会尝试回收不再需要的 ViewHolder 以便节省内存和性能。然而,在某些情况下,你可能希望保持 ViewHolder 不被回收,比如在执行动画时需要保持视图。

  • FLAG_RETURNED_FROM_SCRAP标志:表示这个 ViewHolder 是从废弃状态返回的。当一个 ViewHolder 从废弃状态返回时,它会保留在废弃列表中,直到布局过程结束,然后如果没有重新添加到 RecyclerView 中,它会被 RecyclerView 回收。

  • FLAG_IGNORE标志: 表示这个 ViewHolder 被布局管理器完全管理。它不会被废弃、回收或移除,除非布局管理器被替换。这个 ViewHolder 对布局管理器来说仍然是完全可见的。通常情况下,这种标志用于标识 RecyclerView 中的某些视图应该由布局管理器自己处理,而不被 RecyclerView 的回收和复用系统管理。

  • FLAG_TMP_DETACHED标志:表示当视图从父视图中分离(detached)时,设置了这个标志,以便在需要将其移除或添加回来时采取正确的操作。这个标志通常用于处理视图的临时分离状态。

源码解析

从next方法开始

解析缓存机制的话我们还是以LinearLayoutManager为例,从它的next方法作为切入点进行分析,因为在布局是LinearLayoutManager是通过next方法来获取下一个布局的视图的,来看next方法:

View next(RecyclerView.Recycler recycler) {
    if (mScrapList != null) {
        return nextViewFromScrapList();
    }
    final View view = recycler.getViewForPosition(mCurrentPosition);
    mCurrentPosition += mItemDirection;
    return view;
}

这个方法张红出现了一个mScrapList变量,根据注释:

在RecyclerView中,LayoutManager(LLM)可以设置一个mScrapList(或称为"废弃列表")来存储特定的ViewHolder对象。当LayoutManager需要布局特定的视图时,它将这些视图的ViewHolder对象添加到mScrapList中。然后,在LayoutState中,当需要获取下一个要布局的视图时,会首先尝试从mScrapList中获取ViewHolder对象,如果找到则返回,否则返回null。

这个机制的目的是提高性能,因为LayoutManager在进行布局时可能需要多次获取ViewHolder对象,而不必每次都从Recycler中获取。通过使用mScrapList,LayoutManager可以在内部管理一组特定的ViewHolder对象,以提高布局效率。

需要注意的是,mScrapList是一个临时的列表,用于特定的布局操作,它不会永久存储ViewHolder对象。这些ViewHolder对象仍然受Recycler的管理,并可以被回收和重用。

我们可以把这个mScrapList看做是一个由LinearLayoutManager管理的缓存列表,不过它不会永久存储ViewHolder对象。这些ViewHolder对象仍然受Recycler的管理,并可以被回收和重用。RecyclerView实现缓存主要也不是依赖于LayoutManager类,而是通过RecyclerView自身的Recycler回收池,就比如之后就调用了recycler.getViewForPosition(mCurrentPosition),这就是通过Recycler来获取view的。我们首先大致看一看Recycler类的大致结构。

Recycler类

这个类也有必要的注释:

在这里插入图片描述

Recycler(回收器)是负责管理被丢弃或分离的视图以供重用的组件。

"被丢弃"的视图是指仍然附加到其父RecyclerView但已被标记为要删除或重用的视图。

RecyclerView.LayoutManager通常通过Recycler来获取适配器的数据集中特定位置或项ID的视图。如果要重用的视图被认为是 “脏的”,则适配器将被要求重新绑定它。如果不是脏视图,LayoutManager可以快速地重新使用它,而无需进一步的工作。未请求布局的干净视图可能会被LayoutManager重新定位,而无需重新测量。Recycler的主要作用是协助RecyclerView的LayoutManager有效地管理和重用视图,以提高性能和滚动的流畅性。

如果要对数据进行缓存的话,显然需要一些成员变量来作为容器,我们正好可以在Recycler类中发现这些成员变量:

final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();//一级缓存
final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();//二级缓存
RecycledViewPool mRecyclerPool;//四级缓存
private ViewCacheExtension mViewCacheExtension;//三级缓存

主要是通过这四级缓存来构建起来了整个RecyclerView的缓存机制。

何处真正获取到View对象

tryGetViewHolderForPositionByDeadline方法的第一部分

前面说到,next方法中将通过recycler.getViewForPosition(mCurrentPosition)方法来试图获取到View,而这个方法实际上又会跳转到别的方法中,最终会调用到RecyclerView的tryGetViewHolderForPositionByDeadline方法中,同样的,这个方法也有注释:

在这里插入图片描述

这段注释可以说是很重要,因为它清楚地指出了view的获取来源:

这段代码描述了RecyclerView中的一个方法,用于获取给定位置的ViewHolder。ViewHolder是用来显示RecyclerView中的每个项的视图容器。这个方法的作用是尝试从不同的来源获取ViewHolder,包括Recycler的临时存储(scrap)、缓存(cache)、RecycledViewPool(用于存储被回收的视图)、或者直接创建ViewHolder。

这就指明了常用的三个缓存区,分别就是mAttachedScrapmCachedViewsmRecyclerPool.接下来分析这个方法,把这个方法拆解成几个步骤:

boolean fromScrapOrHiddenOrCache = false;
ViewHolder holder = null;
// 0) If there is a changed scrap, try to find from there
if (mState.isPreLayout()) {
    holder = getChangedScrapViewForPosition(position);
    fromScrapOrHiddenOrCache = holder != null;
}

这一步将根据当前是否在进行预布局来决定是否要通过 getChangedScrapViewForPosition(position)方法来获得ViewHolder。这个是否正在进行预布局是在哪里设置的呢?在RecyclerView中这个标志位主要是根据另一个标志位mRunPredictiveAnimations来决定的,当该标志位为true时才需要进行预布局。而这个mRunPredictiveAnimations又需要mRunSimpleAnimations标志位来决定,总的来说如果有动画的话才会进行预布局,默认状态下是为false的。

// 1) Find by position from scrap/hidden list/cache
if (holder == null) {
    holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
    if (holder != null) {
        if (!validateViewHolderForOffsetPosition(holder)) {
            // recycle holder (and unscrap if relevant) since it can't be used
            if (!dryRun) {
                // we would like to recycle this but need to make sure it is not used by
                // animation logic etc.
                holder.addFlags(ViewHolder.FLAG_INVALID);
                if (holder.isScrap()) {
                    removeDetachedView(holder.itemView, false);
                    holder.unScrap();
                } else if (holder.wasReturnedFromScrap()) {
                    holder.clearReturnedFromScrapFlag();
                }
                recycleViewHolderInternal(holder);
            }
            holder = null;
        } else {
            fromScrapOrHiddenOrCache = true;
        }
    }
}

当在之前的判断中没有获得holder的情况下先会调用getScrapOrHiddenOrCachedHolderForPosition来获得来自mAttachedScrap,mHiddenViews或者mCachedViews中的holder,这其实就是从缓存中获取数据。我们可以根据从获取数据三个缓存区为分界,将这个方法分成三个部分,分别就是从mAttachedScrap,mHiddenViewsmCachedViews中获取holder的过程,查看该方法的具体逻辑:

	final int scrapCount = mAttachedScrap.size();
    // Try first for an exact, non-invalid match from scrap.
    for (int i = 0; i < scrapCount; i++) {
        final ViewHolder holder = mAttachedScrap.get(i);
        if (!holder.wasReturnedFromScrap() && holder.getLayoutPosition() == position
                && !holder.isInvalid() && (mState.mInPreLayout || !holder.isRemoved())) {
            holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
            return holder;
        }
    }    

第一部分会首先尝试在mAttachedScrap中获取holder,如果当前holder在之前尚未被从mAttachedScrap返回的话(即是首次从mAttachedScrap中返回),且满足布局位置与布局位置相同等一系列条件的话,就说明当前的holder是可以重用的。如果可以重用的话就会标记当前holder已经被重用了,最后就可以返回获得的holder了。

if (!dryRun) {
    View view = mChildHelper.findHiddenNonRemovedView(position);
    if (view != null) {
        // This View is good to be used. We just need to unhide, detach and move to the
        // scrap list.
        final ViewHolder vh = getChildViewHolderInt(view);
        mChildHelper.unhide(view);
        int layoutIndex = mChildHelper.indexOfChild(view);
        if (layoutIndex == RecyclerView.NO_POSITION) {
            throw new IllegalStateException("layout index should not be -1 after "
                    + "unhiding a view:" + vh + exceptionLabel());
        }
        mChildHelper.detachViewFromParent(layoutIndex);
        scrapView(view);
        vh.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP
                | ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
        return vh;
    }
}

第二部分会尝试从HiddenView中获取,也就是从隐藏列表中获取view,然后根据这个view构建出一个viewHolder,把这个viewHolder添加进入Scrap中,接着会设置标志位以表明该view已经被重用,最后返回viewHolder。需要解释的是这个dryRun标志位,这个用于指示是否执行 “干运行”,也就是在查找ViewHolder时不执行真正的操作,而只是找到匹配的ViewHolder但不进行后续的移除或处理操作。这个参数通常用于在不实际操作ViewHolder的情况下,只是查找匹配的ViewHolder并返回它,以便后续根据需要进行操作。如果 dryRun 为 true,那么操作是干净的,不会对数据产生实际影响;如果为 false,那么操作将会对数据产生实际影响,比如从缓存中移除ViewHolder。

// Search in our first-level recycled view cache.
final int cacheSize = mCachedViews.size();
for (int i = 0; i < cacheSize; i++) {
    final ViewHolder holder = mCachedViews.get(i);
    // invalid view holders may be in cache if adapter has stable ids as they can be
    // retrieved via getScrapOrCachedViewForId
    if (!holder.isInvalid() && holder.getLayoutPosition() == position
            && !holder.isAttachedToTransitionOverlay()) {
        if (!dryRun) {
            mCachedViews.remove(i);
        }
        if (DEBUG) {
            Log.d(TAG, "getScrapOrHiddenOrCachedHolderForPosition(" + position
                    + ") found match in cache: " + holder);
        }
        return holder;
    }
}

第三部分是会尝试从mCachedViews获取holder,注释中也提到这是尝试从第一级回收视图缓存。它会依次获取并检查这个holder的状态,如果该holder有效,布局位置与所需的位置匹配,以及ViewHolder没有附加到过渡叠加层(transition overlay)上的话就可以返回这个holder。如果没有被标记为dryRun的话还有从缓存中将该holder给移除。

可以用一个流程图概括一下这个方法:

在这里插入图片描述

tryGetViewHolderForPositionByDeadline方法的第二部分

书接上段,让我们继续回到tryGetViewHolderForPositionByDeadline方法中:

if (holder == null) {
    holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
    if (holder != null) {
        if (!validateViewHolderForOffsetPosition(holder)) {
            // recycle holder (and unscrap if relevant) since it can't be used
            if (!dryRun) {
                // we would like to recycle this but need to make sure it is not used by
                // animation logic etc.
                holder.addFlags(ViewHolder.FLAG_INVALID);
                if (holder.isScrap()) {
                    removeDetachedView(holder.itemView, false);
                    holder.unScrap();
                } else if (holder.wasReturnedFromScrap()) {
                    holder.clearReturnedFromScrapFlag();
                }
                recycleViewHolderInternal(holder);
            }
            holder = null;
        } else {
            fromScrapOrHiddenOrCache = true;
        }
    }
}

如果在getScrapOrHiddenOrCachedHolderForPosition方法中获得了holder的话就会对该holder的有效性再次进行验证validateViewHolderForOffsetPosition(holder)方法,如果holder无效的话就会进入到接下来的分支中对该holder进行一些处理:

  • 如果 ViewHolder 是从缓存中取出的(holder.isScrap() 返回 true),它会将 ViewHolder 的 itemView(视图)从 RecyclerView 中移除,并将 ViewHolder 标记为未被回收状态(holder.unScrap())。
  • 如果 ViewHolder 是从回收池中返回的(holder.wasReturnedFromScrap() 返回 true),它会清除 ViewHolder 的返回自回收池标志。
  • 最后,无论是哪种情况,都会调用 recycleViewHolderInternal(holder) 方法将 ViewHolder 回收,即将其添加回 RecyclerView 的回收池中。

第一个isScrap在之前的获得holder的方法有所体现,是在HiddenView中查找的流程中,查找到的话会调用一个scrapView方法,调用这个方法后isScrap就会被设置为true了。如果之前获得的holder有效的话将 fromScrapOrHiddenOrCache 标志设置为 true,以表示它是从缓存或隐藏状态中获取的。

总的来说这第二部分是对之前获得到的ViewHolder进行验证,如果无法使用,则将其标记为无效并回收;如果可以使用,则将其标记为来自缓存或隐藏状态。这是为了确保 RecyclerView 中的 ViewHolder 在正确的位置使用,以避免出现问题。

tryGetViewHolderForPositionByDeadline方法的第三部分

if (holder == null) {
    final int offsetPosition = mAdapterHelper.findPositionOffset(position);
    .......
    final int type = mAdapter.getItemViewType(offsetPosition);
    // 2) Find from scrap/cache via stable ids, if exists
    if (mAdapter.hasStableIds()) {
        holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                type, dryRun);
        if (holder != null) {
            // update position
            holder.mPosition = offsetPosition;
            fromScrapOrHiddenOrCache = true;
        }
    }
    .........
   }

这第三部分对应的是Adapter有StableIds的情况,hasStableIds() 是 RecyclerView.Adapter 类的一个方法,用于指示 RecyclerView 的适配器是否为数据集中的每个项提供了稳定的唯一标识符(ID)。这个标识符可以作为数据集中给定位置上的项的键,如果该项在数据集中重新定位,那么应该返回相同的 ID。

在这一部分将会通过getScrapOrCachedViewForId方法从Scrap中或者CachedView中获得ViewHolder。来看这个方法的第一部分:

// Look in our attached views first
final int count = mAttachedScrap.size();
for (int i = count - 1; i >= 0; i--) {
    final ViewHolder holder = mAttachedScrap.get(i);
    if (holder.getItemId() == id && !holder.wasReturnedFromScrap()) {
        if (type == holder.getItemViewType()) {
            holder.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP);
            if (holder.isRemoved()) {
                // this might be valid in two cases:
                // > item is removed but we are in pre-layout pass
                // >> do nothing. return as is. make sure we don't rebind
                // > item is removed then added to another position and we are in
                // post layout.
                // >> remove removed and invalid flags, add update flag to rebind
                // because item was invisible to us and we don't know what happened in
                // between.
                if (!mState.isPreLayout()) {
                    holder.setFlags(ViewHolder.FLAG_UPDATE, ViewHolder.FLAG_UPDATE
                            | ViewHolder.FLAG_INVALID | ViewHolder.FLAG_REMOVED);
                }
            }
            return holder;
        } else if (!dryRun) {
            // if we are running animations, it is actually better to keep it in scrap
            // but this would force layout manager to lay it out which would be bad.
            // Recycle this scrap. Type mismatch.
            mAttachedScrap.remove(i);
            removeDetachedView(holder.itemView, false);
            quickRecycleScrapView(holder.itemView);
        }
    }
}

和之前第一部分获取holder的方法很类似,会尝试从mAttachedScrap获取holder,如果holder有效且type符合就会返回这个holder并将该holder标记为已重用。如果type不符合的话还会将该holder从mAttachedScrap给移除并将其视图分离,最后将该试图拆解并回收。最后这个quickRecycleScrapView方法会继续向后调用recyclerViewHolderInternal方法,这个方法是用来回收ViewHolder的,会试图将ViewHolder回收入mRecyclerPool或者mCachedViews中。我们看看这个方法:

void recycleViewHolderInternal(ViewHolder holder) {
    ......
    if (forceRecycle || holder.isRecyclable()) {
	.......
            if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
                recycleCachedViewAt(0);//从mCachedViews中移除并且将其添加进入RecyclerPool中
                cachedViewSize--;
            }
		   .......
            mCachedViews.add(targetCacheIndex, holder);//添加进mCachedViews中
            cached = true;
        }
        if (!cached) {
            addViewHolderToRecycledViewPool(holder, true);//如果未被cached就将其添加入RecyclerPool中
            recycled = true;
        }
    } else {
		.......
    }
    // even if the holder is not removed, we still call this method so that it is removed
    // from view holder lists.
    mViewInfoStore.removeViewHolder(holder);//将该holder完全移除
    if (!cached && !recycled && transientStatePreventsRecycling) {
        holder.mOwnerRecyclerView = null;
    }
}

首先会判断mCachedViews是否可用,如果可用会再次判断mCachedViews是否满了,如果满了的话就会将mCachedViews中index为0处的holder给移除并将其添加进入mRecyclerPool中。如果mCachedViews不可用就会直接将其添加进入mRecyclerPool中。

接下来回到getScrapOrCachedViewForId方法的第二部分:

// Search the first-level cache
final int cacheSize = mCachedViews.size();
for (int i = cacheSize - 1; i >= 0; i--) {
    final ViewHolder holder = mCachedViews.get(i);
    if (holder.getItemId() == id && !holder.isAttachedToTransitionOverlay()) {
        if (type == holder.getItemViewType()) {
            if (!dryRun) {
                mCachedViews.remove(i);
            }
            return holder;
        } else if (!dryRun) {
            recycleCachedViewAt(i);
            return null;
        }
    }
}

这里就是从第一层缓存中寻找,具体来说是从mCachedViews中寻找holder,如果找到了的话就会将其从mCachedViews中移除,如果发现viewType不匹配还会将其从mCachedViews中移入RecyclerPool中去。

tryGetViewHolderForPositionByDeadline方法的第四部分

if (holder == null && mViewCacheExtension != null) {
    // We are NOT sending the offsetPosition because LayoutManager does not
    // know it.
    final View view = mViewCacheExtension
            .getViewForPositionAndType(this, position, type);
    if (view != null) {
        holder = getChildViewHolder(view);
        if (holder == null) {
            throw new IllegalArgumentException("getViewForPositionAndType returned"
                    + " a view which does not have a ViewHolder"
                    + exceptionLabel());
        } else if (holder.shouldIgnore()) {
            throw new IllegalArgumentException("getViewForPositionAndType returned"
                    + " a view that is ignored. You must call stopIgnoring before"
                    + " returning this view." + exceptionLabel());
        }
    }
}

这第四部分会尝试从mViewCacheExtension中获取holder,这个默认是空的,指的是我们自定义的视图缓存一般反正我也不用,我们直接跳过这一段。

tryGetViewHolderForPositionByDeadline方法的第五部分

if (holder == null) { // fallback to pool
    if (DEBUG) {
        Log.d(TAG, "tryGetViewHolderForPositionByDeadline("
                + position + ") fetching from shared pool");
    }
    holder = getRecycledViewPool().getRecycledView(type);
    if (holder != null) {
        holder.resetInternal();
        if (FORCE_INVALIDATE_DISPLAY_LIST) {
            invalidateDisplayListInt(holder);
        }
    }
}

经历了前四部分相信这里我们理解起来就很清晰了,会尝试从RecyclerPool中获取holder,如果成功从共享池中获取到一个 ViewHolder(holder != null),还会执行以下步骤:

  • 重置 ViewHolder 的内部状态,以便它可以被重新使用。
  • 如果 FORCE_INVALIDATE_DISPLAY_LIST 为 true,还会调用 invalidateDisplayListInt(holder) 方法来强制刷新视图的显示列表(这通常在一些特定情况下需要,以确保视图的显示正确)。

tryGetViewHolderForPositionByDeadline方法的第五部分

if (holder == null) {
    long start = getNanoTime();
    if (deadlineNs != FOREVER_NS
            && !mRecyclerPool.willCreateInTime(type, start, deadlineNs)) {
        // abort - we have a deadline we can't meet
        return null;
    }
    holder = mAdapter.createViewHolder(RecyclerView.this, type);
    if (ALLOW_THREAD_GAP_WORK) {
        // only bother finding nested RV if prefetching
        RecyclerView innerView = findNestedRecyclerView(holder.itemView);
        if (innerView != null) {
            holder.mNestedRecyclerView = new WeakReference<>(innerView);
        }
    }

    long end = getNanoTime();
    mRecyclerPool.factorInCreateTime(type, end - start);
    if (DEBUG) {
        Log.d(TAG, "tryGetViewHolderForPositionByDeadline created new ViewHolder");
    }
}

最后这一部分的情况就是当在所有的缓存中都没有找到可以重用的holder时就会调用我们重写的createViewHolder方法来生成一个全新的ViewHolder。

总结如何从缓存中获取ViewHolder

终于分析完了这个巨长无比的tryGetViewHolderForPositionByDeadLine方法了,现在估计还是很疑惑,现在让我们对上面这个过程进行一下总结。

在这里插入图片描述

最后我光看到了将viewHolder添加进入RecyclerPool而没有看到清除,所以对其源码进行了查看,发现mRecyclerPool是通过ArrayList存储viewHolder,用SparseArray存储与之相关的一些信息,通过这两个容器检查了回收池中指定类型的视图是否已满。如果池已满,就直接返回,不会将视图添加到回收池中。这是为了限制池中视图的数量,以防止占用过多内存。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/977472.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

nginx搭建DNS服务器

目录 搭建DNS服务器搭建一个DNS缓存域名服务器主从DNS服务器的工作原理查域里的域名服务器记录记录类型做一个自己的域名数据库&#xff0c;给sc.com提供解析 反向解析的配置文件和数据文件 搭建DNS服务器 主配置文件&#xff1a;/etc/named.conf 次要配置文件&#xff1a;/et…

openGauss学习笔记-61 openGauss 数据库管理-常见主备部署方案

文章目录 openGauss学习笔记-61 openGauss 数据库管理-常见主备部署方案61.1 单中心61.2 同城双中心61.3 两地三中心61.4 两地三中心流式容灾方案 openGauss学习笔记-61 openGauss 数据库管理-常见主备部署方案 61.1 单中心 图 1 单中心部署图 组网特点&#xff1a; 单AZ部署…

【大数据】Flink 详解(七):源码篇 Ⅱ

本系列包含&#xff1a; 【大数据】Flink 详解&#xff08;一&#xff09;&#xff1a;基础篇【大数据】Flink 详解&#xff08;二&#xff09;&#xff1a;核心篇 Ⅰ【大数据】Flink 详解&#xff08;三&#xff09;&#xff1a;核心篇 Ⅱ【大数据】Flink 详解&#xff08;四…

CocosCreator3.8研究笔记(八)CocosCreator 节点和组件的使用

我们知道&#xff0c;在CocosCreator 节点和组件的修改有两种方法&#xff1a; 属性检查器中的设置脚本中的动态修改 脚本中动态修改&#xff0c;能实现各种各样的游戏逻辑&#xff0c;例如响应玩家输入&#xff0c;删除、修改、销毁节点或组件。 不过想要实现这些游戏逻辑&a…

51单片机简易时钟闹钟八位数码管显示仿真( proteus仿真+程序+原理图+报告+讲解视频)

51单片机简易时钟闹钟八位数码管显示仿真( proteus仿真程序原理图报告讲解视频&#xff09; 1.主要功能&#xff1a;2.仿真3. 程序代码4. 原理图元器件清单 5. 设计报告6. 设计资料内容清单&&下载链接资料下载链接&#xff08;可点击&#xff09;&#xff1a; 51单片机…

数据结构插入排序

好久不见&#xff0c;这几天有点事情&#xff0c;都快一个礼拜没有学习&#xff0c;对键盘都要陌生起来了&#xff0c;今天也是刚刚学了一点排序&#xff0c;在这里也给大家更新一个插入排序&#xff0c;后面也会渐渐的把八大排序更新完的&#xff0c;还有就是二叉树&#xff0…

【大数据Hive】hive 加载数据常用方案使用详解

目录 一、前言 二、load 命令使用 2.1 load 概述 2.1.1 load 语法规则 2.1.2 load语法规则重要参数说明 2.2 load 数据加载操作演示 2.2.1 前置准备 2.2.2 加载本地数据 2.2.3 HDFS加载数据 2.2.4 从HDFS加载数据到分区表中并指定分区 2.3 hive3.0 load 命令新特性 …

学习Bootstrap 5的第五天

目录 图像 图像形状 实例 对齐图像 实例 居中图像 实例 响应式图像 实例 Jumbotron 实例 图像 图像形状 .rounded 类可以用于为图像或任何具有边框的元素添加圆角。这个类适用于Bootstrap的所有版本&#xff0c;并且在最新版本中得到了进一步的增强。 实例 <…

【爬虫】7.4. 字体反爬案例分析与爬取实战

字体反爬案例分析与爬取实战 文章目录 字体反爬案例分析与爬取实战1. 案例介绍2. 案例分析3. 爬取 本节来分析一个反爬案例&#xff0c;该案例将真实的数据隐藏到字体文件里&#xff0c;即使我们获取了页面源代码&#xff0c;也无法直接提取数据的真实值。 1. 案例介绍 案例网…

WPF调用Grpc

一、服务端 1、创建proto文件&#xff1a; Protos文件夹 —>右键添加新建项 —> ASP.Net Core下常规中选择"协议缓冲区文件" 2、proto文件协议&#xff1a; 需要使用空参数和空返回值时&#xff0c;需要使用这个协议文件 import “google/protobuf/empty.pr…

windows系统一键开启防火墙并设置开放规则

脚本 echo off REM 设置开放端口&#xff0c;上下行 set PORT1433-1488,33,44REM 关闭防火墙 netsh advfirewall set allprofiles state off REM 打开防火墙 netsh advfirewall set allprofiles state onrem 设置指定端口变量和出入站规则名称 set INPUT_RULE_NAMEZLG-IN set …

Python3.x 文件读写操作

文章目录 一、文件读写操作1、文件的打开方法—open 内建函数&#xff08;1&#xff09;基本语法&#xff08;2&#xff09;参数介绍 2、文件读操作&#xff08;1&#xff09;read 方法 —— 读取文件&#xff08;2&#xff09;文件指针&#xff08;3&#xff09;readline 方法…

一维数组笔试题及其解析

Lei宝啊 &#xff1a;个人主页 愿所有美好不期而遇 前言&#xff1a; 数组名在寻常情况下表示首元素地址&#xff0c;但有两种情况例外&#xff1a; 1.sizeof(数组名)&#xff0c;这里的数组名表示整个数组&#xff0c;计算的是整个数组的大小 2.&数组名&#xff0c;这里的…

AJAX学习笔记7 AJAX实现省市联动

需求:网页上选择对应省份之后,动态的关联出该省份对应的市.选择对应的市之后,动态的关联出该市对应的区 关于省市区全国三级Mysql数据&#xff1a;全国省市区三级地区MySQL数据_biubiubiu0706的博客-CSDN博客 页面加载完毕显示所有省份 <!DOCTYPE html> <html lang&…

行业追踪,2023-09-05

自动复盘 2023-09-05 凡所有相&#xff0c;皆是虚妄。若见诸相非相&#xff0c;即见如来。 k 线图是最好的老师&#xff0c;每天持续发布板块的rps排名&#xff0c;追踪板块&#xff0c;板块来开仓&#xff0c;板块去清仓&#xff0c;丢弃自以为是的想法&#xff0c;板块去留让…

Win11查看安装的Python路径及安装的库

Win11查看安装的Python路径及安装的库 anaconda3最新版安装|使用详情|Error: Please select a valid Python interpreter 一: python安装路径查看 1.1: windows键R 打开cmd窗口&#xff1a;输入python 然后输入import sys ; sys.path; 如下图所示即可查看安装的位置; 退…

同步FIFO的verilog实现(1)——计数法

一、FIFO概述 1、FIFO的定义 FIFO是英文First-In-First-Out的缩写&#xff0c;是一种先入先出的数据缓冲器&#xff0c;与一般的存储器的区别在于没有地址线&#xff0c; 使用起来简单&#xff0c;缺点是只能顺序读写数据&#xff0c;其数据地址由内部读写指针自动加1完成&…

Flex 布局详解

Flex布局的概念与基础概况 Flex布局又称弹性布局。它使用Flex Box使得容器有了弹性&#xff0c;更加适应不同的设备的不同高度&#xff0c;而不必依赖于传统的块状布局和浮动布局。它是css3中新增的规范&#xff0c;目前主流的浏览器都已支持。但flex不支持IE9及以下版本。 F…

uniapp 解决跨域的问题

uniapp 解决跨域的问题 我真的是个 沙雕 找对了解决办法 写错了地方 "h5" : {"devServer" : {"disableHostCheck" : true,"https": false,"proxy" : {"/app" : {"target" : "https://192.16…

使用IntelliJ IDEA本地启动调试Flink流计算工程的2个异常解决

记录&#xff1a;471 场景&#xff1a;使用IntelliJ IDEA本地启动调试Flink流计算时&#xff0c;报错一&#xff1a;加载DataStream报错java.lang.ClassNotFoundException。报错二&#xff1a;No ExecutorFactory found to execute the application。 版本&#xff1a;JDK 1.…