【Android】RecyclerView回收复用机制

概述

RecyclerView 是 Android 中用于高效显示大量数据的视图组件,它是 ListView 的升级版本,支持更灵活的布局和功能。

我们创建一个RecyclerView的Adapter:

public class MyRecyclerView extends RecyclerView.Adapter<MyRecyclerView.MyHolder> {private List<String> strings;private Context context;public MyRecyclerView(List<String> strings, Context context) {this.strings = strings;this.context = context;}@NonNull@Overridepublic MyRecyclerView.MyHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {View view = LayoutInflater.from(parent.getContext()).inflate(android.R.layout.simple_list_item_1, parent, false);MyRecyclerView.MyHolder viewHolder = new MyHolder(view);Log.d("MyRecyclerView", "onCreateViewHolder: ");return viewHolder;}@Overridepublic void onBindViewHolder(@NonNull MyRecyclerView.MyHolder holder, int position) {holder.textView.setText("第" + position + "项");Log.d("MyRecyclerView", "onBindViewHolder: " + position);}@Overridepublic int getItemCount() {return strings == null ? 0 : strings.size();}public class MyHolder extends RecyclerView.ViewHolder {private TextView textView;public MyHolder(@NonNull View itemView) {super(itemView);textView = itemView.findViewById(android.R.id.text1);}}
}

我们在onCreateViewHolder和onBindViewHolder都打印log。

onCreateViewHolder()会在创建一个新view的时候调用,onBindViewHolder()会在已存在view,绑定数据的时候调用。

我们来看一下运行时打印的log:

image-20241119212232289

在最开始加载view的时候,两个方法onCreateViewHolder()onBindViewHolder()都执行了,但是当我们上下滑动RecyclerView的时候,我们会发现只执行了onBindViewHolder()方法。所以说,RecyclerView并不是会一直重新创建View,而是会对view进行复用。

复用机制

当我们想去通过看源码去了解缓存复用机制的时候,我们要去想看源码的入口在哪里。上文我们提到是在滑动RecyclerView的时候进行了缓存复用,所以我们会想到去看 onTouchEvent 这个方法:

