RecyclerView源码解析(四):RecyclerView对ViewHolder的回收

news2024/12/29 10:05:47

RecyclerView源码解析(四):RecyclerView对ViewHolder的回收

在这里插入图片描述

导言

前面几篇文章我们已经介绍了RecyclerView绘图的三大流程和其四层缓存机制,不过对于我来说还有一个疑问,那就是RecyclerView是如何将分离的ViewHolder进行回收的,最终又回收到了哪一层缓存之中。所以本篇文章就是为了解决这个问题。

创建ViewHolder和回收ViewHolder的方法

在缓存机制中我们提到过了,RecyclerView尝试获取ViewHolder并不是一开始就直接创建一个ViewHolder的,它会首先尝试从它的四层缓存中获取一个可用的ViewHolder,如果没有找到,最后才会调用方法创建一个ViewHolder,而这个方法正是onCreateViewHolder,也就是我们在适配器中重写的方法。所以每次创建ViewHolder时才会调用该方法。

从我具体测试的结果来看当我们一共有五个数据项的时候RecyclerView只会创建四个ViewHolder,也就是说有一个数据项的显示会通过ViewHolder复用的方式来显示。**也就是说,之前必定有至少一个ViewHolder被回收了。**这个回收过程在哪里呢?我们可以从RecyclerView中可重写的方法中找到突破口。

有以下五个比较重要的回调,下面由我的蹩脚英语进行翻译:

  • onAttachedToRecyclerView(recyclerView: RecyclerView):当RecyclerView开始监听其Adapter时调用。
  • onDetachedFromRecyclerView(recyclerView: RecyclerView):当RecyclerView不再监听其Adapter时调用。
  • onViewAttachedToWindow(holder: ViewHolder):当由适配器创建的View被添加到Window时调用,这可以被视为是用户即将见到该View的标志。在onViewDetachedFromWindow(holder: ViewHolder)释放的资源应当在这里被恢复
  • onViewDetachedFromWindow(holder: ViewHolder):当由适配器创建的View被从Window分离时调用,这可能并不是一个永久的状态,RecyclerView可能会选择缓存这些被分离的不可见的View。
  • onViewRecycled(holder: ViewHolder):当ViewHolder被RecyclerView回收时调用。当LayoutManager判断一个View不再需要被绑定在其父RecyclerView时,这个View就会被回收。LayoutManager这样判断的原因可能是因为该View消失在视线之外或者或者是因为一组被缓存的视图仍然由附加到父 RecyclerView 的视图表示。RecyclerView将在清除Holder内部的数据和将其发送到RecycledViewPool之前调用该方法。

这五个方法之中显然最后一个方法关联度是最高的,不过其后面的那一句“或者是因为一组被缓存的视图仍然由附加到父 RecyclerView 的视图表示”的意思可能比较迷惑。在 RecyclerView 中,为了提高性能,通常会维护一个视图池(View Pool),用于存储那些已经创建但当前不可见的视图。这些视图池中的视图仍然附加(attached)到父 RecyclerView,但它们当前可能不可见,因为可能滚动到了屏幕之外或者被遮挡住了。当用户滚动 RecyclerView 时,系统会检查这个视图池,看看是否有可以重用的视图,而不是每次都创建新的视图。这就是所谓的 “缓存的视图”,它们被缓存起来以备将来使用。当需要显示新的数据时,可以从视图池中获取这些缓存的视图,并根据新的数据进行更新,而不需要重新创建整个视图。这样可以显著提高性能,因为重用现有视图比创建新视图要快得多。

因此,这句话的含义是,RecyclerView 中的视图可能不可见,要么因为已经滚动到屏幕之外,要么因为被其他视图遮挡住了,但它们仍然存在于 RecyclerView 的视图池中,可以被重用,以提高性能。

源码分析

分离视图

