当前位置: 首页 > news >正文

【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方法了。


已经到底啦!!

相关文章:

【Android】RecyclerView回收复用机制

概述 RecyclerView 是 Android 中用于高效显示大量数据的视图组件&#xff0c;它是 ListView 的升级版本&#xff0c;支持更灵活的布局和功能。 我们创建一个RecyclerView的Adapter&#xff1a; public class MyRecyclerView extends RecyclerView.Adapter<MyRecyclerVie…...

自制Windows系统(十)

上图 &#xff08;真的不是Windows破解版&#xff09; 开源地址&#xff1a;仿Windows...

Linux——初识操作系统(Operator System)

前言&#xff1a;大佬写博客给别人看&#xff0c;菜鸟写博客给自己看&#xff0c;我是菜鸟。 一、冯偌伊曼体系 图一&#xff1a; 在初识操作系统之前&#xff0c;我们需要对计算机的硬件组成做一定的了解。本篇优先对数据信号做初步分析&#xff0c;暂时不考虑控制信号(操作系…...

RuoYi(若依)框架的介绍与基本使用(超详细分析)

**RuoYi&#xff08;若依&#xff09;**是一个基于Spring Boot和Spring Cloud的企业级快速开发平台。它集成了多种常用的技术栈和中间件&#xff0c;旨在帮助企业快速构建稳定、高效的应用系统。以下是关于RuoYi框架的详细介绍和基本使用教程&#xff0c;涵盖了从环境搭建到核心…...

js:基础

js是什么 JavaScript是一种运行在客户端的编程语言&#xff0c;实现人机交互的效果 js只要有个浏览器就能跑 js可以做网页特效、表单验证、数据交互、服务端编程 服务端编程是前端人拿他们特有的后端语言node.js来干后端干的事情 js怎么组成 JavaScriptECMAScript(语言基…...

easyui combobox 只能选择第一个问题解决

easyui combobox 只能选择第一个问题解决 问题现象 在拆分开票的时候&#xff0c;弹出框上面有一个下拉框用于选择需要新增的明细行&#xff0c;但是每次只能选择到第一个 选择第二条数据的时候默认选择到第一个了 代码如下 /*新增发票编辑窗口*/function addTicketDialog…...

【RISC-V CPU 专栏 -- 香山处理器介绍】

文章目录 RISC-V 香山处理器介绍雁栖湖处理器南湖处理器RISC-V 香山处理器介绍 相信很多小伙伴对于“香山”都不陌生,它是一款开源RISC-V处理器核,香山的每一代架构,都是采用了湖的名字,第一代架构被命名为雁栖湖,第二代架构则叫做 “南湖”。 “雁栖湖”这款处理器的 R…...

深入理解下oracle 11g block组成

深层次说&#xff0c;oracle数据库的最少组成单位应该是块&#xff0c;一般默认情况下&#xff0c;oracle数据库的块大小是8kb&#xff0c;其中存储着我们平常所需的数据。我们在使用过程中&#xff0c;难免会疑问道&#xff1a;“oracle数据块中到底是怎样组成的&#xff0c;平…...

“华为杯”研究生数学建模比赛历年赛题汇总(2004-2024)

文章目录 赛题链接历年赛题2004年赛题2005年赛题2006年赛题2007年赛题2008年赛题2009年赛题2010年赛题2011年赛题2012年赛题2013年赛题2014年赛题2015年赛题2016年赛题2017年赛题2018年赛题2019年赛题2020年赛题2020年赛题2021年赛题2022年赛题2023年赛题2024年赛题 赛题链接 部…...

LLM PPT Translator

LLM PPT Translator 引言Github 地址UI PreviewTranslated Result Samples 引言 周末开发了1个PowerPoint文档翻译工具&#xff0c;上传PowerPoint文档&#xff0c;指定想翻译的目标语言&#xff0c;通过LLM的能力将文档翻译成目标语言的文档。 Github 地址 https://github.…...

【深度学习之一】2024最新pytorch+cuda+cudnn下载安装搭建开发环境

