RecyclerView流程学习

RecyclerView流程学习

  • 模块划分
  • 绘制流程
    • onMeasure
      • mLayout为null
      • mLayout开启自动测量
      • 未开启自动测量
    • onLayout
    • onDraw
    • onLayoutChildren
  • 缓存
  • 预加载
  • 滚动和fling

RecyclerView

模块划分

RecyclerView中根据其功能可以分为以下几个模块:

  1. Recycler mRecycler // 缓存管理者,final类型-不允许扩展
  2. LayoutManager mLayoutManager // 数据展示者
  3. RecyclerViewDataObserver mObserver // 数据观察者
  4. Adapter mAdapter // 数据提供者
  5. ItemAnimator mItemAnimator // 动画类

绘制流程

onMeasure

protected void onMeasure(int widthSpec, int heightSpec) {
	//mLayout就是之前说过的LayoutManager
    if (mLayout == null) {
        // 第一种情况
    }
    if (mLayout.isAutoMeasureEnabled()) {
        // 第二种情况
    } else {
        // 第三种情况
    }
}

在onMeasure方法中,recyclerView根据mLayout分为了三种情况,接下来会对这三种情况进行梳理。

mLayout为null

第一种,当mLayout为null的时候:

if (mLayout == null) {
    defaultOnMeasure(widthSpec, heightSpec);
    return;
}
void defaultOnMeasure(int widthSpec, int heightSpec) {
    // calling LayoutManager here is not pretty but that API is already public and it is better
    // than creating another method since this is internal.
    final int width = LayoutManager.chooseSize(widthSpec,
            getPaddingLeft() + getPaddingRight(),
            ViewCompat.getMinimumWidth(this));
    final int height = LayoutManager.chooseSize(heightSpec,
            getPaddingTop() + getPaddingBottom(),
            ViewCompat.getMinimumHeight(this));

    setMeasuredDimension(width, height);
}
public static int chooseSize(int spec, int desired, int min) {
    final int mode = View.MeasureSpec.getMode(spec);
    final int size = View.MeasureSpec.getSize(spec);
    switch (mode) {
    	//表示精确模式,View的大小已经确认,为SpecSize所指定的值
        case View.MeasureSpec.EXACTLY:
            return size;
        //指定了最大大小
        case View.MeasureSpec.AT_MOST:
            return Math.min(size, Math.max(desired, min));
        //父容器不对子View有限制,子View要多大给多大
        case View.MeasureSpec.UNSPECIFIED:
        default:
            return Math.max(desired, min);
    }
}

可以看到在mLayout为Null的情况下,recyclerView还是做了测量操作,但是由于在onLayout方法中跳过了layout,因此不会展示任何东西。

if (mLayout == null) {
    Log.e(TAG, "No layout manager attached; skipping layout");
    // leave the state in START
    return;
}

mLayout开启自动测量

在这种情况下,RecyclerView会先后调用dispatchLayoutStep1()dispatchLayoutStep2()方法,除此之外,还有一个dispatchLayoutStep3()方法会在onLayout中进行调用,这三个方法对应了RecyclerView的三种不同状态,State.STEP_START、State.STEP_LAYOUT和State.STEP_ANIMATIONS。
State.STEP_START表示RecyclerView还未经历dispatchLayoutStep1()
State.STEP_LAYOUT表示此时处于layout阶段,这个阶段会调用dispatchLayoutStep2方法layout RecyclerView的children。调用dispatchLayoutStep2方法之后,此时mState.mLayoutStep变为了State.STEP_ANIMATIONS。
当mState.mLayoutStep为State.STEP_ANIMATIONS时,表示RecyclerView处于第三个阶段,也就是执行动画的阶段,也就是调用dispatchLayoutStep3方法。当dispatchLayoutStep3方法执行完毕之后,mState.mLayoutStep又变为了State.STEP_START。
具体地,先来看一下源码:

if (mLayout.isAutoMeasureEnabled()) {
    final int widthMode = MeasureSpec.getMode(widthSpec);
    final int heightMode = MeasureSpec.getMode(heightSpec);
    //官方说明:这里的调用已经废弃,事实上已经替换为defaultOnMeasure
    //也就是说mLayout应该调用defaultOnMeasure()方法
    //但是为了防止第三方代码被破坏,还是保留了下来
    //所有开发人员在isAutoMeasureEnabled为true不应该覆盖onMeasure方法
    mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
    final boolean measureSpecModeIsExactly =
            widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
    if (measureSpecModeIsExactly || mAdapter == null) {
        return;
    }

    if (mState.mLayoutStep == State.STEP_START) {
        dispatchLayoutStep1();
    }
	// 在第 2 步中设置尺寸。 为了保持一致性,应该使用旧尺寸进行预布局
    mLayout.setMeasureSpecs(widthSpec, heightSpec);
    mState.mIsMeasuring = true;
    dispatchLayoutStep2();

    // 现在我们可以从孩子那里得到宽度和高度.
    mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);

	// 如果 RecyclerView 的宽度和高度不准确,并且至少有一个孩子的宽度和高度也不准确,我们必须重新测量。
    if (mLayout.shouldMeasureTwice()) {
        mLayout.setMeasureSpecs(
                MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY),
                MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY));
        mState.mIsMeasuring = true;
        dispatchLayoutStep2();
        // now we can get the width and height from the children.
        mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
    }
}

可以看到这里最终还是调用了defaultOnMeasure()这个方法在之前进行了介绍,但是与之前不同的一点是在此之后又调用了dispatchLayoutStep1()dispatchLayoutStep2(),并且如果需要二次测量的话会重新执行
dispatchLayoutStep2()
那么这两个方法具体起到了什么作用呢?
先来看一下这两个方法的源码:

private void dispatchLayoutStep1() {
    ... ...
    processAdapterUpdatesAndSetAnimationFlags();
    ... ...

    if (mState.mRunSimpleAnimations) {
       // 找到没有被remove的ItemView,保存OldViewHolder信息,准备预布局
    }
    if (mState.mRunPredictiveAnimations) {
       // 进行预布局
    } else {
        clearOldPositions();
    }
    onExitLayoutOrScroll();
    stopInterceptRequestLayout(false);
    mState.mLayoutStep = State.STEP_LAYOUT;
}

private void processAdapterUpdatesAndSetAnimationFlags() {
    if (mDataSetHasChangedAfterLayout) {
		// 因为数据集意外更改,处理这些项目没有价值。 
		// 相反,我们只是重置它。
        mAdapterHelper.reset();
        if (mDispatchItemsChangedEvent) {
            mLayout.onItemsChanged(this);
        }
    }
	// 简单动画是高级动画的一个子集(这将导致预布局步骤)如果布局支持预测动画,
	// 则进行预处理以决定我们是否要运行它们
    if (predictiveItemAnimationsEnabled()) {
        mAdapterHelper.preProcess();
    } else {
        mAdapterHelper.consumeUpdatesInOnePass();
    }
    boolean animationTypeSupported = mItemsAddedOrRemoved || mItemsChanged;
    //重点,这里主要设置了mRunSimpleAnimations和mRunPredictiveAnimations的值
    //mFirstLayoutComplete是指第一次绘制流程完成,当未完成时为false
    //因此当一次绘制的时候mRunSimpleAnimations和mRunPredictiveAnimations都为false
    //不会加载动画
    mState.mRunSimpleAnimations = mFirstLayoutComplete
            && mItemAnimator != null
            && (mDataSetHasChangedAfterLayout
            || animationTypeSupported
            || mLayout.mRequestedSimpleAnimations)
            && (!mDataSetHasChangedAfterLayout
            || mAdapter.hasStableIds());
    mState.mRunPredictiveAnimations = mState.mRunSimpleAnimations
            && animationTypeSupported
            && !mDataSetHasChangedAfterLayout
            && predictiveItemAnimationsEnabled();
}

private void dispatchLayoutStep2() {

    ...
    
    mState.mItemCount = mAdapter.getItemCount();
    mState.mDeletedInvisibleItemCountSincePreviousLayout = 0;

    // Step 2: Run layout
    //重点,尝试执行layoutChildren
    mState.mInPreLayout = false;
    mLayout.onLayoutChildren(mRecycler, mState);

    ...
    mState.mLayoutStep = State.STEP_ANIMATIONS;
    ...
}

总结一下,step1的功能是与Animations有关,控制是否加载动画,而step2的功能是尝试layoutChildren。

未开启自动测量