在上面的回调中onViewDetachedFromWindow(holder: ViewHolder)是用来分离视图的,所以我们可以从这个方法入手看看RecyclerView中哪里调用了该方法。层层向前推,可以发现这个方法会指向RecyclerView中的一个回调类中:

    private final ViewInfoStore.ProcessCallback mViewInfoProcessCallback =
            new ViewInfoStore.ProcessCallback() {
                @Override
                public void processDisappeared(ViewHolder viewHolder, @NonNull ItemHolderInfo info,
                        @Nullable ItemHolderInfo postInfo) {
                    mRecycler.unscrapView(viewHolder);
                    animateDisappearance(viewHolder, info, postInfo);
                }
                @Override
                public void processAppeared(ViewHolder viewHolder,
                        ItemHolderInfo preInfo, ItemHolderInfo info) {
                    animateAppearance(viewHolder, preInfo, info);
                }

                @Override
                public void processPersistent(ViewHolder viewHolder,
                        @NonNull ItemHolderInfo preInfo, @NonNull ItemHolderInfo postInfo) {
                    viewHolder.setIsRecyclable(false);
                    if (mDataSetHasChangedAfterLayout) {
                        // since it was rebound, use change instead as we'll be mapping them from
                        // stable ids. If stable ids were false, we would not be running any
                        // animations
                        if (mItemAnimator.animateChange(viewHolder, viewHolder, preInfo,
                                postInfo)) {
                            postAnimationRunner();
                        }
                    } else if (mItemAnimator.animatePersistence(viewHolder, preInfo, postInfo)) {
                        postAnimationRunner();
                    }
                }
                @Override
                public void unused(ViewHolder viewHolder) {
                    mLayout.removeAndRecycleView(viewHolder.itemView, mRecycler);
                }
            };

会指向最后的一个unused(ViewHolder viewHolder)方法中,这个方法会调用mLayout.removeAndRecycleView(viewHolder.itemView, mRecycler)方法,最终会触发onViewDetachedFromWindow(holder: ViewHolder)方法。这个方法最后会在我们之前在布局过程中说过的dispatchLayoutStep3()中触发,最终触发process方法:

    void process(ProcessCallback callback) {
        for (int index = mLayoutHolderMap.size() - 1; index >= 0; index--) {
            final RecyclerView.ViewHolder viewHolder = mLayoutHolderMap.keyAt(index);
            final InfoRecord record = mLayoutHolderMap.removeAt(index);
            if ((record.flags & FLAG_APPEAR_AND_DISAPPEAR) == FLAG_APPEAR_AND_DISAPPEAR) {
                // Appeared then disappeared. Not useful for animations.
                callback.unused(viewHolder);
            } else if ((record.flags & FLAG_DISAPPEARED) != 0) {
                // Set as "disappeared" by the LayoutManager (addDisappearingView)
                if (record.preInfo == null) {
                    // similar to appear disappear but happened between different layout passes.
                    // this can happen when the layout manager is using auto-measure
                    callback.unused(viewHolder);
                } else {
                    callback.processDisappeared(viewHolder, record.preInfo, record.postInfo);
                }
            } else if ((record.flags & FLAG_APPEAR_PRE_AND_POST) == FLAG_APPEAR_PRE_AND_POST) {
                // Appeared in the layout but not in the adapter (e.g. entered the viewport)
                callback.processAppeared(viewHolder, record.preInfo, record.postInfo);
            } else if ((record.flags & FLAG_PRE_AND_POST) == FLAG_PRE_AND_POST) {
                // Persistent in both passes. Animate persistence
                callback.processPersistent(viewHolder, record.preInfo, record.postInfo);
            } else if ((record.flags & FLAG_PRE) != 0) {
                // Was in pre-layout, never been added to post layout
                callback.processDisappeared(viewHolder, record.preInfo, null);
            } else if ((record.flags & FLAG_POST) != 0) {
                // Was not in pre-layout, been added to post layout
                callback.processAppeared(viewHolder, record.preInfo, record.postInfo);
            } else if ((record.flags & FLAG_APPEAR) != 0) {
                // Scrap view. RecyclerView will handle removing/recycling this.
            } else if (DEBUG) {
                throw new IllegalStateException("record without any reasonable flag combination:/");
            }
            InfoRecord.recycle(record);
        }
    }

这个方法就是通过判断FLAG标志位来决定到底回调哪个方法,可以发现当FLAG的值为FLAG_DISAPPEARED或者FLAG_APPEAR_AND_DISAPPEAR时会触发上面的unused方法,总的来说就是当View消失(DISAPPEARED)时就会触发这个方法。也许你可能会有疑问:“dispatchLayout这一系列方法不是在布局中调用的吗,难道会反复进行布局吗?”实际上dispatchLayout方法除了在layout中触发之外还会在scrollByInternal方法中触发,显然这是滑动时会调用到的方法,在onTouchEvent中执行。所以说,每当我们滑动时会先执行dispatchLayout方法,然后在这其中一旦当前View为不可见状态就会触发unused方法,最后回调到onViewDetachedFromWindow(holder: ViewHolder)中去。整个调用链如下所示:
在这里插入图片描述

