RecyclerView源码解析(二):结合LinearLayout分析绘制流程

news2024/11/29 1:40:54

RecyclerView源码解析(二):结合LinearLayout分析绘制流程

封面:

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

导言

上篇文章中主要已经介绍了RecyclerView的View的三大工作流程(onMeasure,onLayout,onDraw),实际上看了上篇文章还是很迷糊,因为RecylerView由于实现了高度的解耦,所以阅读整个RecyclerView的源码和理解整个RecyclerView需要结合多个相关的类别。本篇文章我们将以配合我们最常用的LinearLayoutManager类来继续分析RecyclerView。上一篇文章权当是介绍一些前置概念和对整个RecyclerView有一个大致的了解吧。

源码分析

从setAdapter方法开始

这里我们以setAdapter方法为切入点,来从这个方法开始继续分析RecyclerView的工作流程:

public void setAdapter(@Nullable Adapter adapter) {
    // bail out if layout is frozen
    setLayoutFrozen(false);
    setAdapterInternal(adapter, false, true);
    processDataSetCompletelyChanged(false);
    requestLayout();
}

这里主要做了以下几步:

  • 解除Layout的Frozen状态,使之后的布局可以正常进行

  • 调用setAdapterInternal方法来设置新的适配器,这一步中将会卸载之前的适配器,清除其相关信息并且更新Recycler回收池,最后将新的适配器安装上去

  • 由于数据集发生了变化,所以需要清除掉之前的缓存,并将之前的数据标记为无效

  • 请求重新布局视图,这一步将重新触发View的三大工作流程,下面是ChatGpt的一些解释:

    调用 requestLayout() 方法后,会触发以下一系列的方法和事件,以重新布局视图:

    1. onMeasure(int, int): 这是布局传递的第一步。视图会测量自身的尺寸,确定它应该占用多少空间。
    2. onLayout(boolean, int, int, int, int): 这是布局传递的第二步。视图根据测量结果和布局参数来安排子视图的位置和大小。
    3. onDraw(Canvas): 如果视图包含绘制操作,它将在绘制阶段被调用。
    4. onSizeChanged(int, int, int, int): 如果视图的大小发生变化,这个方法会被调用。
    5. invalidate(): requestLayout() 方法通常伴随着 invalidate() 方法的调用,以便在下一个绘制帧时重新绘制视图。
    6. onLayoutChildren(RecyclerView.Recycler, RecyclerView.State): 如果是 RecyclerView 等特殊视图容器,这个方法将负责重新排列和布局子视图。

    总之,requestLayout() 方法会触发测量、布局、绘制和刷新等一系列操作,以确保视图的正确显示和布局。具体的方法调用顺序和影响取决于视图的类型和视图层次结构。

所以可以说setAdapter方法主要就是为了重置状态,设置新的适配器,请求重新进行视图布局这三大步,而在这三大步中我们最关心的无非就是重新布局。而这个重新布局的流程的大致框架我们已经在上一篇文章中介绍过了,这篇文章将会着重介绍LayoutManager将会如何实现布局。

重新进行视图布局

这其中第一步的onMeasure就不细讲了,因为LinearLayoutManager和其他两个Google官方的LayoutManager都是启动自动测量的,也没有什么额外的逻辑。我们首先最需要关注的是LayoutManager的onLayoutChildren方法,因为这个方法将会真正进行子视图的布局并且在dispatchLayoutStep1dispatchLayoutStep2方法中都会被调用到。如果我们查看LinearLayoutManager的这个方法的源码的话,在方法内容之前有巨长的一段注释:

根据翻译,这段描述涉及到RecyclerView的布局过程和动画处理:

  1. 默认情况下,RecyclerView启用了简单的项动画(simple item animations),这意味着在适配器上执行添加/移除操作时,会有动画效果,包括新添加或出现的项、移除或消失的项以及移动的项。RecyclerView默认使用非空的ItemAnimator来处理这些动画。
  2. 如果一个LayoutManager返回false,表示不支持预测性项动画(predictive item animations),这也是默认设置,那么它会在onLayoutChildren(RecyclerView.Recycler, RecyclerView.State)方法中执行普通的布局操作。在这种情况下,RecyclerView会有足够的信息来简单地运行这些动画。例如,默认的ItemAnimator(DefaultItemAnimator)只会淡入淡出视图,无论它们实际上是被添加/移除还是由于其他添加/移除操作而在屏幕上移动或移出屏幕。
  3. 如果一个LayoutManager希望提供更好的项动画体验,其中项可以根据它们在不在屏幕上的位置进行动画,那么LayoutManager应该从supportsPredictiveItemAnimations()方法中返回true,并在onLayoutChildren(RecyclerView.Recycler, RecyclerView.State)方法中添加额外的逻辑。支持预测性动画意味着onLayoutChildren(RecyclerView.Recycler, RecyclerView.State)方法将被调用两次:首先作为“预”布局步骤,用于确定在实际布局之前项的位置,然后再进行“实际”布局。在预布局阶段,项将记住它们在预布局时的位置,以便它们能够被正确地布局。此外,已移除的项将从废弃项(scrap)中返回,以帮助确定其他项的正确位置。这些已移除的项不应该添加到子项列表中,但应该用于计算其他视图的正确位置,包括之前不在屏幕上的视图(称为APPEARING视图),但可以根据有关预布局已移除视图的额外信息来确定它们的预布局位置。
  4. 第二个布局过程是实际的布局,在这个过程中只使用未被移除的视图。在这个过程中,唯一的额外要求是,如果supportsPredictiveItemAnimations()返回true,则需要注意哪些视图存在于布局之前的子项列表中,哪些在布局之后不在了(称为DISAPPEARING视图),并且需要正确地定位/布局这些视图,而不考虑RecyclerView的实际边界。这允许动画系统知道将这些消失的视图动画到的位置。

总之,RecyclerView的默认LayoutManager实现已经处理了动画的所有要求。RecyclerView的客户端可以直接使用这些LayoutManager之一,或者查看它们的onLayoutChildren()方法的实现,以了解它们如何处理APPEARING和DISAPPEARING视图。

根据这段描述,我们也可以了解到上篇文章提到的预布局和实际布局这两个布局过程的作用,预布局将会实现更好的动画效果,因为它记录了新动画开始之前的旧状态,这可以为更合理的动画迭代效果提供更多的信息。

接下来继续回到onLayoutChildren中,这个方法负责布局逻辑,大体逻辑分为四步:

  1. 首先,通过检查子项和其他变量,找到一个锚点坐标和一个锚点项的位置。
  2. 然后,从底部开始堆叠,向起始方向填充子项。
  3. 接下来,从顶部开始堆叠,向结束方向填充子项。
  4. 最后,根据需要滚动,以满足从底部堆叠等要求。

上面提到的锚点就是布局开始的位置,而锚点项正是与之对应的一个列表项。我们也可以根据这大体的四步逻辑来拆分这个方法,来看源码:

public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    ......
    if (!mAnchorInfo.mValid || mPendingScrollPosition != RecyclerView.NO_POSITION
            || mPendingSavedState != null) {
        mAnchorInfo.reset();
        mAnchorInfo.mLayoutFromEnd = mShouldReverseLayout ^ mStackFromEnd;
        // calculate anchor position and coordinate
        updateAnchorInfoForLayout(recycler, state, mAnchorInfo);
        mAnchorInfo.mValid = true;
    } else if (focused != null && (mOrientationHelper.getDecoratedStart(focused)
                    >= mOrientationHelper.getEndAfterPadding()
            || mOrientationHelper.getDecoratedEnd(focused)
            <= mOrientationHelper.getStartAfterPadding())) {
        // This case relates to when the anchor child is the focused view and due to layout
        // shrinking the focused view fell outside the viewport, e.g. when soft keyboard shows
        // up after tapping an EditText which shrinks RV causing the focused view (The tapped
        // EditText which is the anchor child) to get kicked out of the screen. Will update the
        // anchor coordinate in order to make sure that the focused view is laid out. Otherwise,
        // the available space in layoutState will be calculated as negative preventing the
        // focused view from being laid out in fill.
        // Note that we won't update the anchor position between layout passes (refer to
        // TestResizingRelayoutWithAutoMeasure), which happens if we were to call
        // updateAnchorInfoForLayout for an anchor that's not the focused view (e.g. a reference
        // child which can change between layout passes).
        mAnchorInfo.assignFromViewAndKeepVisibleRect(focused, getPosition(focused));
    }
   ......
}