if (mHasFixedSize) {
    mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
    return;
}
// custom onMeasure
if (mAdapterUpdateDuringMeasure) {
    eatRequestLayout();
    processAdapterUpdatesAndSetAnimationFlags();

    if (mState.mRunPredictiveAnimations) {
        mState.mInPreLayout = true;
    } else {
        // consume remaining updates to provide a consistent state with the layout pass.
        mAdapterHelper.consumeUpdatesInOnePass();
        mState.mInPreLayout = false;
    }
    mAdapterUpdateDuringMeasure = false;
    resumeRequestLayout(false);
}

if (mAdapter != null) {
    mState.mItemCount = mAdapter.getItemCount();
} else {
    mState.mItemCount = 0;
}
eatRequestLayout();
mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
resumeRequestLayout(false);
mState.mInPreLayout = false; // clear

如果mHasFixedSize为true,就直接调用LayoutManager.onMeasure方法进行测量,如果mHasFixedSize为false,则先判断是否有数据更新,有的话先处理数据更新,再调用LayoutManager.onMeasure方法进行测量。

onLayout

onMeasure方法完成之后,接下来应该进行onLayout方法:

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);
    dispatchLayout();
    TraceCompat.endSection();
    mFirstLayoutComplete = true;
}

这里最重要的就是dispatchLayout方法,来具体地看一下这个方法的实现:

    void dispatchLayout() {
    	... ...
        mState.mIsMeasuring = false;
        //前两步已经在onMeasure的时候完成,
        //但是当size发生变化的时候,仍然需要重新onLayout
        if (mState.mLayoutStep == State.STEP_START) {
            dispatchLayoutStep1();
            mLayout.setExactMeasureSpecsFrom(this);
            dispatchLayoutStep2();
        } else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()
                || mLayout.getHeight() != getHeight()) {
            mLayout.setExactMeasureSpecsFrom(this);
            dispatchLayoutStep2();
        } else {
            // always make sure we sync them (to ensure mode is exact)
            mLayout.setExactMeasureSpecsFrom(this);
        }
        //布局的最后一步,我们保存有关动画视图的信息,触发动画并进行任何必要的清理。
        //这里设置了mState.mLayoutStep = State.STEP_START
        dispatchLayoutStep3();
    }

onDraw

public void draw(Canvas c) {
    super.draw(c);
    final int count = mItemDecorations.size();
    //调用了mItemDecorations的onDraw方法
    //此时item已经在绘制了,这意味着ItemDecoration会在item上方被绘制
    for (int i = 0; i < count; i++) {
        mItemDecorations.get(i).onDrawOver(c, this, mState);
    }
    ... ...
}
@Override
public void onDraw(Canvas c) {
    super.onDraw(c);
    //调用了mItemDecorations的onDraw方法
    //此时item还没有绘制,这意味着ItemDecoration会在item下方被绘制
    final int count = mItemDecorations.size();
    for (int i = 0; i < count; i++) {
        mItemDecorations.get(i).onDraw(c, this, mState);
    }
}

这里有两个知识要点:

  1. 当view draw的时候会以适当执行以下方法(但是3一定会在4前面):
    1. 绘制背景
    2. 如有必要,保存画布的图层以备褪色
    3. 绘制视图的内容(调用onDraw()方法)
    4. 画孩子(dispatchDraw() 将draw事件分发给children)
    5. 如有必要,绘制褪色边缘并恢复图层
    6. 绘制装饰(例如滚动条)
    7. 如有必要,绘制默认焦点高亮
  2. itemDecoration是指item的装饰,系统默认实现了一个DividerItemDecoration用作item之间的分割线。实际上,itemDecoration可以实现更多的效果,这里可以参考这篇文章RecyclerView系列之二ItemDecoration。

总而言之draw方法主要做了以下几件事情:

  • 将draw事件分发给子类
  • 绘制itemDecoration
  • 根据setClipToPadding制定特殊的滑动效果

onLayoutChildren

在之前的dispatchLayoutStep2中,RecyclerView调用了mLayout.onLayoutChildren,LayoutManager是具体的item展示者,由它来确定item应该如何展示,如何布局。具体的应用可以查看这篇文章:LayoutManager及其自定义。
这里只讲述LayoutManager是如何绘制item的,以LinearLayoutManager为例:

public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    //布局算法:
    // 1) 通过检查children和其他变量,找到锚点坐标和锚点项位置。
    // 2) 向开始填充,从底部堆叠
    // 3) 向末端填充,从顶部堆叠
    // 4) 滚动以满足从底部堆叠的要求。
    // 创建布局状态
    // ······
    // 第一步
    final View focused = getFocusedChild();
    if (!mAnchorInfo.mValid || mPendingScrollPosition != NO_POSITION
            || mPendingSavedState != null) {
        mAnchorInfo.reset();
        mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
        // 计算锚点的位置和坐标
        updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
        mAnchorInfo.mValid = true;
    }
    // ······
    // 移除回收子view
    detachAndScrapAttachedViews(recycler);
    mLayoutState.mIsPreLayout = state.isPreLayout();
    // 开始填充
    if (mAnchorInfo.mLayoutFromEnd) {
        // 向开始填充,更新LayoutState
        updateLayoutStateToFillStart(mAnchorInfo);
        mLayoutState.mExtra = extraForStart;
        fill(recycler, mLayoutState, state, false);
        startOffset = mLayoutState.mOffset;
        final int firstElement = mLayoutState.mCurrentPosition;
        if (mLayoutState.mAvailable > 0) {
            extraForEnd += mLayoutState.mAvailable;
        }
        // 向末端填充
        updateLayoutStateToFillEnd(mAnchorInfo);
        mLayoutState.mExtra = extraForEnd;
        mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
        fill(recycler, mLayoutState, state, false);
        endOffset = mLayoutState.mOffset;

        if (mLayoutState.mAvailable > 0) {
            // end could not consume all. add more items towards start
            extraForStart = mLayoutState.mAvailable;
            updateLayoutStateToFillStart(firstElement, startOffset);
            mLayoutState.mExtra = extraForStart;
            fill(recycler, mLayoutState, state, false);
            startOffset = mLayoutState.mOffset;
        }
    } else {
        // 向开始填充
        updateLayoutStateToFillEnd(mAnchorInfo);
        mLayoutState.mExtra = extraForEnd;
        fill(recycler, mLayoutState, state, false);
        endOffset = mLayoutState.mOffset;
        final int lastElement = mLayoutState.mCurrentPosition;
        if (mLayoutState.mAvailable > 0) {
            extraForStart += mLayoutState.mAvailable;
        }
        // 向末端填充
        updateLayoutStateToFillStart(mAnchorInfo);
        mLayoutState.mExtra = extraForStart;
        mLayoutState.mCurrentPosition += mLayoutState.mItemDirection;
        fill(recycler, mLayoutState, state, false);
        startOffset = mLayoutState.mOffset;

        if (mLayoutState.mAvailable > 0) {
            extraForEnd = mLayoutState.mAvailable;
            // start could not consume all it should. add more items towards end
            updateLayoutStateToFillEnd(lastElement, endOffset);
            mLayoutState.mExtra = extraForEnd;
            fill(recycler, mLayoutState, state, false);
            endOffset = mLayoutState.mOffset;
        }
    }
    // ······
}

可以看到在这个方法中,首先计算出了锚点的位置和坐标,然后以锚点开始向start方向或者end方向进行填充,如果还有剩余位置的话就从另一个方向开始进行填充。
简单总结一下整个流程就是:

确定锚点
更新layoutState
根据layoutState和填充方向开始fill
向另外一个方向填充

fill方法中,真正填充的方法是layoutChunk

int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
        RecyclerView.State state, boolean stopOnFocusable) {
    // ······
    while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
        // ······
        layoutChunk(recycler, state, layoutState, layoutChunkResult);
    }
     // ······
}

layoutChunk的执行流程如下:

调用LayoutState的next方法获得一个ItemView。千万别小看这个next方法,RecyclerView缓存机制的起点就是从这个方法开始,可想而知,这个方法到底为我们做了多少事情。
如果RecyclerView是第一次布局Children的话(layoutState.mScrapList == null为true),会先调用addView,将View添加到RecyclerView里面去。
调用measureChildWithMargins方法,测量每个ItemView的宽高。注意这个方法测量ItemView的宽高考虑到了两个因素:1.margin属性;2.ItemDecoration的offset。
调用layoutDecoratedWithMargins方法,布局ItemView。这里也考虑上面的两个因素的。

缓存

在讲述缓存之前,不妨先思考一下,为什么RecyclerView需要缓存机制?
在RecyclerView的Adapter中,对于不同的数据类型会为其创建一个ViewHolder,并且将数据绑定到这个ViewHolder上。

public class MyAdapter extends RecyclerView.Adapter<MessageViewHolder> {
    @Override
    public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType){
		... ...
	}
	@Override
    public void onBindViewHolder(@NonNull MyViewHolder holder, int position){
		... ...
	}
}

