在前一篇文章 RecyclerView 源码分析(一) —— 绘制流程解析 介绍了 RecyclerView 的绘制流程,RecyclerView 通过将绘制流程从 View 中抽取出来,放到 LayoutManager 中,使得 RecyclerView 在不同的 LayoutManager 中,拥有不同的样式,使得 RecyclerView 异常灵活,大大加强了 RecyclerView 使用场景。

当然,RecyclerView 的缓存机制也是它特有的一个优点,减少了对内存的占用以及重复的绘制工作,因此,本文意在介绍和学习 RecyclerView 的缓存设计思想。

当我们在讨论混存的时候,一定会经历创建-缓存-复用的过程。因此对于 RecyclerView 的缓存机制也是按照如下的步骤进行。

创建 ViewHolder(VH)

在讲到对子 itemView 测量的时候,layoutChunk 方法中会先获得每一个 itemView,在获取后,在将其添加到 RecyclerView 中。所以我们先来看看创建的过程:

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

next 就是调用 RecyclerView 的 getViewForPosition 方法来获取一个 View 的。而 getViewForPosition 方法最终会调用到 RecyclerView tryGetViewHolderForPositionByDeadline 方法。

tryGetViewHolderForPositionByDeadline

这个方法很长,但是其实逻辑很简单,整个过程前面部分是先从缓存尝试获取 VH,如果找不到,就会创建新的 VH,然后绑定数据,最后将再将 VH 绑定到 LayoutParams (LP) 上。

        ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
if (position < 0 || position >= mState.getItemCount()) {
throw new IndexOutOfBoundsException("Invalid item position " + position
+ "(" + position + "). Item count:" + mState.getItemCount()
+ exceptionLabel());
}
boolean fromScrapOrHiddenOrCache = false;
ViewHolder holder = null;
// 省略从缓存查找 VH 的逻辑,下面是如果还是没找到,就会创建一个新的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;
}
            // 创建 VH
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");
}
}
} // This is very ugly but the only place we can grab this information
// before the View is rebound and returned to the LayoutManager for post layout ops.
// We don't need this in pre-layout since the VH is not updated by the LM.
if (fromScrapOrHiddenOrCache && !mState.isPreLayout() && holder
.hasAnyOfTheFlags(ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST)) {
holder.setFlags(0, ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
if (mState.mRunSimpleAnimations) {
int changeFlags = ItemAnimator
.buildAdapterChangeFlagsForAnimations(holder);
changeFlags |= ItemAnimator.FLAG_APPEARED_IN_PRE_LAYOUT;
final ItemHolderInfo info = mItemAnimator.recordPreLayoutInformation(mState,
holder, changeFlags, holder.getUnmodifiedPayloads());
recordAnimationInfoIfBouncedHiddenView(holder, info);
}
} boolean bound = false;
if (mState.isPreLayout() && holder.isBound()) {
// do not update unless we absolutely have to.
holder.mPreLayoutPosition = position;
} else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
if (DEBUG && holder.isRemoved()) {
throw new IllegalStateException("Removed holder should be bound and it should"
+ " come here only in pre-layout. Holder: " + holder
+ exceptionLabel());
}
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
          // 进行数据绑定
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
} final ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();
final LayoutParams rvLayoutParams;
       // 下面逻辑就是将 VH 绑定到 LP, LP 又设置到 ItemView 上
if (lp == null) {
rvLayoutParams = (LayoutParams) generateDefaultLayoutParams();
holder.itemView.setLayoutParams(rvLayoutParams);
} else if (!checkLayoutParams(lp)) {
rvLayoutParams = (LayoutParams) generateLayoutParams(lp);
holder.itemView.setLayoutParams(rvLayoutParams);
} else {
rvLayoutParams = (LayoutParams) lp;
}
rvLayoutParams.mViewHolder = holder;
rvLayoutParams.mPendingInvalidate = fromScrapOrHiddenOrCache && bound;
return holder;
}

即使省略了中间从缓存查找 VH 的逻辑,剩下部分的代码还是很长。那我再概括下 tryGetViewHolderForPositionByDeadline 方法所做的事:

  1. 从缓存查找 VH ;

  2. 缓存没有,那么就创建一个 VH;

  3. 判断 VH 需不需要更新数据,如果需要就会调用 tryBindViewHolderByDeadline 绑定数据;

  4. 将 VH 绑定到 LP, LP 又设置到 ItemView 上,互相依赖;