回收视图

至于这个分离视图的过程到底做了什么,我们再来继续往前看,之前提到unused回调,该回调如下所示:

public void unused(ViewHolder viewHolder) {
    mLayout.removeAndRecycleView(viewHolder.itemView, mRecycler);
}

可以看到会触发mLayout.removeAndRecyclerView方法,实际上我们上面给的那张图进行了一定的简化,我们将其详细化,如下:
在这里插入图片描述
可以看到unused方法出发了mLayout.removeAndRecycleView,这个方法是有默认实现的:

public void removeAndRecycleView(@NonNull View child, @NonNull Recycler recycler) {
    removeView(child);
    recycler.recycleView(child);
}

可以看到他在触发removeView方法之后还触发了recycler自身的recycleView方法,不出所料这块应该就是重头戏了,该方法的注释:
在这里插入图片描述
注释上也写得很清楚了,该方法是用于回收一个分离的view的,特定的view还会被添加进入view Pool(视图池)中以便之后的重新绑定数据和复用。一个View在被回收之前必须被完全地从父Recycler中分离。当一个View被报废时,它将从scrap List中被移除。接下来看其方法:

public void recycleView(@NonNull View view) {
            // This public recycle method tries to make view recycle-able since layout manager
            // intended to recycle this view (e.g. even if it is in scrap or change cache)
            ViewHolder holder = getChildViewHolderInt(view);//获得目标View的ViewHolder
            if (holder.isTmpDetached()) {//判断该holder是不是暂时分离状态
                removeDetachedView(view, false);//移除分离的view,将触发ViewGroup的removeDetachedView方法
            }
            if (holder.isScrap()) {//判断holder是否有mScrapContainer,这是一个Recycler类
                holder.unScrap();//将其从mScrapContainer中移除
            } else if (holder.wasReturnedFromScrap()) {
                holder.clearReturnedFromScrapFlag();
            }
            recycleViewHolderInternal(holder);
            // In most cases we dont need call endAnimation() because when view is detached,
            // ViewPropertyAnimation will end. But if the animation is based on ObjectAnimator or
            // if the ItemAnimator uses "pending runnable" and the ViewPropertyAnimation has not
            // started yet, the ItemAnimatior on the view may not be cleared.
            // In b/73552923, the View is removed by scroll pass while it's waiting in
            // the "pending moving" list of DefaultItemAnimator and DefaultItemAnimator later in
            // a post runnable, incorrectly performs postDelayed() on the detached view.
            // To fix the issue, we issue endAnimation() here to make sure animation of this view
            // finishes.
            //
            // Note the order: we must call endAnimation() after recycleViewHolderInternal()
            // to avoid recycle twice. If ViewHolder isRecyclable is false,
            // recycleViewHolderInternal() will not recycle it, endAnimation() will reset
            // isRecyclable flag and recycle the view.
            if (mItemAnimator != null && !holder.isRecyclable()) {
                mItemAnimator.endAnimation(holder);
            }
        }

看起来这个方法大段大段的都是注释,首先开头的这两句的意思是:“当LayoutManager想要回收这个view时,该公共方法将尝试使该view变成可回收的。”上面的holder.isScrap()判断报废状态以我的理解holder的第一级缓存是mAttachedScrap,这个mAttachedScrap是被装在一个Recycler中的,所以holder需要通过这个Recycler才能找到这个mAttachedScrap,这时候就需要一个变量来指向存储holder的Recycler,这个参数就是ScrapContainer。当一个ViewHolder被添加进入一个mAttachedScrap时同时还需要给它设置一个ScrapContainer来指向存储mAttachedScrap的Recycler。也就是说这个holder.isScrap()判断的就是当前holder是否在第一级缓存mAttachedScrap中的。