但是无论是onCreateViewHolder还是onBindViewHolder都会使用到findViewById方法,如果每次一个新item进入都需要回调这两个方法会导致效率很低,因此需要对ViewHolder进行缓存复用以减少回调频次。

先看看这篇文章吧
这10张图拿去,别再说学不会RecyclerView的缓存复用机制了!

public final class Recycler {
    ...
    /**
     * Attempts to get the ViewHolder for the given position, either from the Recycler scrap,
     * cache, the RecycledViewPool, or creating it directly.
     * 
     * 尝试通过从Recycler scrap缓存、RecycledViewPool查找或直接创建的形式来获取指定位置的ViewHolder。
     * ...
     */
    @Nullable
    ViewHolder tryGetViewHolderForPositionByDeadline(int position,
            boolean dryRun, long deadlineNs) {
        if (mState.isPreLayout()) {
            // 0 尝试从mChangedScrap中获取ViewHolder对象
            holder = getChangedScrapViewForPosition(position);
            ...
        }
        if (holder == null) {
            // 1.1 尝试根据position从mAttachedScrap或mCachedViews中获取ViewHolder对象
            holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
            ...
        }
        if (holder == null) {
            ...
            final int type = mAdapter.getItemViewType(offsetPosition);
            if (mAdapter.hasStableIds()) {
                // 1.2 尝试根据id从mAttachedScrap或mCachedViews中获取ViewHolder对象
                holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
                        type, dryRun);
                ...
            }
            if (holder == null && mViewCacheExtension != null) {
                // 2 尝试从mViewCacheExtension中获取ViewHolder对象
                final View view = mViewCacheExtension
                        .getViewForPositionAndType(this, position, type);
                if (view != null) {
                    holder = getChildViewHolder(view);
                    ...
                }
            }
            if (holder == null) { // fallback to pool
                // 3 尝试从mRecycledViewPool中获取ViewHolder对象
                holder = getRecycledViewPool().getRecycledView(type);
                ...
            }
            if (holder == null) {
                // 4.1 回调createViewHolder方法创建ViewHolder对象及其关联的视图
                holder = mAdapter.createViewHolder(RecyclerView.this, type);
                ...
            }
        }

        if (mState.isPreLayout() && holder.isBound()) {
            ...
        } else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
            ...
            // 4.1 回调bindViewHolder方法提取数据填充ViewHolder的视图内容
            bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
        }

        ...

        return holder;
    }
    ...
}    

从上述代码,可以看到RecyclerView在尝试获取ViewHolder的时候会依次从mChangedScrap、mAttachedScrap、mCachedViews、mViewCacheExtension、mRecycledViewPool获取,如果都获取不到才会创建viewHolder。
mChangedScrap、mAttachedScrap主要用于存放当前屏幕可见,但是被标记为“移除”或者“重用”的列表项。
mChangedScrap主要用于notifyItemChanged、notifyItemRangeChanged这类方法在动画开启下的场景,而mAttachedScrap则用于notifyItemMoved、notifyItemRemoved这类列表项发生移动的场景。
当调用notifyItemRemoved后,RecyclerView首先会将所有的可见的viewHolder加入到mAttachedScrap中,等到重新布局完成,开始展示子视图之后再遍历mAttachedScrap找到对应position的viewHolder。

mCachedViews用于存放已经被移除屏幕,但是很快有可能进入屏幕的列表项,默认大小为2。这里比较好理解,假设RecyclerView向下滑动,当最上面的viewHolder滚动出可见区的时候,这个viewHolder会加入到mCachedViews中。之后如果反方向滑动,当最上面的viewHolder再次进入到可见区域之后,就会尝试从mCachedViews再次获取之前的viewHolder。

mViewCacheExtension主要用于提供额外的、开发人员使用的缓冲区。

mRecyclerPool主要用于按不同的itemType分别存放超出mCachedViews限制的、被移出屏幕的列表项。

public static class RecycledViewPool {
    private static final int DEFAULT_MAX_SCRAP = 5;
    //自定义了数据类型ScrapData,每个ScrapData内部存有最多5个同一viewType类型的viewHolder。
    static class ScrapData {
        final ArrayList<ViewHolder> mScrapHeap = new ArrayList<>();
        int mMaxScrap = DEFAULT_MAX_SCRAP;
        long mCreateRunningAverageNs = 0;
        long mBindRunningAverageNs = 0;
    }
    SparseArray<ScrapData> mScrap = new SparseArray<>();
    ...
}