兵马未动&#xff0c;粮草先行。作为深度学习的初学者&#xff0c;快速搭建一个属于自己的开发环境就是头等大事&#xff0c;可以让我们节省许多的时间。这一期我们主要讲一讲2024年最新pytorchcudacudnn下载安装搭建开发环境&#xff0c;以及安装过程中可能遇到的一些问题以及…...

摄像机视频分析软件下载LiteAIServer视频智能分析平台玩手机打电话检测算法技术的实现

随着科技的不断进步&#xff0c;摄像机视频分析软件的发展已经为我们的生活带来了许多便捷。其中&#xff0c;LiteAIServer视频智能分析平台的玩手机打电话检测算法技术尤为突出&#xff0c;它利用先进的图像处理和人工智能技术&#xff0c;能够自动识别并监控视频中的玩手机或…...

HTML5和CSS3新增特性

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

删除word中页眉里的横线

使用快捷键‌简单粗暴&#xff1a; 双击页眉&#xff0c;将光标定位在页眉的横线上&#xff0c;按下CtrlShiftN快捷键&#xff0c;页眉横线即可删除。...

列表代码思路

目录 列表添加修改删除(单删,批删) 页面>Controller>service>Dao 一.列表的jsp页面 : 一. 想要用户已经来就看到的数据使用文档就绪函数 ①文档就绪函数 : 二. 封装ajax方法 二 : 在body中间 一 : 多条件查询的文本框 二. 写列表 三.在body的下面写脚本 1.给搜…...

40分钟学 Go 语言高并发:Context包与并发控制

Context包与并发控制 学习目标 知识点掌握程度应用场景context原理深入理解实现机制并发控制和请求链路追踪超时控制掌握超时设置和处理API请求超时、任务限时控制取消信号传播理解取消机制和传播链优雅退出、资源释放context最佳实践掌握使用规范和技巧工程实践中的常见场景…...

el-row el-col显示失效问题修复

el-row el-col显示失效 问题&#xff1a; 在列表显示页面&#xff0c;头部有几个搜索框和选择框&#xff0c;由于搜索条件框太多&#xff0c;写了el-row 和el-col进行分行分列展示。测试发现并没有按照行列展示。 <el-form :inline"true" :model"paramForm…...

libphone desktop编译

linphone-desktop 在ubuntu20.04 下编译 linphone 介绍 Linphone是一款遵循GPL的开源网络视频电话系统&#xff0c;支持多种平台如Windows、Linux、Android等。它基于SIP协议&#xff0c;提供语音、视频通话及即时文本消息功能。核心功能包括SIP用户代理、音频视频Codec支持、…...

实现一个可配置的TCP设备模拟器,支持交互和解析配置

前言 诸位在做IOT开发的时候是否有遇到一个问题&#xff0c;那就是模拟一个设备来联调测试&#xff0c;虽然说现在的物联网通信主要是用mqtt通信&#xff0c;但还是有很多设备使用TCP这种协议交互&#xff0c;例如充电桩&#xff0c;还有一些工业设备&#xff0c;TCP这类报文交…...

Rust环境安装乱码解决

安装rust环境open with visual studio2022操作系统乱码问题解决 打开“设置”&#xff0c;找到“时间和语言”。 进去之后依次选择“语言”->"管理语言设置"->“更改系统区域设置” 取消勾选“Beta版:使用Unicode UTF-8 提供全球语言支持”&#xff0c;然后重…...

Zookeeper实现分布式锁、Zookeeper实现配置中心

一、Zookeeper实现分布式锁 分布式锁主要用于在分布式环境中保证数据的一致性。 包括跨进程、跨机器、跨网络导致共享资源不一致的问题。 1.Zookeeper分布式锁的代码实现 新建一个maven项目ZK-Demo,然后在pom.xml里面引入相关的依赖 <dependency><groupId>com.…...

学习日记_20241126_聚类方法(自组织映射Self-Organizing Maps, SOM)