@Override
public boolean onTouchEvent(MotionEvent e) {...case MotionEvent.ACTION_MOVE: {final int index = e.findPointerIndex(mScrollPointerId);if (index < 0) {Log.e(TAG, "Error processing scroll; pointer index for id "+ mScrollPointerId + " not found. Did any MotionEvents get skipped?");return false;}final int x = (int) (e.getX(index) + 0.5f);final int y = (int) (e.getY(index) + 0.5f);int dx = mLastTouchX - x;int dy = mLastTouchY - y;if (mScrollState != SCROLL_STATE_DRAGGING) {boolean startScroll = false;if (canScrollHorizontally) {if (dx > 0) {dx = Math.max(0, dx - mTouchSlop);} else {dx = Math.min(0, dx + mTouchSlop);}if (dx != 0) {startScroll = true;}}if (canScrollVertically) {if (dy > 0) {dy = Math.max(0, dy - mTouchSlop);} else {dy = Math.min(0, dy + mTouchSlop);}if (dy != 0) {startScroll = true;}}if (startScroll) {setScrollState(SCROLL_STATE_DRAGGING);}}if (mScrollState == SCROLL_STATE_DRAGGING) {mReusableIntPair[0] = 0;mReusableIntPair[1] = 0;if (dispatchNestedPreScroll(canScrollHorizontally ? dx : 0,canScrollVertically ? dy : 0,mReusableIntPair, mScrollOffset, TYPE_TOUCH)) {dx -= mReusableIntPair[0];dy -= mReusableIntPair[1];// Updated the nested offsetsmNestedOffsets[0] += mScrollOffset[0];mNestedOffsets[1] += mScrollOffset[1];// Scroll has initiated, prevent parents from interceptinggetParent().requestDisallowInterceptTouchEvent(true);}mLastTouchX = x - mScrollOffset[0];mLastTouchY = y - mScrollOffset[1];if (scrollByInternal(canScrollHorizontally ? dx : 0,canScrollVertically ? dy : 0,e)) {getParent().requestDisallowInterceptTouchEvent(true);}if (mGapWorker != null && (dx != 0 || dy != 0)) {mGapWorker.postFromTraversal(this, dx, dy);}}} break;...return true;
}

case:MotionEvent.ACTION_MOVE里有 scrollByInternal() 这个方法:

boolean scrollByInternal(int x, int y, MotionEvent ev) {int unconsumedX = 0;int unconsumedY = 0;int consumedX = 0;int consumedY = 0;consumePendingUpdateOperations();if (mAdapter != null) {mReusableIntPair[0] = 0;mReusableIntPair[1] = 0;scrollStep(x, y, mReusableIntPair);consumedX = mReusableIntPair[0];consumedY = mReusableIntPair[1];unconsumedX = x - consumedX;unconsumedY = y - consumedY;}if (!mItemDecorations.isEmpty()) {invalidate();}mReusableIntPair[0] = 0;mReusableIntPair[1] = 0;dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY, mScrollOffset,TYPE_TOUCH, mReusableIntPair);unconsumedX -= mReusableIntPair[0];unconsumedY -= mReusableIntPair[1];boolean consumedNestedScroll = mReusableIntPair[0] != 0 || mReusableIntPair[1] != 0;// Update the last touch co-ords, taking any scroll offset into accountmLastTouchX -= mScrollOffset[0];mLastTouchY -= mScrollOffset[1];mNestedOffsets[0] += mScrollOffset[0];mNestedOffsets[1] += mScrollOffset[1];if (getOverScrollMode() != View.OVER_SCROLL_NEVER) {if (ev != null && !MotionEventCompat.isFromSource(ev, InputDevice.SOURCE_MOUSE)) {pullGlows(ev.getX(), unconsumedX, ev.getY(), unconsumedY);}considerReleasingGlowsOnScroll(x, y);}if (consumedX != 0 || consumedY != 0) {dispatchOnScrolled(consumedX, consumedY);}if (!awakenScrollBars()) {invalidate();}return consumedNestedScroll || consumedX != 0 || consumedY != 0;
}

里面的 scrollStep() 方法:

void scrollStep(int dx, int dy, @Nullable int[] consumed) {startInterceptRequestLayout();onEnterLayoutOrScroll();TraceCompat.beginSection(TRACE_SCROLL_TAG);fillRemainingScrollValues(mState);int consumedX = 0;int consumedY = 0;if (dx != 0) {consumedX = mLayout.scrollHorizontallyBy(dx, mRecycler, mState);}if (dy != 0) {consumedY = mLayout.scrollVerticallyBy(dy, mRecycler, mState);}TraceCompat.endSection();repositionShadowingViews();onExitLayoutOrScroll();stopInterceptRequestLayout(false);if (consumed != null) {consumed[0] = consumedX;consumed[1] = consumedY;}
}

scrollHorizontallyByscrollVerticallyBy

@Override
public int scrollHorizontallyBy(int dx, RecyclerView.Recycler recycler,RecyclerView.State state) {if (mOrientation == VERTICAL) {return 0;}return scrollBy(dx, recycler, state);
}
@Override
public int scrollVerticallyBy(int dy, RecyclerView.Recycler recycler,RecyclerView.State state) {if (mOrientation == HORIZONTAL) {return 0;}return scrollBy(dy, recycler, state);
}

两个都执行的 scrollBy

int scrollBy(int delta, RecyclerView.Recycler recycler, RecyclerView.State state) {if (getChildCount() == 0 || delta == 0) {return 0;}ensureLayoutState();mLayoutState.mRecycle = true;final int layoutDirection = delta > 0 ? LinearLayoutManager.LayoutState.LAYOUT_END : LinearLayoutManager.LayoutState.LAYOUT_START;final int absDelta = Math.abs(delta);updateLayoutState(layoutDirection, absDelta, true, state);final int consumed = mLayoutState.mScrollingOffset+ fill(recycler, mLayoutState, state, false);if (consumed < 0) {if (DEBUG) {Log.d(TAG, "Don't have any more elements to scroll");}return 0;}final int scrolled = absDelta > consumed ? layoutDirection * consumed : delta;mOrientationHelper.offsetChildren(-scrolled);if (DEBUG) {Log.d(TAG, "scroll req: " + delta + " scrolled: " + scrolled);}mLayoutState.mLastScrollDelta = scrolled;return scrolled;
}

里面的 fill 方法最为关键:

int fill(RecyclerView.Recycler recycler, LinearLayoutManager.LayoutState layoutState,RecyclerView.State state, boolean stopOnFocusable) {...while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {layoutChunkResult.resetInternal();if (RecyclerView.VERBOSE_TRACING) {TraceCompat.beginSection("LLM LayoutChunk");}layoutChunk(recycler, state, layoutState, layoutChunkResult);if (RecyclerView.VERBOSE_TRACING) {TraceCompat.endSection();}if (layoutChunkResult.mFinished) {break;}layoutState.mOffset += layoutChunkResult.mConsumed * layoutState.mLayoutDirection;/*** Consume the available space if:* * layoutChunk did not request to be ignored* * OR we are laying out scrap children* * OR we are not doing pre-layout*/if (!layoutChunkResult.mIgnoreConsumed || layoutState.mScrapList != null|| !state.isPreLayout()) {layoutState.mAvailable -= layoutChunkResult.mConsumed;// we keep a separate remaining space because mAvailable is important for recyclingremainingSpace -= layoutChunkResult.mConsumed;}if (layoutState.mScrollingOffset != LinearLayoutManager.LayoutState.SCROLLING_OFFSET_NaN) {layoutState.mScrollingOffset += layoutChunkResult.mConsumed;if (layoutState.mAvailable < 0) {layoutState.mScrollingOffset += layoutState.mAvailable;}recycleByLayoutState(recycler, layoutState);}if (stopOnFocusable && layoutChunkResult.mFocusable) {break;}}if (DEBUG) {validateChildOrder();}return start - layoutState.mAvailable;
}

这个方法功能是填充给定的布局,通过while循环不断进行填充,其中的 layoutChunk() 方法:

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,LayoutState layoutState, LayoutChunkResult result) {View view = layoutState.next(recycler);//获取下一项需要布局的视图if (view == null) {if (DEBUG && layoutState.mScrapList == null) {throw new RuntimeException("received null view when unexpected");}// if we are laying out views in scrap, this may return null which means there is// no more items to layout.result.mFinished = true;return;}RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) view.getLayoutParams();if (layoutState.mScrapList == null) {if (mShouldReverseLayout == (layoutState.mLayoutDirection== LayoutState.LAYOUT_START)) {addView(view);//将视图添加到布局的末尾} else {addView(view, 0);//将视图添加到布局的开头}} else {if (mShouldReverseLayout == (layoutState.mLayoutDirection== LayoutState.LAYOUT_START)) {addDisappearingView(view);} else {addDisappearingView(view, 0);}}...
}

依次点击:

image-20241120203800209

image-20241120204024167

image-20241120204140384

最后我们就找到了回收复用的最关键的代码。

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;// 0) If there is a changed scrap, try to find from thereif (mState.isPreLayout()) {holder = getChangedScrapViewForPosition(position);fromScrapOrHiddenOrCache = holder != null;}// 1) Find by position from scrap/hidden list/cacheif (holder == null) {holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);if (holder != null) {if (!validateViewHolderForOffsetPosition(holder)) {// recycle holder (and unscrap if relevant) since it can't be usedif (!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;}}}if (holder == null) {final int offsetPosition = mAdapterHelper.findPositionOffset(position);if (offsetPosition < 0 || offsetPosition >= mAdapter.getItemCount()) {throw new IndexOutOfBoundsException("Inconsistency detected. Invalid item "+ "position " + position + "(offset:" + offsetPosition + ")."+ "state:" + mState.getItemCount() + exceptionLabel());}final int type = mAdapter.getItemViewType(offsetPosition);// 2) Find from scrap/cache via stable ids, if existsif (mAdapter.hasStableIds()) {holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),type, dryRun);if (holder != null) {// update positionholder.mPosition = offsetPosition;fromScrapOrHiddenOrCache = true;}}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());}}}if (holder == null) { // fallback to poolif (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);}}}if (holder == null) {long start = getNanoTime();if (deadlineNs != FOREVER_NS&& !mRecyclerPool.willCreateInTime(type, start, deadlineNs)) {// abort - we have a deadline we can't meetreturn null;}holder = mAdapter.createViewHolder(RecyclerView.this, type);if (ALLOW_THREAD_GAP_WORK) {// only bother finding nested RV if prefetchingRecyclerView 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;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;
}

从代码中我们可以看出,复用的并不是一个个控件,而是 ViewHolder(ItemView)

我们可以通过上面代码看出来RecyclerView的复用机制

第一层:Changed Scrap

代码:

if (mState.isPreLayout()) {holder = getChangedScrapViewForPosition(position);fromScrapOrHiddenOrCache = holder != null;
}

解释

  • 如果当前处于预布局(isPreLayout == true),会优先从变更缓存中获取。
  • getChangedScrapViewForPosition(position)查找变更缓存(mChangedScrap),这里保存的是那些被标记为需要更新的ViewHolder
  • 如果找到,直接返回,不需要从其他层中查找。

RecyclerView 的布局过程中,预布局(Pre-Layout)RecyclerView 为支持动画效果(如插入、删除、移动等操作)而执行的一个特殊布局阶段。

以下操作都会触发 预布局阶段,并可能从变更缓存中获取视图来进行后续处理:

  • 插入、移除、范围更新notifyItemInserted, notifyItemRemoved, notifyItemRangeChanged 等)
  • 视图的布局和数据绑定(例如,setAdapter, setHasStableIds
  • 布局管理器或动画的变化setLayoutManager, setItemAnimator
  • 视图的移动notifyItemMoved
  • 所有与动画相关的操作(包括添加、删除、移动动画)

第二层:Scrap/Hidden/Cache

代码:

if (holder == null) {holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);if (holder != null) {if (!validateViewHolderForOffsetPosition(holder)) {if (!dryRun) {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;}}
}

解释

  • 如果第一层缓存未命中,尝试从

    普通缓存层中获取,包括:

    1. Scrap:表示那些视图项暂时不可见但还可以复用的视图,它们是最常见的一种缓存方式。被标记为废弃的视图,在没有被回收之前可以复用。
    2. Hidden:隐藏视图与 Scrap 类似,但它们的存在通常是为了支持更复杂的布局切换、动画等。它们在显示区域外,但仍然保留在缓存中,直到需要重新显示。
    3. CacheRecyclerView 使用缓存来存储那些根据视图 ID 或类型等条件频繁访问的视图。它们通常在视图池中存储较长时间,直到达到缓存容量限制。
  • 调用

    validateViewHolderForOffsetPosition(holder)
    

    检查缓存的有效性:

    • 如果无效(比如位置错位),将其回收。
    • 如果有效,标记fromScrapOrHiddenOrCache = true

第三层:Stable ID Cache

代码:

if (holder == null && mAdapter.hasStableIds()) {holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition), type, dryRun);if (holder != null) {holder.mPosition = offsetPosition;fromScrapOrHiddenOrCache = true;}
}