这一部分的源码主要就是来寻找锚点,首先在第一行的if中会判断锚点信息是否有效(mAnchorInfo.mValid)或者是否有什么滚动状态需要更新,这个mAnchorInfo.mValid默认是false的,当完成测量后就会被置为true,当完成Layout之后又会被置为false。可以看到如果这个锚点信息无效的话就会进入这个分支中,在该分支中主要是对锚点的信息进行了计算和测量(通过updateAnchorInfoForLayout(recycler, state, mAnchorInfo)方法),然后设置该锚点信息为有效的。

对上面的一小段介绍进行总结的话,其实就是当锚点信息需要更新时,调用updateAnchorInfoForLayout(recycler, state, mAnchorInfo)来重新测量锚点信息。如果之前的锚点信息可用的话就会进入到else-if块中,可以对这个注释进行翻译和总结,其实就是对软键盘弹出等情况进行了处理,这段代码确保了当布局发生变化时,焦点视图(通常是用户正在与之交互的视图)不会因布局调整而被移出屏幕,而是会被正确地布局在可见区域内。

接下来继续往下看:

public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
    ......
    int startOffset;
    int endOffset;
    final int firstLayoutDirection;
    if (mAnchorInfo.mLayoutFromEnd) {
        firstLayoutDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_TAIL
                : LayoutState.ITEM_DIRECTION_HEAD;
    } else {
        firstLayoutDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD
                : LayoutState.ITEM_DIRECTION_TAIL;
    }
    ......
}

这段代码将会根据锚点信息中的mLayoutFromEnd参数来确定布局的方向,具体来说就是到底是从上到下还是从下到上(或者是左右)。

public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
......
onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection);
    detachAndScrapAttachedViews(recycler);
  	......
    if (mAnchorInfo.mLayoutFromEnd) {
        // fill towards start
        updateLayoutStateToFillStart(mAnchorInfo);
        mLayoutState.mExtraFillSpace = extraForStart;
        fill(recycler, mLayoutState, state, false);
        startOffset = mLayoutState.mOffset;
        final int firstElement = mLayoutState.mCurrentPosition;
        if (mLayoutState.mAvailable > 0) {
            extraForEnd += mLayoutState.mAvailable;
        }
        // fill towards end
        updateLayoutStateToFillEnd(mAnchorInfo);
        mLayoutState.mExtraFillSpace = 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.mExtraFillSpace = extraForStart;
            fill(recycler, mLayoutState, state, false);
            startOffset = mLayoutState.mOffset;
        }
    } else {
        // fill towards end
        updateLayoutStateToFillEnd(mAnchorInfo);
        mLayoutState.mExtraFillSpace = extraForEnd;
        fill(recycler, mLayoutState, state, false);
        endOffset = mLayoutState.mOffset;
        final int lastElement = mLayoutState.mCurrentPosition;
        if (mLayoutState.mAvailable > 0) {
            extraForStart += mLayoutState.mAvailable;
        }
        // fill towards start
        updateLayoutStateToFillStart(mAnchorInfo);
        mLayoutState.mExtraFillSpace = 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.mExtraFillSpace = extraForEnd;
            fill(recycler, mLayoutState, state, false);
            endOffset = mLayoutState.mOffset;
        }
    }
......
}

这段代码还是比较长的,不过结构也很清晰,主要就是两个大的分支。首先会调用onAnchorReady(recycler, state, mAnchorInfo, firstLayoutDirection),该方法主要是在锚点信息已经确定的情况下调用的,用于在锚点准备就绪时执行相关操作或更新锚点信息。

接下来会根据锚点信息中的布局方向来确定到底进入哪个分支块代码中,可以说这两段分支代码是一模一样,唯一的区别就是从如何执行fill方法。我们来以第一个分支块为例来介绍如何填充内容:

if (mAnchorInfo.mLayoutFromEnd) {
    // fill towards start
    updateLayoutStateToFillStart(mAnchorInfo);
    mLayoutState.mExtraFillSpace = extraForStart;
    fill(recycler, mLayoutState, state, false);
    startOffset = mLayoutState.mOffset;
    final int firstElement = mLayoutState.mCurrentPosition;
    if (mLayoutState.mAvailable > 0) {
        extraForEnd += mLayoutState.mAvailable;
    }
    // fill towards end
    updateLayoutStateToFillEnd(mAnchorInfo);
    mLayoutState.mExtraFillSpace = 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.mExtraFillSpace = extraForStart;
        fill(recycler, mLayoutState, state, false);
        startOffset = mLayoutState.mOffset;
    }
} 

首先我们看到会调用updateLayoutStateToFillStart(mAnchorInfo)方法,主要是用于更新LayoutState对象,以便在开始填充(fill)布局时使用。之后会正式调用fill方法进行填充,我们将着重分析这个方法:

int fill(RecyclerView.Recycler recycler, LayoutState layoutState,
        RecyclerView.State state, boolean stopOnFocusable) {
    // max offset we should set is mFastScroll + available
	......
    while ((layoutState.mInfinite || remainingSpace > 0) && layoutState.hasMore(state)) {
        layoutChunkResult.resetInternal();
		...
        layoutChunk(recycler, state, layoutState, layoutChunkResult);
		...
        if (!layoutChunkResult.mIgnoreConsumed || layoutState.mScrapList != null
                || !state.isPreLayout()) {
            layoutState.mAvailable -= layoutChunkResult.mConsumed;
            // we keep a separate remaining space because mAvailable is important for recycling
            remainingSpace -= layoutChunkResult.mConsumed;
        }
		....
        if (stopOnFocusable && layoutChunkResult.mFocusable) {
            break;
        }
    }
	......
}

这个方法首先会获取RecyclerView中的剩余可用空间,如果有可用空间且有更多列表项需要加载的话就会进入到while循环中,在这个while循环中首先重置layoutChunkResult变量,这个变量将会存储通过layoutChunk方法填充完数据之后的RecyclerView的状态。而在这个layoutChunk方法中会对内容进行填充,填充完毕之后再次计算RecyclerView的剩余可用空间。

所以说,填充内容靠的还是layoutChunk方法,在layoutChunk方法中一开始会调用到layoutState的next方法,这个方法非常重要,是用来获取下一个加载项视图的:

在这里插入图片描述

具体方法如下,非常短,主要是会跳转到Recycler的getViewForPosition方法

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

这个方法中就会涉及到RecyclerView的缓存机制了,不过在这篇文章中我们先不分析缓存机制,下篇文章再介绍。接下来继续回到next方法中,获得到了这个View之后,next中就会将当前的位置进行更新,更新值即为布局方向,我们也可以看看这两个常量的值:
在这里插入图片描述

在这里插入图片描述

可以看到,如果是TAIL(尾部)的话,该常量为1,否则就为-1。回到之前的layoutChildren中回顾一下这个值是在什么时候被赋予的:

if (mAnchorInfo.mLayoutFromEnd) {
    firstLayoutDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_TAIL
            : LayoutState.ITEM_DIRECTION_HEAD;
} else {
    firstLayoutDirection = mShouldReverseLayout ? LayoutState.ITEM_DIRECTION_HEAD
            : LayoutState.ITEM_DIRECTION_TAIL;
}

可以看到,如果是从底部开始填充且不需要反转布局方向的话,那么此时的方向就会被赋予为LayoutState.ITEM_DIRECTION_HEAD即为-1,这个数字有什么意义呢?大家可以结合这一张图理解(摘自【进阶】RecyclerView源码解析(一)——绘制流程 - 简书 (jianshu.com)):

img

可以看到这种情况即为第一张图的情况,此时的positon当然是每次-1来达到从底部开始向上布局的效果。