好了,继续回到正题接下来会调用recycleViewHolderInternal(holder)方法进一步回收holder,进入到该方法中:

        void recycleViewHolderInternal(ViewHolder holder) {
        ......
            final boolean transientStatePreventsRecycling = holder
                    .doesTransientStatePreventRecycling();
            @SuppressWarnings("unchecked")
            final boolean forceRecycle = mAdapter != null //是否强制回收
                    && transientStatePreventsRecycling
                    && mAdapter.onFailedToRecycleView(holder);
            boolean cached = false;
            boolean recycled = false;
			......
            if (forceRecycle || holder.isRecyclable()) { //如果强制回收或者可回收的话
                if (mViewCacheMax > 0//判断,如果ViewHolder仍然有效的话
                        && !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID 
                        | ViewHolder.FLAG_REMOVED
                        | ViewHolder.FLAG_UPDATE
                        | ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
                    // Retire oldest cached view
                    int cachedViewSize = mCachedViews.size();//获得二级缓存CachedView的大小
                    if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) { //如果CachedView的大小已经超出了限制
                        recycleCachedViewAt(0); //将CachedView中的第一个元素回收
                        cachedViewSize--;
                    }

                    int targetCacheIndex = cachedViewSize; //CachedView中的目标下标,也就是最后一位
                    if (ALLOW_THREAD_GAP_WORK
                            && cachedViewSize > 0
                            && !mPrefetchRegistry.lastPrefetchIncludedPosition(holder.mPosition)) {
                        // when adding the view, skip past most recently prefetched views
                        int cacheIndex = cachedViewSize - 1;
                        while (cacheIndex >= 0) {
                            int cachedPos = mCachedViews.get(cacheIndex).mPosition;
                            if (!mPrefetchRegistry.lastPrefetchIncludedPosition(cachedPos)) {
                                break;
                            }
                            cacheIndex--;
                        }
                        targetCacheIndex = cacheIndex + 1;//更新目标下标
                    }
                    mCachedViews.add(targetCacheIndex, holder);//将holder添加进入二级缓存中
                    cached = true;//将cached标志位置为true,代表该holder成功添加入二级缓存中
                }
                if (!cached) {//如果缓存进一级缓存时失败
                    addViewHolderToRecycledViewPool(holder, true);//将其添加进入RecycledViewPool四级缓存中
                    recycled = true;//将recycled标志位置为true,代表成功添加入四级缓存中
                }
            } else {
                // NOTE: A view can fail to be recycled when it is scrolled off while an animation
                // runs. In this case, the item is eventually recycled by
                // ItemAnimatorRestoreListener#onAnimationFinished.

                // TODO: consider cancelling an animation when an item is removed scrollBy,
                // to return it to the pool faster
                if (DEBUG) {
                    Log.d(TAG, "trying to recycle a non-recycleable holder. Hopefully, it will "
                            + "re-visit here. We are still removing it from animation lists"
                            + exceptionLabel());
                }
            }
            // even if the holder is not removed, we still call this method so that it is removed
            // from view holder lists.
            mViewInfoStore.removeViewHolder(holder);
            if (!cached && !recycled && transientStatePreventsRecycling) {//如果既没有加入二级缓存也没有加入四级缓存
                holder.mOwnerRecyclerView = null;//说明当前holder已经没有宿主RecyclerView了
            }
        }

上面方法中重要的部分我都已经注释出来了简单来说就是先尝试将回收的holder加入二级缓存mCachedView中,如果mCachedView已满就将第一个元素移除再添加。如果缓存二级缓存失败的话再尝试调用addViewHolderToRecycledViewPool尝试将其加入到四级缓存中。

说到现在我们发现这个回收方法只涉及到了第二级缓存和第四级缓存,除去我们自定义的第三级缓存,那第一级缓存去哪里了?实际上,具体将View添加进入第一级缓存需要调用scrapView方法:

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

这个方法向前追踪的话是指向了getScrapOrHiddenOrCachedHolderForPosition,这个方法也是在布局过程中提到的:

ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
            final int scrapCount = mAttachedScrap.size();
			
			//尝试从第一级缓存中获取holder,如果获取成功就直接返回
			........
			//若没有在第一级缓存中找到holder
            if (!dryRun) {
                View view = mChildHelper.findHiddenNonRemovedView(position);//找到隐藏的且没有被移除的View
                if (view != null) {//如果View不为空
                    // This View is good to be used. We just need to unhide, detach and move to the
                    // scrap list.
                    final ViewHolder vh = getChildViewHolderInt(view);//将其包装成一个ViewHolder
                    mChildHelper.unhide(view);//解除其隐藏状态
                    int layoutIndex = mChildHelper.indexOfChild(view);
                    if (layoutIndex == RecyclerView.NO_POSITION) {
                        throw new IllegalStateException("layout index should not be -1 after "
                                + "unhiding a view:" + vh + exceptionLabel());
                    }
                    mChildHelper.detachViewFromParent(layoutIndex);//将其从父RecylerView中分离
                    scrapView(view);//将其添加进入第一级缓存中
                    vh.addFlags(ViewHolder.FLAG_RETURNED_FROM_SCRAP
                            | ViewHolder.FLAG_BOUNCED_FROM_HIDDEN_LIST);
                    return vh;
                }
            }

            ..........
            }
            return null;
        }