前言 提醒&#xff1a; 文章内容为方便作者自己后日复习与查阅而进行的书写与发布&#xff0c;其中引用内容都会使用链接表明出处&#xff08;如有侵权问题&#xff0c;请及时联系&#xff09;。 其中内容多为一次书写&#xff0c;缺少检查与订正&#xff0c;如有问题或其他拓展…...

【webrtc】 mediasoup中m77的IntervalBudget及其在AlrDetector的应用

IntervalBudget 用于带宽控制和流量整形 mediasoup中m77 代码的IntervalBudget ,版本比较老IntervalBudget 在特定时间间隔内的比特预算管理,从而实现带宽控制和流量整形。 一。 pacedsender 执行周期: 下一次执行的时间的动态可变的 int64_t PacedSender::TimeUntilNextPr…...

Python学习指南 + 谷歌浏览器如何安装插件

找往期文章包括但不限于本期文章中不懂的知识点&#xff1a; 个人主页&#xff1a;我要学编程(ಥ_ಥ)-CSDN博客 所属专栏&#xff1a; Python 目录 前言 Python 官方文档的使用 谷歌浏览器中如何安装插件 前言 在学习Python时&#xff0c;我们可能会出现这样的困惑&#x…...

leetcode - LRU缓存

什么是 LRU LRU (最近最少使用算法), 最早是在操作系统中接触到的, 它是一种内存数据淘汰策略, 常用于缓存系统的淘汰策略. LRU算法基于局部性原理, 即最近被访问的数据在未来被访问的概率更高, 因此应该保留最近被访问的数据. 最近最少使用的解释 LRU (最近最少使用算法), 中…...

RabbitMQ7:消息转换器

欢迎来到“雪碧聊技术”CSDN博客&#xff01; 在这里&#xff0c;您将踏入一个专注于Java开发技术的知识殿堂。无论您是Java编程的初学者&#xff0c;还是具有一定经验的开发者&#xff0c;相信我的博客都能为您提供宝贵的学习资源和实用技巧。作为您的技术向导&#xff0c;我将…...

C#(14)七大原则

前言 其实在面向对象设计里&#xff0c;程序猿们互相约定好一些原则&#xff0c;即七大原则。 面向对象的七大原则是一组指导软件设计的原则&#xff0c;旨在帮助开发人员实现松耦合、可维护和可扩展的软件系统。这些原则的设计过程和发展历史可以追溯到20世纪80年代。 单一职…...

JDBC使用连接池druid操作数据库mysql

// 创建DruidDataSource实例DruidDataSource dataSource = new DruidDataSource();String url = "jdbc:mysql://localhost:3306/demo_base?useUnicode=true&characterEncoding=utf8&useSSL=true";String username...

零碎04 MybatisPlus自定义模版生成代码

目录 背景 动手开干 需要的依赖包&#xff0c;需要注意mybatis-plus-generator的3.5版本是没有兼容历史版本的。 定义一个CodeGenerator类&#xff0c;负责生成代码和配置属性 Entity模版 背景 MybatisPlus代码生成使用默认的velocity模版解决不了定制化的需求&#xff0…...

rk3568-linux-5.10.160移植rtl8822cs wifi 模块纪要

rk3568-linux-5.10.160移植rtl8822cs wifi 模块纪要 1、将驱动添加到 kernel/drivers/net/wireless/rockchip_wlan/ 或者 kernel/drivers/net/wireless/realtek/rtlwifi/ 2、修改该目录下的makefile、kconfig 3、修改rtl8822cs/os_dep/linux/os_intfs.c&#xff0c;添加 MO…...

uniapp接入高德地图

下面代码兼容安卓APP和H5 高德地图官网&#xff1a;我的应用 | 高德控制台 &#xff0c;绑定服务选择《Web端(JS API)》 /utils/map.js 需要设置你自己的key和安全密钥 export function myAMap() {return new Promise(function(resolve, reject) {if (typeof window.onLoadM…...

如何通过高效的缓存策略无缝加速湖仓查询