layoutChunk方法中获取到了下一次需要填充的视图之后,我们再来看看接下来该方法的逻辑:

void layoutChunk(RecyclerView.Recycler recycler, RecyclerView.State state,
        LayoutState layoutState, LayoutChunkResult result) {
    View view = layoutState.next(recycler);
    ......
    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);
        }
    }
    ......
}

接下来会获取到RecyclerView的布局参数,并且根据布局方向来通过addView方法正式添加视图,这个插入的位置也和方向有关,如果要从底部开始布局的话就会直接调用addView(view)方法在底部插入,否则就会调用到addView(view, 0)方法从头部插入。继续向下走:

measureChildWithMargins(view, 0, 0);
result.mConsumed = mOrientationHelper.getDecoratedMeasurement(view);
int left, top, right, bottom;
if (mOrientation == VERTICAL) {
    if (isLayoutRTL()) {
        right = getWidth() - getPaddingRight();
        left = right - mOrientationHelper.getDecoratedMeasurementInOther(view);
    } else {
        left = getPaddingLeft();
        right = left + mOrientationHelper.getDecoratedMeasurementInOther(view);
    }
    if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
        bottom = layoutState.mOffset;
        top = layoutState.mOffset - result.mConsumed;
    } else {
        top = layoutState.mOffset;
        bottom = layoutState.mOffset + result.mConsumed;
    }
} else {
    top = getPaddingTop();
    bottom = top + mOrientationHelper.getDecoratedMeasurementInOther(view);

    if (layoutState.mLayoutDirection == LayoutState.LAYOUT_START) {
        right = layoutState.mOffset;
        left = layoutState.mOffset - result.mConsumed;
    } else {
        left = layoutState.mOffset;
        right = layoutState.mOffset + result.mConsumed;
    }
}

首先会触发measureChildWithMargins(view, 0, 0)方法,而在这个方法中又会调用到getItemDecorInsetsForChild并最终触发到mItemDecorations.get(i).getItemOffsets(mTempRect, child, this, mState)方法,而这个方法正是我们在设置分割线时需要重写的,它默认返回一个宽度和高度均为0dp的矩形,实际上就是没有。当我们重写了该方法后分割线就会显现出来了。

接下来又会根据LinearLayoutManager设置的排列方向以及isLayoutRTL的标志位来确定当前的view在相对坐标系中的left,right,top以及bottom坐标。在之后也会用到这四个坐标。

// We calculate everything with View's bounding box (which includes decor and margins)
// To calculate correct layout position, we subtract margins.
layoutDecoratedWithMargins(view, left, top, right, bottom);
if (DEBUG) {
    Log.d(TAG, "laid out child at position " + getPosition(view) + ", with l:"
            + (left + params.leftMargin) + ", t:" + (top + params.topMargin) + ", r:"
            + (right - params.rightMargin) + ", b:" + (bottom - params.bottomMargin));
}
// Consume the available space if the view is not removed OR changed
if (params.isItemRemoved() || params.isItemChanged()) {
    result.mIgnoreConsumed = true;
}
result.mFocusable = view.hasFocusable();

这是layoutChunk方法的最后一部分,可以看到首先是用layoutDecoratedWithMargins(view, left, top, right, bottom)方法将之前计算出来的坐标传入以此来在当前的RecyclerView中布局当前的列表项view,具体来说就是调用到了View的layout方法了。到这里为止这个子方法我们也已经介绍完毕了。

最后让我们回到layoutChildren方法,实际上在填充过程中会进行两次甚至是三次的fill填充:

if (mAnchorInfo.mLayoutFromEnd) {
    // fill towards start
    updateLayoutStateToFillStart(mAnchorInfo);
    mLayoutState.mExtraFillSpace = extraForStart;
    fill(recycler, mLayoutState, state, false);
    startOffset = mLayoutState.mOffset;
    final int firstElement = mLayoutState.mCurrentPosition;
    if (mLayoutState.mAvailable > 0) {
        extraForEnd += mLayoutState.mAvailable;
    }
    // fill towards end
    updateLayoutStateToFillEnd(mAnchorInfo);
    mLayoutState.mExtraFillSpace = 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.mExtraFillSpace = extraForStart;
        fill(recycler, mLayoutState, state, false);
        startOffset = mLayoutState.mOffset;
    }
}