解释

  • 如果Adapter支持稳定ID(hasStableIds == true),尝试通过稳定ID查找缓存中的ViewHolder
  • 调用getScrapOrCachedViewForId,通过ID获取匹配的ViewHolder
  • 如果找到,将其位置更新为offsetPosition,并标记为来自缓存。

如何启用 Stable ID?

为了使 RecyclerView 使用 Stable ID Cache,必须确保以下两点:

  1. 实现 hasStableIds() 方法: 你需要在你的 RecyclerView.Adapter 中重写 hasStableIds() 方法并返回 true。这是启用 Stable ID Cache 的前提。
  2. 返回稳定的 ID: 在适配器的 getItemId() 方法中为每个项返回唯一的 ID。这个 ID 通常是数据中的唯一标识符(比如数据库中的主键)。

示例代码:

public class MyAdapter extends RecyclerView.Adapter<MyViewHolder> {@Overridepublic boolean hasStableIds() {return true; // 启用 Stable ID}@Overridepublic long getItemId(int position) {// 假设你的数据项有一个唯一的 id 字段return myDataList.get(position).getId();}// 其他适配器方法...
}

或者在构造方法中进行设置:

public MyRecyclerViewAdapter(List<String> strings, Context context) {this.strings = strings;this.context = context;setHasStableIds(true); // 启用稳定 ID
}

第四层:Recycled Pool

代码:

if (holder == null) {holder = getRecycledViewPool().getRecycledView(type);if (holder != null) {holder.resetInternal();}
}

解释