到这里关于创建 VH 的逻辑就讲完了。

缓存

在介绍添加到缓存的逻辑时,还是需要介绍缓存相关的类和变量。

缓存整体设计

由图可知,RecyclerView 缓存是一个四级缓存的架构。当然,从 RecyclerView 的代码注释来看,官方认为只有三级缓存,即mCachedViews是一级缓存,mViewCacheExtension 是二级缓存,mRecyclerPool 是三级缓存。从开发者的角度来看,mAttachedScrap 和 mChangedScrap 对开发者是不透明的,官方并未暴露出任何可以改变他们行为的方法。

缓存机制 Recycler 详解

Recycler 是 RecyclerView 的一个内部类。我们来看一下它的主要的成员变量。

  • mAttachedScrap 缓存屏幕中可见范围的 ViewHolder

    final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();

  • mChangedScrap 缓存滑动时即将与 RecyclerView 分离的ViewHolder,按子View的position或id缓存,默认最多存放2个

    ArrayList<ViewHolder> mChangedScrap = null;

  • mCachedViews  ViewHolder 缓存列表,其大小由 mViewCacheMax 决定,默认 DEFAULT_CACHE_SIZE 为 2,可动态设置。

    final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();

  • ViewCacheExtension 开发者可自定义的一层缓存,是虚拟类 ViewCacheExtension 的一个实例,开发者可实现方法 getViewForPositionAndType(Recycler recycler, int position, int type) 来实现自己的缓存。

    private ViewCacheExtension mViewCacheExtension;

  • RecycledViewPool ViewHolder 缓存池,在有限的 mCachedViews 中如果存不下 ViewHolder 时,就会把 ViewHolder 存入 RecyclerViewPool 中。

    RecycledViewPool mRecyclerPool;

添加到缓存

VH 被创建之后,是要被缓存,然后重复利用的,那么他们是什么时候被添加到缓存的呢?此处还是以 LinearLayoutManager 举例说明。在 RecyclerView 源码分析(一) —— 绘制流程解析 一文中曾提到一个方法:

 public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
     // ...
     detachAndScrapAttachedViews(recycler);
     // ...
}

onLayoutChildren 是对子 view 进行绘制。在对子 view 会先调用 detachAndScrapAttachedViews 方法,下面来看看这个方法。

detachAndScrapAttachedViews

下面来看下这个方法:

       // recyclerview
public void detachAndScrapAttachedViews(@NonNull Recycler recycler) {
final int childCount = getChildCount();
for (int i = childCount - 1; i >= 0; i--) {
final View v = getChildAt(i);
          // 每个 view 都会放到里面
scrapOrRecycleView(recycler, i, v);
}
} private void scrapOrRecycleView(Recycler recycler, int index, View view) {
final ViewHolder viewHolder = getChildViewHolderInt(view);
if (viewHolder.shouldIgnore()) {
if (DEBUG) {
Log.d(TAG, "ignoring view " + viewHolder);
}
return;
}
        // 如果 VH 无效,并且已经被移除了,就会走另一个逻辑
if (viewHolder.isInvalid() && !viewHolder.isRemoved()
&& !mRecyclerView.mAdapter.hasStableIds()) {
removeViewAt(index);
recycler.recycleViewHolderInternal(viewHolder);
} else {
          // 先 detch 掉,然后放入缓存中
detachViewAt(index);
recycler.scrapView(view);
mRecyclerView.mViewInfoStore.onViewDetached(viewHolder);
}
}

也就是在上面的逻辑里,被放到缓存中。这里就可以看到

  1. 如果是 remove,会执行 recycleViewHolderInternal(viewHolder) 方法,而这个方法最终会将 ViewHolder 加入 CacheView 和 Pool 中,

  2. 而当是 Detach,会将 View 加入到 ScrapViews 中

需要指出的一点是:需要区分两个概念,Detach 和 Remove

  1. detach: 在 ViewGroup 中的实现很简单,只是将 ChildView 从 ParentView 的 ChildView 数组中移除,ChildView 的 mParent 设置为 null, 可以理解为轻量级的临时 remove, 因为 View此时和 View 树还是藕断丝连, 这个函数被经常用来改变 ChildView 在 ChildView 数组中的次序。View 被 detach 一般是临时的,在后面会被重新 attach。

  2. remove: 真正的移除,不光被从 ChildView 数组中除名,其他和 View 树各项联系也会被彻底斩断(不考虑 Animation/LayoutTransition 这种特殊情况), 比如焦点被清除,从TouchTarget 中被移除等。