预加载

先看看这篇文章吧
掌握这17张图,没人比你更懂RecyclerView的预加载

滚动和fling

RecyclerView 的滚动是怎么实现的?(一)
RecyclerView 的滚动时怎么实现的?(二)| Fling

简单来说,RecyclerView回去捕获MotionEvent.ACTION_MOVE事件,并且计算出移动的距离。
当移动超过一定距离则进入Fling状态,每一帧都会去执行移动任务。
当计算出移动距离之后,委托LayoutManager执行当前的item向反方向移动的动画,并且填充空闲区域。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

yolov5的基本配置

yolov5的基本配置train.pydata.yaml数据集标签文件格式:总结train.py def parse_opt(knownFalse):parser argparse.ArgumentParser()parser.add_argument(--weights, typestr, defaultROOT / yolov5s.pt, helpinitial weights path)parser.add_argument(--cfg, typestr, defau…

uniCloud在线升级APP配置教程

app在线升级背景实现思路流程流程背景 因用户需要添加手机h5页面来进数据操作实现思路流程 实现流程图流程 相关文档&#xff1a;帮助文档 https://uniapp.dcloud.net.cn/uniCloud/cf-functions.html 注册服务空间 https://unicloud.dcloud.net.cn/pages/login/login uni升级…

基于Yolv5s的口罩检测

1.Yolov5算法原理和网络结构 YOLOv5按照网络深度和网络宽度的大小&#xff0c;可以分为YO-LOv5s、YOLOv5m、YOLOv5l、YOLOv5x。本文使用YOLOv5s&#xff0c;它的网络结构最为小巧&#xff0c;同时图像推理速度最快达0.007s。YO-LOv5的网络结构主要由四部分组成&#xff0c;分别…

三天吃透MySQL八股文(2023最新整理)

本文已经收录到Github仓库&#xff0c;该仓库包含计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享等核心知识点&#xff0c;欢迎star~ Github地址&#xff1a;https://github.com/…

博客系统(界面设计)

✏️作者&#xff1a;银河罐头 &#x1f4cb;系列专栏&#xff1a;JavaEE &#x1f332;“种一棵树最好的时间是十年前&#xff0c;其次是现在” 目录实现博客列表页预期效果导航栏页面主体左右布局左侧区域右侧区域完整代码实现博客详情页预期效果导航栏 左侧右侧完整代码实现…

全国程序员薪酬大曝光!看完我酸了····

2023年&#xff0c;随着互联网产业的蓬勃发展&#xff0c;程序员作为一个自带“高薪多金”标签的热门群体&#xff0c;被越来越多的人所关注。在过去充满未知的一年中&#xff0c;他们的职场现状发生了一定的改变。那么&#xff0c;程序员岗位的整体薪资水平、婚恋现状、职业方…

认识TomcatMavenServlet第一个Servlet程序

文章目录一、什么是Tomcat、什么是Servlet二、Tomcat的下载与使用关于下载启动欢迎页面查看可能出现的问题博客系统静态页面的部署三、什么是Maven四、第一个servlet程序1.创建Maven项目2.引入依赖3.创建目录结构4.编写程序5.打包程序6.部署程序7.验证小结五、servlet程序简化版…

学习 Python 之 Pygame 开发魂斗罗(四)

学习 Python 之 Pygame 开发魂斗罗&#xff08;四&#xff09;继续编写魂斗罗1. 创建子弹类2. 根据玩家方向和状态设置子弹发射的位置(1). 站立向右发射子弹(2). 站立向左发射子弹(3). 站立朝上发射子弹(4). 蹲下发射子弹(5). 向斜方发射子弹(6). 奔跑时发射子弹(7). 跳跃时发射…

图片的美白与美化

博主简介 博主是一名大二学生&#xff0c;主攻人工智能研究。感谢让我们在CSDN相遇&#xff0c;博主致力于在这里分享关于人工智能&#xff0c;c&#xff0c;Python&#xff0c;爬虫等方面知识的分享。 如果有需要的小伙伴可以关注博主&#xff0c;博主会继续更新的&#xff0c…

Python雪花代码

前言 用python画个雪花玩玩&#xff0c;源码在文末公众号哈。 雪花类 class Snow(): #雪花类 def __init__(self): self.r 6 #雪花的半径 self.x ra.randint(-1000,1000) #雪花的横坐标 self.y ra.randint(-500,5…

读书笔记——《富爸爸穷爸爸》

《富爸爸穷爸爸》&#xff0c;以前不屑读这种书。这种书就是那种走进书店放在门口展销位的成功学著作&#xff0c;一眼看上去没什么实在的内容&#xff0c;看上去很不靠谱&#xff0c;感觉就是骗一些社会底层又做着暴富梦的人来买的&#xff0c;但是由于自身原因或环境局限根本…

MySQL基本查询

文章目录表的增删查改Create&#xff08;创建&#xff09;单行数据 全列插入多行数据 指定列插入插入否则更新替换Retrieve&#xff08;读取&#xff09;SELECT列全列查询指定列查询查询字段为表达式查询结果指定别名结果去重WHERE 条件基本比较BETWEEN AND 条件连接OR 条件连…

【面试题】Python软件工程师能力评估试题(一)

文章目录前言应试者需知&#xff08;一&#xff09;Python 语言基础能力评估1、理解问题并完成代码&#xff1a;2、阅读理解代码&#xff0c;并在空白处补充完整代码&#xff1a;3、编写一个装饰器&#xff1a;exposer4、阅读代码并在空白处补充完整代码&#xff1a;5、自行用P…

嵌入式 串口通信

目录 1、通信的基本概念 1.1 串行通信 1.2 并行通信 2、串行通信的特点 2.1 单工 2.2 半双工 2.3 全双工 3、串口在STM32的引脚 4、STM32的串口的接线 4.1 STM32的串口1和电脑通信的接线方式 4.2 单片机和具备串口的设备连接图 5、串口通信协议 6、串口通信…

linux进程管理

进程管理 进程是启动的可执行程序的一个指令 1、进程简介 &#xff08;1&#xff09;进程的组成部分 已分配内存的地址空间安全属性&#xff0c;包括所有权凭据和特权程序代码的一个或多个执行线程进程状态 &#xff08;2&#xff09;程序和进程的区别 程序是一个静态的二进制…

第十四届蓝桥杯第三期模拟赛 C/C++ B组 原题与详解

文章目录 一、填空题 1、1 找最小全字母十六进制数 1、1、1 题目描述 1、1、2 题解关键思路与解答 1、2 给列命名 1、2、1 题目描述 1、2、2 题解关键思路与解答 1、3 日期相等 1、3、1 题目描述 1、3、2 题解关键思路与解答 1、4 乘积方案数 1、4、1 题目描述 1、4、2 题解关…

28岁小公司程序员,无车无房不敢结婚,要不要转行?

大家好&#xff0c;这里是程序员晚枫&#xff0c;又来分享程序员的职场故事了~ 今天分享的这位朋友叫小青&#xff0c;我认识他2年多了。以前从事的是土木行业&#xff0c;2年前找我咨询转行程序员的学习路线和职业规划后&#xff0c;通过自学加入了一家创业公司&#xff0c;成…

分享几个常用的运维 shell 脚本

今天咸鱼给大家分享几个不错的 Linux 运维脚本&#xff0c;这些脚本中大量使用了 Linux 的文本三剑客&#xff1a; awkgrepsed 建议大家这三个工具都要了解并最好能够较为熟练的使用 根据 PID 显示进程所有信息 根据用户输入的PID&#xff0c;过滤出该PID所有的信息 #! /b…

第十四届蓝桥杯三月真题刷题训练——第 11 天

目录 第 1 题&#xff1a;卡片 题目描述 运行限制 第 2 题&#xff1a;路径_dpgcd 运行限制 第 3 题&#xff1a;字符统计 问题描述 输入格式 输出格式 样例输入 样例输出 评测用例规模与约定 运行限制 第 4 题&#xff1a;费用报销 第 1 题&#xff1a;卡片 题…

我今天要彻底搞懂线程状态的变化

&#x1f497;推荐阅读文章&#x1f497; &#x1f338;JavaSE系列&#x1f338;&#x1f449;1️⃣《JavaSE系列教程》&#x1f33a;MySQL系列&#x1f33a;&#x1f449;2️⃣《MySQL系列教程》&#x1f340;JavaWeb系列&#x1f340;&#x1f449;3️⃣《JavaWeb系列教程》…