  • Recycled PoolRecyclerView 内部维护的一个缓存池,用于存储已经被回收并不再使用的视图(View)。这些视图通常是已经滑出屏幕或者暂时不可见的视图。通过回收池,RecyclerView 可以避免每次滚动时都重新创建视图,而是将已回收的视图重新利用,从而提升滚动性能。

    回收池的工作机制是,RecyclerView 会在视图不再需要时将它们放入回收池(即已回收的视图池)。当需要新的视图时,RecyclerView 会从回收池中获取一个合适的视图进行重用。

  • 回收池存储的是所有超出缓存数量限制的ViewHolder,按type分类。

  • 如果找到,调用resetInternal()重置其状态。

最后一层:创建新 ViewHolder

代码中涉及的部分:

if (holder == null) {holder = mAdapter.createViewHolder(RecyclerView.this, type);
}

说明

  • 如果所有缓存机制都未找到匹配的 ViewHolder,最终会调用 Adapter.createViewHolder 来创建新的实例。
  • 这是性能代价最高的一步。

我们通过点击

image-20241120215023059

image-20241120215058358

image-20241120215142001

就可以找到我们每次写Adapter都用重写的 onCreateViewHolder方法了。

在后面的代码中,我们依次点击:

image-20241120215850814

image-20241120215912483

image-20241120215934540

image-20241120215952052

image-20241120220013888

就可以找到我们每次写Adapter都用重写的 onBindViewHolder方法了。


已经到底啦!!

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

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

相关文章

Kotlin DSL Gradle 指南

本文是关于 Kotlin DSL Gradle 的指南&#xff08;上篇&#xff09;&#xff0c;介绍了 Gradle 作为 Android 开发构建工具的作用及优势&#xff0c;包括初始配置、生命周期、依赖管理、Task 相关内容。如 Task 的创建、自定义、各种方法和属性&#xff0c;以及文件操作等&…

数据库导论

data 数据是数据库中存储的基本数据&#xff0c;描述事物的符号称为数据。 DB 数据库是长期存储在计算机内&#xff0c;有组织&#xff0c;可共享的大量数据的集合。数据库中的数据按照一定的数据模型组织&#xff0c;描述和存储&#xff0c;具有较小的冗余度&#xff0c;较…

HTML实现 扫雷游戏

前言&#xff1a; 游戏起源与发展 扫雷游戏的雏形可追溯到 1973 年的 “方块&#xff08;cube&#xff09;” 游戏&#xff0c;后经改编出现了 “rlogic” 游戏&#xff0c;玩家需为指挥中心探出安全路线避开地雷。在此基础上&#xff0c;开发者汤姆・安德森编写出了扫雷游戏的…

Spring Boot英语知识网站:开发策略

5系统详细实现 5.1 管理员模块的实现 5.1.1 用户信息管理 英语知识应用网站的系统管理员可以对用户信息添加修改删除以及查询操作。具体界面的展示如图5.1所示。 图5.1 用户信息管理界面 5.1.2 在线学习管理 系统管理员可以对在线学习信息进行添加&#xff0c;修改&#xff0…

HTML5和CSS3新增特性

HTML5的新特性 HTML5新增的语义化标签 HTML5 的新增特性主要是针对于以前的不足&#xff0c;增加了一些新的标签、新的表单和新的表单属性等。 这些新特性都有兼容性问题&#xff0c;基本是 IE9 以上版本的浏览器才支持&#xff0c;如果不考虑兼容性问题&#xff0c;可以大量…

width设置100vh但出现横向滚动条的问题

在去做flex左右固定,中间自适应宽度的布局时, 发现这样一个问题: 就是我明明是宽度占据整个视口, 但是却多出了横向的滚动条 效果是这样的 把width改成100%,就没有滚动条了 原因: body是有默认样式的, 会有一定的默认边距, 把默认边距清除就是正常的了 同时, 如果把高度设…

EasyExcel: 结合springboot实现表格导出入(单/多sheet), 全字段校验,批次等操作(全)

全文目录,一步到位 1.前言简介1.1 链接传送门1.1.1 easyExcel传送门 2. Excel表格导入过程2.1 easyExcel的使用准备工作2.1.1 导入maven依赖2.1.2 建立一个util包2.1.3 ExcelUtils统一功能封装(单/多sheet导入)2.1.4 ExcelDataListener数据监听器2.1.5 ResponseHelper响应值处理…

css:转换

转换 移动 /* transform: translate(100px, 200px); */transform: translateX(100px);transform: translateY(100px); /*一个意思*/ 如果后面跟百分数的意思是移动盒子自身x/y方向长度的百分比&#xff0c;可以用作子绝父相控制盒子水平居中垂直居中 translate里的xy值是相对…

webp 网页如何录屏?

工作中正好研究到了一点&#xff1a;记录下这里&#xff1a; 先看下效果&#xff1a; 具体实现代码&#xff1a; &#xfeff; <!DOCTYPE html> <html lang"zh-CN"> <head><meta charset"UTF-8"><meta name"viewport&qu…

SpringCloud Gateway转发请求到同一个服务的不同端口

SpringCloud Gateway默认不支持将请求路由到一个服务的多个端口 本文将结合Gateway的处理流程&#xff0c;提供一些解决思路 需求背景 公司有一个IM项目&#xff0c;对外暴露了两个端口8081和8082&#xff0c;8081是springboot启动使用的端口&#xff0c;对外提供一些http接口…

SlickGrid复选框

分析 1、先在columns首列添加复选框&#xff1b; 2、在SlickGrid注册刚添加的复选框&#xff1b; 3、添加复选框变化事件&#xff1b; 4、注册按钮点击事件&#xff0c;点击获取已选中的行。 展示 代码 复选框样式&#xff08;CSS&#xff09; .slick-cell-checkboxsel {bac…

摄像头原始数据读取——V4L2(userptr模式,V4L2_MEMORY_USERPTR)

摄像头原始数据读取——V4L2(userptr模式,V4L2_MEMORY_USERPTR) 用户指针方式允许用户空间的应用程序分配内存&#xff0c;并将内存地址传递给内核中的驱动程序。驱动程序直接将数据填充到用户空间的内存中&#xff0c;从而避免了数据的拷贝过程。 流程&#xff1a; 通过VIDI…

浏览器缓存与协商缓存

1. 强缓存&#xff08;Strong Cache&#xff09; 定义 强缓存是指在缓存的资源有效期内&#xff0c;浏览器会直接使用缓存中的数据&#xff0c;而不会发起网络请求。也就是说&#xff0c;浏览器会直接从本地缓存读取资源&#xff0c;不会与服务器进行任何交互。 如何控制强缓…

AI 写作(一):开启创作新纪元(1/10)

一、AI 写作&#xff1a;重塑创作格局 在当今数字化高速发展的时代&#xff0c;AI 写作正以惊人的速度重塑着创作格局。AI 写作在现代社会中占据着举足轻重的地位&#xff0c;发挥着不可替代的作用。 随着信息的爆炸式增长&#xff0c;人们对于内容的需求日益旺盛。AI 写作能够…

RabbitMQ 篇-深入了解延迟消息、MQ 可靠性(生产者可靠性、MQ 可靠性、消费者可靠性)

??博客主页&#xff1a;【_-CSDN博客】** 感谢大家点赞??收藏评论** 文章目录 ???1.0 RabbitMQ 的可靠性 ? ? ? ? 2.0 发送者的可靠性 ? ? ? ? 2.1 生产者重试机制 ? ? ? ? 2.2 生产者确认机制 ? ? ? ? 2.2.1 开启生产者确认机制 ? ? ? ? 2.2…

问:SpringBoot核心配置文件都有啥,怎么配?

在SpringBoot的开发过程中&#xff0c;核心配置文件扮演着至关重要的角色。这些文件用于配置应用程序的各种属性和环境设置&#xff0c;使得开发者能够灵活地定制和管理应用程序的行为。本文将探讨SpringBoot的核心配置文件&#xff0c;包括它们的作用、区别&#xff0c;并通过…

【机器学习】数据集合集!

本文将为您介绍经典、热门的数据集&#xff0c;希望对您在选择适合的数据集时有所帮助。 1 privacy 更新时间&#xff1a;2024-11-26 访问地址: GitHub 描述&#xff1a; 此存储库包含 TensorFlow Privacy&#xff08;一种 Python&#xff09;的源代码 库&#xff0c;其中包…

Linux V4L2框架介绍

linux V4L2框架介绍 V4L2框架介绍 V4L2&#xff0c;全称Video for Linux 2&#xff0c;是Linux操作系统下用于视频数据采集设备的驱动框。它提供了一种标准化的方式使用户空间程序能够与视频设备进行通信和交互。通过V4L2接口&#xff0c;用户可以方便地实现视频图像数据的采…

[网安靶场] [更新中] UPLOAD LABS —— 靶场笔记合集

GitHub - c0ny1/upload-labs: 一个想帮你总结所有类型的上传漏洞的靶场一个想帮你总结所有类型的上传漏洞的靶场. Contribute to c0ny1/upload-labs development by creating an account on GitHub.https://github.com/c0ny1/upload-labs 0x01&#xff1a;UPLOAD LABS 靶场初识…

SpringBoot社团管理:用户体验优化

3系统分析 3.1可行性分析 通过对本社团管理系统实行的目的初步调查和分析&#xff0c;提出可行性方案并对其一一进行论证。我们在这里主要从技术可行性、经济可行性、操作可行性等方面进行分析。 3.1.1技术可行性 本社团管理系统采用SSM框架&#xff0c;JAVA作为开发语言&#…