这里我们也截取最重要的部分并且我已经加入了部分注释了。可以看到这所有的第一级缓存中的ViewHolder的来源都是一个叫做mHiddenViews中的List中获取的。这个mHiddenViews中的View按照我的理解都是已经不可见但是仍未从被从RecyclerView分离的View的列表。所以RecyclerView将ViewHolder回收进这三级缓存的逻辑已经很清楚了。

总结

我们最后来总结一下RecyclerView回收ViewHolder的逻辑,首先RecyclerView尝试填充内容时会先尝试从这四级缓存中获取可用的ViewHolder,首先就是从第一级缓存mScrapViews中查找,如果找到了就直接返回;如果第一级缓存中没有可用的Holder,那么接下来RecyclerView还会额外查看是否有不可见但是仍然附加在RecyclerView的View,如果有的话就将其包装成Holder添加进入第一级缓存中,并将其从之前的父RecyclerView中分离,这就是第一级缓存的来源

之后在RecyclerView的滑动过程中又有一个新的View不可见的话就会触发新的回调,该回调中首先会判断当前的View是否已经被第一级缓存存储了,如果已经被第一级缓存存储就直接返回;否则就会先尝试将其回收进入第二级缓存中,当第二级缓存中缓存失败时又会尝试进行第四级缓存;至于第三级缓存是我们自定义的,一般也不用。

最后上一张总结图:
在这里插入图片描述

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

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

相关文章

MATLAB:电机控制(Motor Control)

目录 前言1、Overview1.1、Specific objectives1.2、Resources1.3 、Industrial context1.4 、Architecture of the system1.5 、Approach/Steps 2、MODELLING THE MOTOR AND THE LOAD2.1Simulate a DC motor by its physical model2.2、Modelling the motor / load by its mat…

堆内存与栈内存

文章目录 1. 栈内存2. 堆内存3. 区别和联系参考资料 1. 栈内存 栈内存是为线程留出的临时空间 每个线程都有一个固定大小的栈空间&#xff0c;而且栈空间存储的数据只能由当前线程访问&#xff0c;所以它是线程安全的。栈空间的分配和回收是由系统来做的&#xff0c;我们不需…

《C++ primer plus》精炼(OOP部分)——对象和类(5)

“学习是照亮心灵的火炬&#xff0c;它永不熄灭&#xff0c;永不止息。” 文章目录 类的自动和强制类型转换原始类型转换为自定义类型将自定义类型转换为原始类型 类的自动和强制类型转换 原始类型转换为自定义类型 可以用一个参数的构造函数来实现&#xff0c;例如&#xff…

【Linux操作系统】信号的产生捕获

&#x1f525;&#x1f525; 欢迎来到小林的博客&#xff01;&#xff01;       &#x1f6f0;️博客主页&#xff1a;✈️林 子       &#x1f6f0;️博客专栏&#xff1a;✈️ Linux       &#x1f6f0;️社区 :✈️ 进步学堂       &#x1f6f0…

destoon关于archiver归档的性能优化

今天在处理一个项目时候发现archiver单个模块归档超过百万数据&#xff0c;打开速度就特慢&#xff0c;所以打开archiver下index.php文件进行分析&#xff0c;发现有句sql作怪&#xff0c;查询需要三四分钟&#xff0c;所以要修改这个。 $result $db->query("SELECT …

login (mobile email usercode)