recycleViewHolderInternal

下面来看 Recycler 两个的具体逻辑方法:

        /**
* internal implementation checks if view is scrapped or attached and throws an exception
* if so.
* Public version un-scraps before calling recycle.
*/
void recycleViewHolderInternal(ViewHolder holder) {
       // ...省略前面的代码,前面都是在做检验
final boolean transientStatePreventsRecycling = holder
.doesTransientStatePreventRecycling();
@SuppressWarnings("unchecked")
final boolean forceRecycle = mAdapter != null
&& transientStatePreventsRecycling
&& mAdapter.onFailedToRecycleView(holder);
boolean cached = false;
boolean recycled = false;
if (DEBUG && mCachedViews.contains(holder)) {
throw new IllegalArgumentException("cached view received recycle internal? "
+ holder + exceptionLabel());
}
if (forceRecycle || holder.isRecyclable()) {
if (mViewCacheMax > 0
&& !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
| ViewHolder.FLAG_REMOVED
| ViewHolder.FLAG_UPDATE
| ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
// Retire oldest cached view 如果缓存数量超了,就会先移除最先加入的
int cachedViewSize = mCachedViews.size();
if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
recycleCachedViewAt(0);
cachedViewSize--;
} int targetCacheIndex = cachedViewSize;
if (ALLOW_THREAD_GAP_WORK
&& cachedViewSize > 0
&& !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {
// when adding the view, skip past most recently prefetched views
int cacheIndex = cachedViewSize - 1;
while (cacheIndex >= 0) {
int cachedPos = mCachedViews.get(cacheIndex).mPosition;
if (!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) {
break;
}
cacheIndex--;
}
targetCacheIndex = cacheIndex + 1;
}
            // 添加到缓存
mCachedViews.add(targetCacheIndex, holder);
cached = true;
}
if (!cached) {
addViewHolderToRecycledViewPool(holder, true);
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);
if (!cached && !recycled && transientStatePreventsRecycling) {
holder.mOwnerRecyclerView = null;
}
}

该方法所做的事具体如下:

  1. 检验该 VH 的有效性,确保已经不再被使用;

  2. 判断缓存的容量,超了就会进行移除,然后找一个合适的位置进行添加。

  3. 如果不能加入到 CacheViews 中,则加入到 Pool 中。

mCachedViews

mCachedViews 对应的数据结构也是 ArrayList 但是该缓存对集合的大小是有限制的,默认是 2。该缓存中 ViewHolder 的特性和 mAttachedScrap 中的特性是一样的,只要 position或者 itemId 对应上了,那么它就是干净的,无需重新绑定数据。开发者可以调用 setItemViewCacheSize(size) 方法来改变缓存的大小。该层级缓存触发的一个常见的场景是滑动 RV。当然 notifyXXX 也会触发该缓存。该缓存和 mAttachedScrap 一样特别高效。

RecyclerViewPool

RecyclerViewPool 缓存可以针对多ItemType,设置缓存大小。默认每个 ItemType 的缓存个数是 5。而且该缓存可以给多个 RecyclerView 共享。由于默认缓存个数为 5,假设某个新闻 App,每屏幕可以展示 10 条新闻,那么必然会导致缓存命中失败,频繁导致创建 ViewHolder 影响性能。所以需要扩大缓存size。

scrapView

接下去看 scrapView 这个方法:

        void scrapView(View view) {
final ViewHolder holder = getChildViewHolderInt(view);
if (holder.hasAnyOfTheFlags(ViewHolder.FLAG_REMOVED | ViewHolder.FLAG_INVALID)
|| !holder.isUpdated() || canReuseUpdatedViewHolder(holder)) {
if (holder.isInvalid() && !holder.isRemoved() && !mAdapter.hasStableIds()) {
throw new IllegalArgumentException("Called scrap view with an invalid view."
+ " Invalid views cannot be reused from scrap, they should rebound from"
+ " recycler pool." + exceptionLabel());
}
holder.setScrapContainer(this, false); // 这里的 false
mAttachedScrap.add(holder);
} else {
if (mChangedScrap == null) {
mChangedScrap = new ArrayList<ViewHolder>();
}
holder.setScrapContainer(this, true); // 这里是 true
mChangedScrap.add(holder);
}
}

该方法就比较简单了,没有那么多需要检验的逻辑。这里根据条件,有两种缓存类型可以选择,具体就不展开了,大家都可以看懂。这里讲解下两个 scrapView 的缓存。

mAttachedScrap

mAttachedScrap 的对应数据结构是ArrayList,在 LayoutManager#onLayoutChildren 方法中,对 views 进行布局时,会将 RecyclerView 上的 Views 全部暂存到该集合中,以备后续使用,该缓存中的 ViewHolder 的特性是,如果和 RV 上的 position 或者 itemId 匹配上了,那么认为是干净的 ViewHolder,是可以直接拿出来使用的,无需调用 onBindViewHolder 方法。该 ArrayList 的大小是没有限制的,屏幕上有多少个 View,就会创建多大的集合。

触发该层级缓存的场景一般是调用 notifyItemXXX 方法。调用 notifyDataSetChanged 方法,只有当 Adapter hasStableIds 返回 true,会触发该层级的缓存使用。

mChangedScrap

mChangedScrap 和 mAttachedScrap 是同一级的缓存,他们是平等的。但是mChangedScrap的调用场景是notifyItemChanged和notifyItemRangeChanged,只有发生变化的ViewHolder才会放入到 mChangedScrap 中。mChangedScrap缓存中的ViewHolder是需要调用onBindViewHolder方法重新绑定数据的。那么此时就有个问题了,为什么同一级别的缓存需要设计两个不同的缓存?

在 dispatchLayoutStep2 阶段 LayoutManager onLayoutChildren方法中最终会调用 layoutForPredictiveAnimations 方法,把 mAttachedScrap 中剩余的 ViewHolder 填充到屏幕上,所以他们的区别就是,mChangedScrap 中的 ViewHolder 在 RV 填充满的情况下,是不会强行填充到 RV 上的。那么有办法可以让发生改变的 ViewHolder 进入 mAttachedScrap 缓存吗?当然可以。调用 notifyItemChanged(int position, Object payload) 方法可以,实现局部刷新功能,payload 不为空,那么发生改变的 ViewHolder 是会被分离到 mAttachedScrap 中的。

使用缓存

下面进入到最后一节,使用缓存。这个在之前绘制篇幅也有提到,下面直接看对应的方法:

//根据传入的position获取ViewHolder
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
---------省略----------
boolean fromScrapOrHiddenOrCache = false;
ViewHolder holder = null;
//预布局 属于特殊情况 从mChangedScrap中获取ViewHolder
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
}
if (holder == null) {
//1、尝试从mAttachedScrap中获取ViewHolder,此时获取的是屏幕中可见范围中的ViewHolder
//2、mAttachedScrap缓存中没有的话,继续从mCachedViews尝试获取ViewHolder
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
----------省略----------
}
if (holder == null) {
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
---------省略----------
final int type = mAdapter.getItemViewType(offsetPosition);
//如果Adapter中声明了Id,尝试从id中获取,这里不属于缓存
if (mAdapter.hasStableIds()) {
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
type, dryRun);
}
if (holder == null && mViewCacheExtension != null) {
3、从自定义缓存mViewCacheExtension中尝试获取ViewHolder,该缓存需要开发者实现
final View view = mViewCacheExtension
.getViewForPositionAndType(this, position, type);
if (view != null) {
holder = getChildViewHolder(view);
}
}
if (holder == null) { // fallback to pool
//4、从缓存池mRecyclerPool中尝试获取ViewHolder
holder = getRecycledViewPool().getRecycledView(type);
if (holder != null) {
//如果获取成功,会重置ViewHolder状态,所以需要重新执行Adapter#onBindViewHolder绑定数据
holder.resetInternal();
if (FORCE_INVALIDATE_DISPLAY_LIST) {
invalidateDisplayListInt(holder);
}
}
}
if (holder == null) {
---------省略----------
//5、若以上缓存中都没有找到对应的ViewHolder,最终会调用Adapter中的onCreateViewHolder创建一个
holder = mAdapter.createViewHolder(RecyclerView.this, type);
}
} boolean bound = false;
if (mState.isPreLayout() && holder.isBound()) {
holder.mPreLayoutPosition = position;
} else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
//6、如果需要绑定数据,会调用Adapter#onBindViewHolder来绑定数据
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}
----------省略----------
return holder;
}