引言 本文将探讨如何利用开源项目 StarRocks 的缓存策略来加速湖仓查询&#xff0c;为企业提供更快速、更灵活的数据分析能力。作为 StarRocks 社区的主要贡献者和商业化公司&#xff0c;镜舟科技深度参与 StarRocks 项目开发&#xff0c;也为企业着手构建湖仓架构提供更多参考…...

微信小程序数据请求教程:GET与POST请求详解

微信小程序数据请求教程:GET与POST请求详解 引言 在微信小程序的开发过程中,数据请求是至关重要的一部分。通过与后端服务器进行通信,小程序能够获取动态数据,实现丰富的功能。在这篇文章中,我们将深入探讨微信小程序中的数据请求,重点介绍GET和POST请求的使用方法、示…...

【Redis 】Bitmap 使用

Redis Bitmap介绍 Redis Bitmap 是一种特殊的数据类型&#xff0c;它通过字符串类型键来存储一系列连续的二进制位&#xff08;bits&#xff09;&#xff0c;每个位可以独立地表示一个布尔值&#xff08;0 或 1&#xff09;。这种数据结构非常适合用于存储和操作大量二值状态的…...

【C语言】指针与数组的例题详解:深入分析与高级用法

博客主页&#xff1a; [小ᶻ☡꙳ᵃⁱᵍᶜ꙳] 本文专栏: C语言 文章目录 &#x1f4af;前言&#x1f4af;题目一详细分析与解答代码逐步解析 &#x1f4af;进一步优化和拓展1. 指针与数组的关系2. 指针运算的注意事项3. 常见的错误和陷阱4. 拓展&#xff1a;指针操作的应用场…...

CTF之密码学(密码特征分析)

一.MD5,sha1,HMAC,NTLM 1.MD5:MD5一般由32/16位的数字(0-9)和字母(a-f)组成的字符串 2.sha1:这种加密的密文特征跟MD5差不多,只不过位数是40(sha256:64位;sha512:128位) 3.HMAC:这种算法就是在前两种加密的基础上引入了秘钥,而秘钥又只有传输双方才知道,所以基本上…...

docker compose 使用记录

作用 Docker Compose 是一个用于定义和运行多容器 Docker 应用程序的工具。通过一个 YAML 文件来配置应用程序的服务&#xff0c;然后使用一个命令即可创建并启动所有服务。 文档位置 Part 7: Use Docker Compose | Docker Docs 使用方法 1. 安装 Docker Compose sudo curl -…...

Python的3D可视化库 - vedo (3)visual子模块 点对象的可视化控制

文章目录 3 PointsVisual的方法3.1 对象属性3.1.1 顶点大小3.1.2 复制属性3.1.3 颜色设置3.1.4透明度设置 3.2 对象光效3.2.1 点的形状3.2.2 点的表面光效 3.3 尾随线和投影3.3.1 尾随线3.3.2 投影 3.4 给对象附加文字说明3.4.1 标注3.4.2 2D标注3.4.3 气泡说明3.4.4 旗标说明3…...

计算机网络易混淆知识点串记

文章目录 计算机网络易混淆知识点串记各层PDU首部:地址长度 计算机网络易混淆知识点串记 各层PDU首部: PUD首部长度 (B:字节)首部单位数据链路–帧帧首:14B帧尾部:4B——IPV420~60字节4B [通过4位二进制表示]IPV6固定首部40字节[可拓展]4BTCP20~60字节4BUDP8B字节 地址长度 …...

信息安全实验--密码学实验工具:CrypTool

1. CrypTool介绍&#x1f4ad; CrypTool 1的开源教育工具&#xff0c;用于密码学研究。通过CrypTool 1&#xff0c;可以实现加密和解密操作&#xff0c;数字签名。CrypTool1和2有很多区别的。 2. CrpyTool下载&#x1f527; 在做信息安全实验--密码学相关实验时&#xff0c;发…...

Leetcode 将有序数组转换为二叉搜索树