手机、邮箱、用户编码登录 package login;/*** 常量类(改造拉到你自己项目里面)** author ZengWenFeng* date 2023.09.17* email 117791303qq.com* mobile 13805029595*/ public class Constant {// 登录界面的登录方式&#xff1a;手机、邮箱、用户名public static final Str…

YOLOv8学习

1 YOLOv8训练自己的数据集 1.1 部署和运行 1.1.1 第一步&#xff0c;下载代码 源码地址 1.1.2 第二步&#xff0c;创建环境 默认已经安装好conda conda create -n yolopy38 python3.8conda activate yolopy38安装所需要的包&#xff0c;先cd到代码目录下 pip install -r…

深眸科技自研工业AI视觉检测设备,检测精度99.9%加速智造进程

随着机器视觉技术的持续升级&#xff0c;国内制造行业不断发展&#xff0c;工艺水平持续优化&#xff0c;产品的数量和种类与日俱增。当前社会将产品质量标准提高&#xff0c;在满足正常的性能使用外&#xff0c;还需要具有良好的表面外观质量。但在工业制造过程中&#xff0c;…

【C语言】【数据存储】用%d打印char类型数据,猜结果是啥

题目代码如下&#xff1a; #include <stdio.h> int main() {char a -1;signed char b-1;unsigned char c-1;printf("a%d,b%d,c%d",a,b,c);return 0; }解题关键&#xff1a; 1.二进制存储&#xff1a;原码&#xff0c;反码&#xff0c;补码 互换 2.截断 3.整型…

密码学入门——Feistel网络

文章目录 参考书一、引入二、Feistel网络计算流程三、Feistel网络的解密四、Feistel网络的性质 参考书 图解密码技术&#xff0c;第三版 一、引入 DES的基本结构是由Horst Feistel设计的&#xff0c;因此也被成为Feistel网络&#xff08;Feistel结构&#xff0c;Feistel密码…

《动手学深度学习 Pytorch版》 6.6 卷积神经网络

import torch from torch import nn from d2l import torch as d2l6.6.1 LeNet LetNet-5 由两个部分组成&#xff1a; - 卷积编码器&#xff1a;由两个卷积核组成。 - 全连接层稠密块&#xff1a;由三个全连接层组成。模型结构如下流程图&#xff08;每个卷积块由一个卷积层、…

分布式电源接入对配电网影响分析(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

汽车电子——产品标准规范汇总和梳理(电磁兼容)

文章目录 前言 一、辐射发射 二、传导发射 三、沿电源线瞬态传导发射 四、辐射抗扰 五、大电流注入抗扰 六、磁场抗扰 七、便携式发射机抗扰 八、沿电源线瞬态抗扰 九、非电源线瞬态抗扰 十、电快速脉冲群抗扰 十一、静电抗扰 总结 前言 见《汽车电子——产品标准规…

Oracle数据库安装卸载

文章目录 一、数据库版本说明二、软件的下载和安装1.下载软件2.安装数据3.创建数据库4.PLSQL 三、数据库的卸载1.关闭相关服务3.卸载软件3.删除注册信息 四、用户和权限 一、数据库版本说明 1998年Oracle8i&#xff1a;i指internet&#xff0c;表示oracle向互联网发展&#xf…

bootstrap柵格

.col-xs- 超小屏幕 手机 (<768px) .col-sm- 小屏幕 平板 (≥768px) .col-md- 中等屏幕 桌面显示器 (≥992px) .col-lg- 大屏幕 大桌面显示器 (≥1200px) 分为12个格子 -后面的1代表占12分子1也就是一份 1.中等屏幕 <div class"container-fluid a">&l…

ARM Linux DIY(十二)NES 游戏

文章目录 前言交叉编译工具链使能 Cnes 游戏模拟器移植游戏手柄调试 前言 很多小伙伴为了不让自己的 V3s 吃灰&#xff0c;进而将其打造成游戏机。 我们 DIY 的板子具备屏幕、扬声器、USB Host&#xff08;可以接游戏手柄&#xff09;&#xff0c;当然也要凑一凑热闹。 交叉编…

SpringBootTest

SpringBootTest注解 --基于SpringBoot2.5.7版本 可以在运行基于Spring Boot的测试的测试类上指定的注释。在常规的Spring TestContext框架之上提供了以下特性: 默认提供SpringBootContextLoader作为ContextLoader&#xff0c;也通过 ContextConfiguration(loader…)来自定义 若…

docker安装zookeeper(单机版)

第一步&#xff1a;拉取镜像 docker pull zookeeper第二步&#xff1a;启动zookeeper docker run -d -e TZ"Asia/Shanghai" -p 2181:2181 -v /home/sunyuhua/docker/zookeeper:/data --name zookeeper --restart always zookeeper

基于改进莱维飞行和混沌映射的粒子群优化BP神经网络预测股票价格研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

1990-2022年上市公司常用控制变量数据(1300+变量)

1990-2022年上市公司常用控制变量数据&#xff08;1300变量&#xff09; 1、时间&#xff1a;1990-2022年 2、指标&#xff1a;共计1391个变量&#xff0c;包括但不限于以下类别&#xff1a;、基本信息&#xff08;性质、行业、地址&#xff09;、股票市场&#xff08;发行、…