上述逻辑用流程图表示:

总结一下上述流程:通过 mAttachedScrap、mCachedViews 及 mViewCacheExtension 获取的 ViewHolder 不需要重新创建布局及绑定数据;通过缓存池 mRecyclerPool 获取的 ViewHolder不需要重新创建布局,但是需要重新绑定数据;如果上述缓存中都没有获取到目标 ViewHolder,那么就会回调 Adapter#onCreateViewHolder 创建布局,以及回调 Adapter#onBindViewHolder来绑定数据。

ViewCacheExtension

我们已经知道 ViewCacheExtension 属于第三级缓存,需要开发者自行实现,那么 ViewCacheExtension 在什么场景下使用?又是如何实现的呢?

首先我们要明确一点,那就是 Recycler 本身已经设置了好几级缓存了,为什么还要留个接口让开发者去自行实现缓存呢?

关于这一点,来看看 Recycler 中的其他缓存:

  1. mAttachedScrap 用来处理可见屏幕的缓存;

  2. mCachedViews 里存储的数据虽然是根据 position 来缓存,但是里面的数据随时可能会被替换的;

  3. mRecyclerPool 里按 viewType 去存储 ArrayList< ViewHolder>,所以 mRecyclerPool 并不能按 position 去存储 ViewHolder,而且从 mRecyclerPool 取出的 View 每次都要去走 Adapter#onBindViewHolder 去重新绑定数据。