算法思想及代码解析&#xff1a; 这段代码的目的是将一个有序数组转换为 高度平衡的二叉搜索树&#xff08;Balanced Binary Search Tree, BST&#xff09;。以下是算法的详细解释&#xff1a; 1. 什么是高度平衡的二叉搜索树&#xff1f; 二叉搜索树&#xff1a;对于树中的每…...

VMware虚拟机安装Win7专业版详细教程(附镜像包)

目录 一、Win7镜像下载 二、配置虚拟机 三、安装Win7 四、常见问题 一、Win7镜像下载 镜像下载链接&#xff1a;https://pan.quark.cn/s/a8d3a2880101 此镜像为Win7专业版&#xff08;收藏级镜像 已自用几年&#xff09; 官方纯净系统没有附带任何其他第三方软件&#xf…...

IDEA算法的详细介绍及Python实现

目录 IDEA算法的详细介绍及Python实现引言第一部分:IDEA算法的原理与背景1.1 IDEA算法的来源与特点1.2 IDEA算法的核心步骤第二部分:IDEA算法的Python实现(面向对象设计)2.1 核心类设计2.2 代码实现2.3 使用示例第三部分:案例1 - 函数优化问题(策略模式)3.1 问题描述3.2…...

Spring Boot 开发环境搭建详解

下面安装spring boot的详细步骤&#xff0c;涵盖了从安装 JDK 和 Maven 到创建和运行一个 Spring Boot 项目的全过程。 文章目录 1. 安装 JDK步骤 1.1&#xff1a;下载 JDK步骤 1.2&#xff1a;安装 JDK步骤 1.3&#xff1a;配置环境变量 2. 安装 Maven步骤 2.1&#xff1a;下载…...

Element-Ui组件(icon组件)

一、前言 本篇文章主要是对官网的Icon组件进行总结归纳Icon 图标 | Element Plus 在现代Web应用开发中&#xff0c;图标是用户界面设计中不可或缺的一部分。它们不仅提升了用户体验&#xff0c;还使得信息的传达更加直观和高效。本文主要对Element Plus 官方提供的Icon组件进行…...

使用docker搭建hysteria2服务端

源链接&#xff1a;https://github.com/apernet/hysteria/discussions/1248 官网地址&#xff1a;https://v2.hysteria.network/zh/docs/getting-started/Installation/ 首选需要安装docker和docker compose 切换到合适的目录 cd /home创建文件夹 mkdir hysteria创建docke…...

Docker login 报证书存储错误的解决办法

文章目录 docker login 出现错误&#xff0c;提示&#xff1a;Error saving credentials: error storing credentials - err: exit status 1, out: Cannot autolaunch D-Bus without X11 $DISPLAY 环境 使用的是 Mint Linux &#xff0c;容器为 docker-ce 最新版 1 2 3 4 $…...

vxe-modal VxeUI 窗口组件弹窗多窗口模式

VxeUI 实现在 vue 中使用弹窗组件&#xff0c;弹窗多个窗口可叠加&#xff0c;实现多实例的窗口组件。 npm install vxe-pc-ui4.3.6// ...import VxeUI from vxe-pc-uiimport vxe-pc-ui/lib/style.css// ...createApp(App).use(VxeUI).mount(#app)// ...官网&#xff1a;https…...

华为昇腾 acl_pytorch

目录 sam部署教程&#xff1a; segment-anyghing地址&#xff1a; sam onnx地址&#xff1a; 报错&#xff1a; encode继续报错&#xff1a; sam部署教程&#xff1a; Ascend/ModelZoo-PyTorch - Gitee.com segment-anyghing地址&#xff1a; https://github.com/visher…...

Mybatis控制台打印SQL执行信息(执行方法、执行SQL、执行时间)

文章目录 前言一、基本功能介绍1.1本章功能效果预览图: 二、可执行源码2.1 yaml基础配置2.2 MybatisAnalyzeSQLInterceptor实现SQL拦截 前言 SQL性能监控是一个程序必要的功能&#xff0c;通常我们可以使用数据库自带的客户端工具进行SQL性能分析。然而对于一些专业度不高的人…...