通过注释我们也可以看出来,一开始是向头来填充,第二次是向底部来填充。如果还有剩余空间的话则会进行第三次填充。这一切都填充完毕后,如果有需要的话还有调用layoutForPredictiveAnimations(recycler, state, startOffset, endOffset)来执行一些预测性动画。

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

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

相关文章

数学思维导图怎么绘制?这个详细绘制方法了解一下

数学思维导图怎么绘制&#xff1f;数学思维导图是数学学习中的一种重要辅助工具。在复杂的数学问题中&#xff0c;思维导图可以帮助学生更好地理解和组织各个知识点&#xff0c;从而更好地解决问题。在绘制数学思维导图时&#xff0c;有很多工具可供选择&#xff0c;下面就给大…

港陆证券:政策累积效应催生A股普涨行情 北证50指数创最大单日涨幅

周一&#xff0c;在活泼资本商场政策继续推出、不断累积的布景下&#xff0c;A股商场迎来久别的普涨行情。金融、白酒与资源类板块集体发力&#xff0c;带动沪深主板指数高开高走。北交所商场体现更为亮眼&#xff0c;北证50指数收盘大涨5.92%&#xff0c;创该指数前史最大单日…

CSS中图片旋转超出父元素解决办法

下面的两种解决办法都会导致图片缩小&#xff0c;可以给图片进行初始化的宽高设置 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta http-equiv"X-UA-Compatible" content"IEedge">…

NIO原理浅析(三)

epoll 首先认识一下epoll的几个基础函数 int s socket(AF_INET, SOCK_STREAM, 0); bind(s, ...); listen(s, ...);int epfd epoll_create(...) epoll_ctl(epfd, ...); //将所有需要监听的socket添加到epfd中while(1) {int n epoll_wait(...);for(接受到数据的socket) {//处…

el-table 垂直表头

效果如下&#xff1a; 代码如下&#xff1a; <template><div class"vertical_head"><el-table style"width: 100%" :data"getTblData" :show-header"false"><el-table-columnv-for"(item, index) in getHe…

【javascript】禁止浏览器调试前端页面

目录 为啥要禁止&#xff1f;无限 debugger基础禁止调试解决对策 为啥要禁止&#xff1f; 由于前端页面会调用很多接口&#xff0c;有些接口会被别人爬虫分析&#xff0c;破解后获取数据&#xff0c;为了杜绝这种情况&#xff0c;最简单的方法就是禁止人家调试自己的前端代码 …

成都瀚网科技有限公司:抖店怎么开通直播?

随着互联网和移动支付的快速发展&#xff0c;越来越多的人选择开设自己的抖音商店。抖音作为国内最受欢迎的短视频平台之一&#xff0c;拥有庞大的用户基础&#xff0c;成为众多创业者青睐的平台。那么&#xff0c;如何经营自己的抖音店铺呢&#xff1f;下面将从几个方面为您介…

C# 中什么是重写(子类改写父类方法)

方法重写是指在继承关系中&#xff0c;子类重新实现父类或基类的某个方法。这种方法允许子类根据需要修改或扩展父类或基类的方法功能。在面向对象编程中&#xff0c;方法重写是一种多态的表现形式&#xff0c;它使得子类可以根据不同的需求和场景提供不同的方法实现。 方法重…

设计实现QSPI Flash的下载算法

mm32-2nd-bootloader技术白皮书&#xff08;3&#xff09;——设计实现QSPI Flash的下载算法 mm32-2nd-bootloader技术白皮书&#xff08;3&#xff09;——设计实现QSPI Flash的下载算法 | MCU加油站 cathy 发布于&#xff1a;周一, 03/20/2023 - 15:29 &#xff0c;关键词&a…

变压器智能在线监测

变压器智能在线监测是一种先进的变压器状态监测方式&#xff0c;采用了先进的技术手段&#xff0c;能够对变压器运行状态进行实时、在线、自动的监测和分析。可以在变压器运行过程中&#xff0c;对其电压、电流、温度、局放等参数进行实时监测&#xff0c;及时发现异常情况并进…