假如我现在需要在一个特定的位置(比如 position=0 位置)一直展示某个 View,且里面的内容是不变的,那么最好的情况就是在特定位置时,既不需要每次重新创建 View,也不需要每次都去重新绑定数据,上面的几种缓存显然都是不适用的,这种情况该怎么办呢?可以通过自定义缓存 ViewCacheExtension 实现上述需求。

RecyclerView & ListView缓存机制对比

结论援引自:Android ListView 与 RecyclerView 对比浅析--缓存机制

ListView和RecyclerView缓存机制基本一致:

  1. mActiveViews 和 mAttachedScrap 功能相似,意义在于快速重用屏幕上可见的列表项ItemView,而不需要重新 createView 和 bindView;

  2. mScrapView 和 mCachedViews + mReyclerViewPool功能相似,意义在于缓存离开屏幕的 ItemView,目的是让即将进入屏幕的 ItemView 重用.

  3. RecyclerView 的优势在于

    1. mCacheViews 的使用,可以做到屏幕外的列表项 ItemView 进入屏幕内时也无须bindView快速重用;

    2. mRecyclerPool 可以供多个 RecyclerView 共同使用,在特定场景下,如 viewpaper+ 多个列表页下有优势.客观来说,RecyclerView 在特定场景下对 ListView 的缓存机制做了补强和完善。

不同使用场景:列表页展示界面,需要支持动画,或者频繁更新,局部刷新,建议使用 RecyclerView,更加强大完善,易扩展;其它情况(如微信卡包列表页)两者都OK,但ListView在使用上会更加方便,快捷。

 

参考文章

https://www.jianshu.com/p/2b19e9bcda84

https://www.jianshu.com/p/6e6bf58b7f0d

https://www.jianshu.com/p/e1b257484961

RecyclerView加载了那么多图,为什么就是不崩呢?