为何跨境界如此看重黑色星期五?这一点是关键

黑色星期五通常会利用折扣和独一无二的优惠为电子商务商店带来大量新客户。黑色星期五可能会给您的在线商店带来丰厚的利润&#xff0c;如果无法留住这些客户&#xff0c;您就会损失巨大。通过对电子商务客户服务策略进行一些调整&#xff0c;您可以将这些黑色星期五客户转变为…

2023最新多功能XL软件库APP源码+PHP后端系统源码/功能强大/软件库自带后台管理系统

2023最新多功能XL软件库APP源码PHP后端系统源码/功能强大/软件库自带后台管理系统31xl软件库最新可以正常使用版: https://url11.ctfile.com/d/25976711-57801726-b66bb0?p6724 &#xff08;访问密码&#xff1a;6724&#xff09; 多功能软件库&#xff0c;支持自定义对接易支…

LiveGBS流媒体平台GB/T28181功能-支持数据库切换为高斯数据库信创瀚高数据信创数据库

LiveGBS流媒体平台GB/T28181功能-支持数据库切换为高斯数据库信创瀚高数据信创数据库 1、如何配置切换高斯数据库&#xff1f;2、如何配置切换信创瀚高数据库&#xff1f;3、搭建GB28181视频直播平台 1、如何配置切换高斯数据库&#xff1f; livecms.ini -> [db]下面添加配…

JavaScript - 好玩的打字动画

效果预览&#xff1a; &#x1f680;HTML版本 <!DOCTYPE html> <html> <head><title>打字动画示例</title><style>.typewriter {color: #000;overflow: hidden; /* 隐藏溢出的文本 */white-space: nowrap; /* 不换行 */border-right: .…

macOS通过钥匙串访问找回WiFi密码的详细教程

如果您忘记了Mac电脑上的WiFi密码&#xff0c;可以通过钥匙串访问来找回它。具体步骤如下&#xff1a; 1.打开Mac电脑的“启动台”&#xff0c;然后在其他文件中找到“钥匙串访问”。 2.运行“钥匙串访问”应用程序&#xff0c;点击左侧的“系统”&#xff0c;然后在右侧找到…

FT232RL国产替代芯片GP232RL无需软硬件修改资料

GP232RL是最新加入 ftdi 系列 usb 接口集成电路设备的设备。 GP232RL是一个 usb 到串行 uart 接口&#xff0c;带有可选的时钟发生器输出&#xff0c;以及新的 ftdichip-idTM 安全加密器特性。此外&#xff0c;还提供了异步和同步位崩接口模式。 通过将外部 eeprom、时钟电路和…

Android Studio更新新版本后无法创建flutter项目

最新更新了AndroidStudio版本&#xff0c;发现无法创建flutter项目。 dart和flutter插件确认都已安装&#xff0c;该有的环境配置都已配置。 最后与同事的插件作比较发现是Android APK Support这个插件没勾选。 勾选后&#xff0c;点击右下角的apply&#xff0c;重启AndroidS…

vue+elementUI el-select 自定义搜索逻辑(filter-method)

下拉列表的默认搜索是搜索label显示label,我司要求输入id显示label名称 <el-form-item label"部门&#xff1a;"><el-select v-model"form.region1" placeholder"请选择部门" filterable clearable:filter-method"dataFilter&qu…

手写apply方法

<script>/** 手写apply方法 * */Function.prototype.myApply function (context, args) {console.log(this, sss)//fnconst key Symbol()context[key] thiscontext[key](...args)delete context[key]return context[key]}const obj {name: zs,age: 18}function fn …

保姆级 C++ 学习路线

上周有小伙伴留言求安排一手C/C学习路线&#xff0c;这周一份保姆级的C语言安排上&#xff01; 以前就写过C语言的学习路线&#xff1a;可能是北半球最好的零基础C语言学习路线&#xff0c;这次把C的学习路线也安排上&#xff0c;专门花了一个多月写了这篇学习路线&#xff0c;…