RecyclerView 源码分析(二) —— 缓存机制的更多相关文章

  1. jQuery1.9.1源码分析--数据缓存Data模块

    jQuery1.9.1源码分析--数据缓存Data模块 阅读目录 jQuery API中Data的基本使用方法介绍 jQuery.acceptData(elem)源码分析 jQuery.data(el ...

  2. Fresco 源码分析(二) Fresco客户端与服务端交互(1) 解决遗留的Q1问题

    4.2 Fresco客户端与服务端的交互(一) 解决Q1问题 从这篇博客开始,我们开始讨论客户端与服务端是如何交互的,这个交互的入口,我们从Q1问题入手(博客按照这样的问题入手,是因为当时我也是从这里 ...

  3. lodash源码分析之缓存使用方式的进一步封装

    在世界上所有的民族之中,支配着他们的喜怒选择的并不是天性,而是他们的观点. --卢梭<社会与契约论> 本文为读 lodash 源码的第九篇,后续文章会更新到这个仓库中,欢迎 star:po ...

  4. 【转】MaBatis学习---源码分析MyBatis缓存原理

    [原文]https://www.toutiao.com/i6594029178964673027/ 源码分析MyBatis缓存原理 1.简介 在 Web 应用中,缓存是必不可少的组件.通常我们都会用 ...

  5. 框架-springmvc源码分析(二)

    框架-springmvc源码分析(二) 参考: http://www.cnblogs.com/leftthen/p/5207787.html http://www.cnblogs.com/leftth ...

  6. kernel 3.10内核源码分析--hung task机制

    kernel 3.10内核源码分析--hung task机制 一.相关知识: 长期以来,处于D状态(TASK_UNINTERRUPTIBLE状态)的进程 都是让人比较烦恼的问题,处于D状态的进程不能接 ...

  7. 十、Spring之BeanFactory源码分析(二)

    Spring之BeanFactory源码分析(二) 前言 在前面我们简单的分析了BeanFactory的结构,ListableBeanFactory,HierarchicalBeanFactory,A ...

  8. Vue源码分析(二) : Vue实例挂载

    Vue源码分析(二) : Vue实例挂载 author: @TiffanysBear 实例挂载主要是 $mount 方法的实现,在 src/platforms/web/entry-runtime-wi ...

  9. 多线程之美8一 AbstractQueuedSynchronizer源码分析&lt;二&gt;

    目录 AQS的源码分析 该篇主要分析AQS的ConditionObject,是AQS的内部类,实现等待通知机制. 1.条件队列 条件队列与AQS中的同步队列有所不同,结构图如下: 两者区别: 1.链表 ...

  10. Tomcat源码分析二:先看看Tomcat的整体架构

    Tomcat源码分析二:先看看Tomcat的整体架构 Tomcat架构图 我们先来看一张比较经典的Tomcat架构图: 从这张图中,我们可以看出Tomcat中含有Server.Service.Conn ...

随机推荐

  1. Javascript高级程序设计——基本类型和引用类型的值

    ECMAScript中的变量有两种不同的数据类型的值: 基本类型:基本类型的值是简单的数据段.包括:Undefined.Null.Number.Boolean.String五种 引用类型:引用类型的值 ...

  2. mysql replace into用法与坑

    需要主键一致 PRIMARY KEY (id) PRIMARY KEY (id, ts) 坑: https://blog.xupeng.me/2013/10/11/mysql-replace-into ...

  3. Edit Box多行显示时如何使滚动条始终在下方

    两种方法: ①  CEdit *pEdit = ((CEdit*)GetDlgItem(IDC_EDIT_RXDATA)); pEdit->LineScroll(pEdit->GetLin ...

  4. SecureCRT for Linux突破30天使用限制

    当然还有一种方法,就是当你试用点i agree到时候,在~/.vandyke/Config 会生成一个文件SecureCRT_eval.lic,删除以后就可以恢复30天试用

  5. 主题模型(概率潜语义分析PLSA、隐含狄利克雷分布LDA)

    一.pLSA模型 1.朴素贝叶斯的分析 (1)可以胜任许多文本分类问题.(2)无法解决语料中一词多义和多词一义的问题--它更像是词法分析,而非语义分析.(3)如果使用词向量作为文档的特征,一词多义和多 ...

  6. JavaScript中的 原型 property 构造函数 和实例对象之间的关系

    1 为什么要使用原型? /* * javascript当中 原型 prototype 对象 * * */ //首先引入 prototype的意义,为什么要使用这个对象 //先来写一个构造函数的面向对象 ...

  7. 超详细动手搭建一个Vuepress站点及开启PWA与自动部署

    超详细动手搭建一个Vuepress站点及开启PWA与自动部署 五一之前就想写一篇关于Vuepress的文章,结果朋友结婚就不了了之了. 记得最后一定要看注意事项! Vuepress介绍 官网:http ...

  8. TensorFlow (RNN)深度学习 双向LSTM(BiLSTM)+CRF 实现 sequence labeling 序列标注问题 源码下载

    http://blog.csdn.net/scotfield_msn/article/details/60339415 在TensorFlow (RNN)深度学习下 双向LSTM(BiLSTM)+CR ...

  9. 关于ARM中的tst、cmp、bne、beq指令

    一.关于cmp的详细用法 假设现在AX寄存器中的数是0002H,BX寄存器中的数是0003H. 执行的指令是:CMP AX, BX 执行这条指令时,先做用AX中的数减去BX中的数的减法运算. 列出二进 ...

  10. Fanvas是一个把swf转为html5 canvas动画的系统

      https://github.com/Tencent/Fanvas   使用方法:     代码: <!DOCTYPE html> <html> <head> ...