MeetingService重构和ParticipantList性能优化实践

news2024/11/27 7:40:36

一丶背景

1.1 现状

  • 最初Rooms客户端只支持加入Rcv meeting这个meeting
    type,RcvMeetingStateService里写了一些加会的状态机转换和Audio, Video,
    Share相关的功能代码。后续有新的业务,需要增加支持Webinar,
    Sip等各种Meeting,MeetingService的代码没有较好的做抽象和封装导致其他Meeting无法复用只能暂时写一些重复的代码去实现支持其他meeting。
  • MeetingService等一些Services遍历List对外通知MeetingStateChange和ParticipantsChange等事件的时候时常有Activity or Fragment收到通知后需要关闭自己然后关闭的同时去MeetingService解除注册, 此时会有并发修改异常产生导致崩溃。其次,由于这些Service都需要写同样的注册,解除注册等代码,导致产生大量重复的模板代码和UT cases。
  • Rcv Meeting最初只支持200人加会,后续我们想支持到一万人,发现ParticipantList界面消耗了大量内存和渲染卡顿。

1.2 目标

  • 对于MeetingService中的代码抽象封装,使其成为一个个高内聚低耦合的独立组件方便给各个MeetingService以组合的方式复用代码。
  • 重构现有的观察者模式使其支持并发修改,且检测出多线程操作数据问题给开发提示让其修改业务代码为在同一个线程操作数据和发通知,其次消除模板代码。
  • 对ParticipantList渲染性能优化,使其能够在万人会议中也能流畅的渲染出List和操作。

二丶解决方案

2.1 ParticipantList 问题分析和解决

Android界面渲染出现卡顿的直接原因是未在1帧内完成UI绘制工作,可能的原因主要有2个方向:

  • 界面嵌套太深,或者在渲染的时候有一些比较耗时的工作导致系统未能在16ms内完成绘制。
  • 程序消耗了大量内存,导致频繁地Full GC而Stop The World.

问题的难点在于如何分析找到耗时方法和内存分配。
对于常规的View层的优化,比较直接有效的手段是减少嵌套层级,在此基础上,我们可以利用AndroidStudio Profile对方法耗时内存分配做监控,结合业务代码对ParticipantList的RecyclerView做针对性的分析和优化。

众所周知,将layout文件渲染成一个View对象因为涉及到io读取和反射是较为耗时的,如果RecyclerView的item view足够简单,我们是可以尝试用JavaCode去直接创建ItemView的,甚至业界也有利用某些工具将Layout文件直接生成对应的JavaCode,但是我的工程中的itemview较为复杂,不适用这一点。既然如此,我们从尽可能地少创建或者提前创建itemview这个方向入手。

2.1.1 提高缓存利用率

RecyclerView复用原理是从它的各级缓存中查找,如果没有命中就调用onCreateViewHolder去创建一个新的ViewHolder,所以我们要尽可能的利用缓存,用空间换时间的理念去充分发挥缓存的作用。

recyclerView.setItemViewCacheSize(4)
recyclerView.recycledViewPool.setMaxRecycledViews(    
    ParticipantListAdapter.ITEM_TYPE_IN_MEETING, 
    7
)

考虑到用户可能会较为频繁的上下反复滚动查找某个Participant,适当将CacheViewSize设置为4,缓存offscreen viewholder. 某些场景下需要全量刷新,这里participant list的最多显示7个item,所以我们将RecyclePool设置到7来缓存全量刷新下的ViewHolder.接着我们设置hasStatbleId和为每一个Item返回唯一id来尽可能地当全量刷新的时候让ViewHolder缓存有效而不是进入RecyclPool。

2.1.2 减少绘制

进一步的,ItemAnimator对于业务没有帮助,我们将其禁止掉来减少绘制,其次由于我们ParticipantList的RecyclerView高度是固定的不会发生改变,我们调用

 recyclerView.setHasFixedSize(true)

来避免requestLayout。

2.1.3 减少没必要的刷新

我们业务中有多处监听去全量刷新(notifyDataSetChanged)的代码,比如LocalParticipant(Myself)的Audio/Video状态的变化,这时候没有必要全局刷新的,只需要刷新LocalParticipant就好了,此外底层的回调也会重复触发这些接口即使状态没有发生真正的变化,所以基于此,我们用

data class ParticipantListRoomSettingModel(
    var hasVideoDevices: Boolean = true,
    var hasAudioDevices: Boolean = true,
    var isWaitingRoomEnable: Boolean = false,
    var localRole: ERoomParticipantRole = ERoomParticipantRole.NORMAL
)

来记录当前的各个状态,当接口通知的时候我们比较确实与本地的状态不一致时我们做局部刷新:

notifyItemChanged(localParticipantIndex)

2.1.4 减少冗余Log

我们发现当业务代码中有非常多的冗余Log在记录Participant的状态,这在当有大量Participant在会议中的时候会频繁的输出大量Log,过多的Log会占用CPU,所以我们精简了一些Log尤其禁止在遍历语句中输出Log.

2.1.5 局部刷新

经我们测试和查看Profile,发现在上万人会议中,大量Participant的状态会频繁的发生更新(update, add, remove),服务端会很频繁的通知客户端去全量刷新RecyclerView,这其实是这次卡顿的真正元凶。此外因为,在业务上用户并不需要这么及时地看到participant list的更新,所以我们让服务端更新全量下发participant list数据的方案,服务端每次只需要在数据发生变化后就起一个1s的延时任务,在这1s内收集这些diff数据下发给客户端即可,客户端在子线程中解析好拿到update list, add list, remove list后我们二分查找这些participant 的Index后去做局部刷新。这样既减少了冗余的数据传输,也解决了客户端过于敏感地刷新数据问题。

	parseJob = ioScope.launch {
			// 起协程解析json
            val participantsNewModel = newGson.fromJson(
                participantListJson,
                ParticipantsNew::class.java
            )
            // 解析updates通知局部刷新类型为 EUpdateType.CHANGED
            participantsNewModel.updates.forEach { participant ->
                val index = participantsList.binarySearch { it.modelId - participant.modelId }
                if (index < 0) return@forEach
                participantsList[index] = participant
                mainScope.launch {
                    notifyEachListener {
                        onParticipantListChanged(EUpdateType.CHANGED, index, 1)
                    }
                }
            }
			// 省略解析 adds, removes 
        }

	// UI 通知adapter做局部刷新
	override fun updateParticipant(
        type: ParticipantListViewModel.EUpdateType,
        index: Int,
        count: Int
    ) {
		when (type) {
            ParticipantListViewModel.EUpdateType.ADDED -> dataAdapter?.notifyItemRangeChanged(
                index,
                count
            )

            ParticipantListViewModel.EUpdateType.DELETED -> dataAdapter?.notifyItemRemoved(
                index
            )

            ParticipantListViewModel.EUpdateType.CHANGED -> dataAdapter?.notifyItemChanged(
                index
            )
		}
	}

2.1.7 滑动时暂停加载图片

我们发现RecyclerView快速滑动的时候,其实用户这时候时不关心具体的item数据展示的,所以此时我们可以选择暂停加载图片。

监听RecyclerView滚动状态,我们在快速滑动的时候暂停加载头像,当速度慢下来和停止滑动的时恢复加载头像

class RecyclerViewScrollController(
    recyclerView: RecyclerView,
    private val listener: RecyclerViewScrollListener,
    private val threshold: Int = 150,
) {

    private val scrollListener = object : RecyclerView.OnScrollListener() {
        override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
            if (newState == SCROLL_STATE_IDLE) {
                listener.onStopScroll(recyclerView)
            }
        }

        override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
            if (abs(dy) < threshold) {
                listener.onSlowScroll(recyclerView)
            } else {
                listener.onFastScroll(recyclerView)
            }
        }
    }

    init {
        recyclerView.addOnScrollListener(scrollListener)
    }
}
class PauseLoadImageOnScrollListener : RecyclerViewScrollListener {
    override fun onFastScroll(recyclerView: RecyclerView) {
        Fresco.getImagePipeline().pause()
    }

    override fun onSlowScroll(recyclerView: RecyclerView) {
        Fresco.getImagePipeline().resume()
    }

    override fun onStopScroll(recyclerView: RecyclerView) {
        Fresco.getImagePipeline().resume()
    }
}

2.1.6 减少内存消耗

内存增长,我们前面用让服务端下发Diff数据已经有效的减少了大量的冗余数据,其实我们还有进一步优化的空间。我们检查json数据,删掉一些冗余字段后,我们借鉴http2的压缩header的方法,我们也对json的key做了精简,然后将其映射表下发给客户端,客户端拿到精简后的数据后再用mapping还原即可。这在非常大的list数据中效果非常明显,经测试对比,大约可以减少41%的json长度。

private val FIELD_NAME_MAP = mapOf(
            "id" to "id",
            "modelId" to "mId",
            "initialsAvatarName" to "iAN",
            "displayName" to "dN",
            "isPstn" to "iP",
            "isGuest" to "iG",
            "isVideoLocalMute" to "iVLM",
            "isVideoServerMute" to "iVSM",
            "isAudioLocalMute" to "iALM",
            "isAudioServerMute" to "iASM",
            "roomParticipantRole" to "rPR",
            "isMe" to "iM",
            "activeDeviceCount" to "aDC",
            "isWhiteBoardSharing" to "iWBS",
            "isOnhold" to "iO",
            "isScreenSharing" to "iSS",
            "isAllowUmuteAudio" to "iAUA",
            "isAllowUmuteVideo" to "iAUV",
            "isRoomExtension" to "iRE",
            "headshotColor" to "hC",
            "hasNonPstnSession" to "hNPS",
            "headshotUrlWithSize" to "hUWS",
            "audioStreamActivated" to "aSA",
        )
  //服务端下发上面的map,然后解析的时候可以用此还原key。      
 private val newGson = GsonBuilder().setFieldNamingStrategy {
        filedNameMap?.getOrDefault(it.name, it.name)
    }.create()

2.1.7 减少对象创建避免内存抖动

我们知道图片加载框架都会对Bitmap对象做缓存复用,ParticipantList里面会加载大量的头想图片,头像由图片加载框架缓存,但是我们加载头像前会创建PlaceHolderDrawable来作为placeholder。这一部分我们可以借鉴这个思路,做一个缓存池来减少RecyclerView滚动中不断创建PlaceHolderDrawable,以此来减少GC次数避免内存抖动。

object PlaceHolderDrawablePools {
    @JvmStatic
    val avatarPlaceholderDrawablePool = SimplePool<AvatarPlaceholderDrawable>(20)

    @JvmStatic
    val avatarTextDrawablePool= SimplePool<AvatarTextDrawable>(20)
}
// 当设置placeholder的时候先从pool中获取,取不到的话再创建新的。
Pools.SimplePool<AvatarTextDrawable> pool = PlaceHolderDrawablePools.getAvatarTextDrawablePool();
AvatarTextDrawable avatarTextDrawable = pool.acquire();

// 图片加载结束后 或者 detach avatarview的时候回收drawaable
AvatarPlaceholderDrawable drawable = (AvatarPlaceholderDrawable) mPlaceholderDrawable;
Log.i(TAG, "recycle AvatarPlaceholderDrawable=" + drawable);
Pools.SimplePool<AvatarPlaceholderDrawable> pool = PlaceHolderDrawablePools.getAvatarPlaceholderDrawablePool();
pool.release(drawable);

此外,因为onBindViewHolder中有一处设置OnClickListener的地方,因为onBindViewHolder会频繁调用,这样也会导致频繁创建listener,对此我们需要将其setOnClickListener放到onCreateViewHolder中调用。

2.1.8 核心代码

  • com.ringcentral.rooms.controller.service.model.ParticipantListViewModel: ParticipantList 的ViewModel,负责Participants的数据解析和对RecyclerView提供绑定
  • com.ringcentral.rooms.controller.meeting.ui.participants.ParticipantInMeetingListPresenter:ParticipantList 的Presenter,决定RecyclerView是用局部刷新还是全量刷新
  • com.ringcentral.rooms.controller.meeting.ui.participants.ParticipantListAdapter:RecyclerView的Adapter
  • com.ringcentral.rooms.controller.meeting.ui.participants.ParticipantListViewFragment: Participants List界面
  • com.ringcentral.rooms.controller.meeting.ui.participants.ParticipantInMeetingItemViewHolder:RecyclerView的ViewHolder
  • com.ringcentral.rooms.common.widget.recyclerview.PauseLoadImageOnScrollListener:当RecyclerView快速滑动时暂停加载头像
  • com.ringcentral.rooms.common.widget.recyclerview.RecyclerViewScrollController:监听RecyclerView的滑动状态组件
  • com.ringcentral.rooms.common.widget.image.PlaceHolderDrawablePools:头像的PlaceHolderDrawable缓存池
  • com.ringcentral.rooms.common.widget.image.AvatarView:Participant的头像

2.1.9 成果举证

1.旧的方案:
旧方案
2.新的方案:
请添加图片描述

旧的方案里,CPU一直被消耗了40%左右,内存峰值达到153.9MB左右,且后续因为有大量的临时对象创建和回收,可以看到内存图有很多锐化,有很明显的内存抖动现象,频繁的GC会使程序感觉到卡顿。这里因为Demo去掉了其他业务,在实际项目中由于还有很多其他业务在一起抢占资源,所以会明显感觉到卡顿。

新方案优化后,CPU消耗了降低了很多,后续几乎不怎么占用CPU资源了。内存因为我们做了一些缓存优化,可以看到峰值较之前少了25.6MB且一直比较稳定不会因为要分配和回收太多内存而造成内存抖动。

2.2 MeetingService问题分析和解决

2.2.1 复用性问题

2.2.1.1 问题分析

因为旧的业务中只有RcvMeeting这一种类型,旧的代码把Meeting相关的逻辑一股脑的都放在了MeetingService里了,不曾想后续新增了很多种MeetingType,这导致无法复用。如果我们能把这些相关的代码按照一定的业务划分为各个内聚的独立组件,如果有某个业务方,比如RcvMeetingService和WebinarMeetingService都需要这部分代码,那么它都可以直接以组合的方式去复用这块代码,那这样就能比较好地做到高内聚低耦合且可以很好地应对需求变更。
这块的难点在于如何将现有的业务和加会流程梳理清晰然后抽象为各个高内聚低耦合的独立组件,再由这些组件组成一个大的MeetingService框架。

2.2.1.2 解决方案

我们抽象一个BaseMeetingService,把原来的join流程和inmeeting相关的逻辑拆成BaseClientInMeetingController、BaseClientJoinMeetingController。然后再抽取可选配的业务组件,比如E2EE模块,BreakoutRoom某块等等将其也封装成E2EEController,BreakoutController, 这样比如RcvMeeting和WebinarMeeting需要E2EEController的话就可以在JoinMeetingController中以组合方式集成,其他的SipMeeting不需要的话就不用集成这个模块。这样我们就将原来的臃肿的MeetingService重构为轻量,高可用的组件,极大的提高了代码可维护性。

2.2.1.3 核心代码

  • com.ringcentral.rooms.controller.service.deprecated.MeetingService:以前旧的Rcv MeetingService
  • com/ringcentral/rooms/common/meeting/service/BaseMeetingStateService:新的BaseMeetingService
  • com.ringcentral.rooms.controller.service.ControllerRcvMeetingService: 新的Rcv MeetingService
  • com.ringcentral.rooms.controller.service.ControllerWebinarPanelistService: 新的Webinar MeetingService
  • com.ringcentral.rooms.controller.service.base.BaseClientInMeetingController: 抽象的InMeeting 组件
  • com.ringcentral.rooms.controller.service.base.BaseClientJoinMeetingController: 抽象的JoinMeeting 组件
  • com.ringcentral.rooms.controller.service.RcvClientInMeetingController: Rcv meeting的Inmeeting 组件的实现
  • com.ringcentral.rooms.controller.service.RcvClientJoinMeetingController:Rcv meeting的JoinMeeting 组件的实现

2.2.1.4 UML

改造后分为各个MeetingService
类图:
在这里插入图片描述
将加会流程和各个Meeting业务抽取封装为各个独立组件(XXXController)
请添加图片描述

2.2.2 观察者模式并发修改异常和模板代码

2.2.2.1 观察者模式问题分析

由于MeetingService使用Set<Listener>来保存订阅者,在MeetingState发生变化的时候通过遍历这个Set<Listener>去通知
订阅者,随着业务复杂度的增加,后续发现当MeetingState变为Idle的时候需要通知Fragment or Activity去退出,而此时Fragment退出的时候又会向MeetingService解除订阅,此时会产生并发修改异常。而这种业务场景却又是符合正常的逻辑的,所以我们希望能自定义一个数据结构来支持这种在遍历中准确的移除or添加Listener.

其次有些通知可能由一些比较复杂的业务模块或者由底层的Corelib团队发出来的,有时候他们会忘记切换线程在子线程通知到业务层,而业务层会在主线程去添加or移除这些Listener,此时会因为线程同步问题而出现一些并发修改问题。借鉴Android规定只有创建View的线程才能操作View以此来规避线程同步问题,我们也不想利用这个理念来规范对数据的并发修改。我们可以对这个数据结构做线程检查,在Debug build中发现有不同的线程在修改这些数据的话,我们可以抛出异常来尽早提示开发者这里可能有线程同步问题需要优化。

最后,由于新增了很多MeetingService(Rcv, Webinar, Sip, Phone, etc.) 这些Service包括其他一些组件都在使用观察者模式,即需要注册Listener,解除注册Listener,存取Listener,相关的UT, 这些都时重复的模板代码,我们利用Kotlin 委托(By)特性, 可以将这些模板代码委托给一个小的ListenerHolder组件来实现,这样就可以消除这些模板代码。

2.2.2.2 观察者模式优化方案

class ConcurrentWeakRefSet<E> {

    private val targetWeakRefElements = mutableSetOf<WeakReference<E>>()
    private val waitingAddElements = mutableSetOf<WeakReference<E>>()

    private val expectId = Thread.currentThread().id
    private val expectName = Thread.currentThread().name
    private var concurrentFlag = 0

    val size: Int
        get() = targetWeakRefElements.size

    fun add(element: E): Boolean {
        if (!checkThread()) return false

        val old = targetWeakRefElements.find { it.get() == element }
        if (old != null) {
            return false
        }
        return if (concurrentFlag == 0) {
            targetWeakRefElements.add(WeakReference(element))
        } else {
            waitingAddElements.add(WeakReference(element))
        }
    }
    
	private fun checkThread(): Boolean {
        if (!NEED_CHECK) return true

        val actualId = Thread.currentThread().id
        val actualName = Thread.currentThread().name
        if (actualId != expectId) {
            val message =
                "expectThread:$expectName($expectId), actualThread:$actualName($actualId)"
            if (!BuildConfig.DEBUG) {
                throw ConcurrentModificationException(message)
            } else {
                Log.e(TAG, message)
            }
            return false
        }
        return true
    }
}

我们自定义一个数据结构,创建的时候我们就会记录下当前的current thread id,当外部操作这个数据结构的时候我们就会检查当前操作的线程和创建的线程时不是同个线程,不是的话在debug build中抛出异常,在release build中给出exception log.
当往这个数据接口添加数据的时候,即添加一个Listener的时候,我们判断当前这个flag如果不需要处理并发修改的话就直接添加跟其他数据接口一样,但是如果需要处理的话,即这次添加时在上一次遍历中添加的,我们把它先添加到waitingAddElements。

private fun forEachWithIndex(action: (E) -> Unit, index: Int) {
        val size = targetWeakRefElements.size
        for (i in (index until size)) {
            val element = targetWeakRefElements.elementAt(i).get()
            element?.runCatch(action)
            if (waitingAddElements.isNotEmpty()) {
                targetWeakRefElements.addAll(waitingAddElements)
                waitingAddElements.clear()
                forEachWithIndex(action, i + 1)
                return
            }
        }
}

而在遍历的时候,我们取出来如果弱引用的对象还存在的话就执行这个闭包,且当执行闭包完毕以后,我们就会检查当前的waitingAddElements是否有待添加的Listener,如果有的话,我们添加到尾部,然后从当前这个位置重新发起遍历,这样就规避掉并发修改异常了。
再来看移除,如果当前不需要处理并发,只需要直接移除,如果要处理我们先把弱引用的对象置空,这样遍历的时候就获取不到这个Listener也就不会下发给这个Listener通知,然后在遍历结束的时候再移除这些无效的弱引用对象即可。

fun remove(element: E): Boolean {
        if (!checkThread()) return false

        return if (concurrentFlag == 0) {
            targetWeakRefElements.removeAll { it.get() == element }
        } else {
            targetWeakRefElements.forEach {
                if (it.get() == element) it.clear()
            }
            true
        }
    }

fun forEach(reversed: Boolean = false, action: (E) -> Unit) {
        if (!checkThreadSafely()) return

        concurrentFlag += 1
        if (reversed) {
            forEachWithReversed(action, 0)
        } else {
            forEachWithIndex(action, 0)
        }
        concurrentFlag -= 1
        if (concurrentFlag == 0) {
            targetWeakRefElements.removeAll { it.get() == null }
        }
}

借此我们解决了并发问题,并且我们将线程同步的潜在问题在开发阶段暴露出来给开发去识别和处理,提高了代码的健壮性。

消除模板代码问题我们用by interface的特性来把实现委托给ConcurrentListenerHolder

class ConcurrentListenerHolder<T> : IListenerHolder<T> {

    private val listeners = ConcurrentWeakRefSet<T>()

    override fun registerListener(listener: T): Boolean {
        return listeners.add(listener)
    }

    override fun unregisterListener(listener: T): Boolean {
        return listeners.remove(listener)
    }

    override fun clearListeners(): Boolean {
        return listeners.clear()
    }

    override fun notifyEachListener(reversed: Boolean, action: T.() -> Unit) {
        listeners.forEach(reversed, action)
    }
	
	override fun bindLifecycleListener(listener: T, lifecycle: Lifecycle, state: Lifecycle.State) {
        lifecycle.addObserver(ListenerLifecycle(listener, state))
        if (lifecycle.currentState.isAtLeast(state)) {
            listeners.add(listener)
        }
    }
}

而且我们还可以结合Lifecycle来实现自动解除订阅:

private inner class ListenerLifecycle(private val listener: T, private val targetState: Lifecycle.State) : DefaultLifecycleObserver {

        override fun onCreate(owner: LifecycleOwner) {
            if (targetState == Lifecycle.State.CREATED) {
                listeners.add(listener)
            }
        }

        override fun onStart(owner: LifecycleOwner) {
            if (targetState == Lifecycle.State.STARTED) {
                listeners.add(listener)
            }
        }

        override fun onResume(owner: LifecycleOwner) {
            if (targetState == Lifecycle.State.RESUMED) {
                listeners.add(listener)
            }
        }

        override fun onPause(owner: LifecycleOwner) {
            if (targetState == Lifecycle.State.RESUMED) {
                listeners.remove(listener)
            }
        }

        override fun onStop(owner: LifecycleOwner) {
            if (targetState == Lifecycle.State.STARTED) {
                listeners.remove(listener)
            }
        }

        override fun onDestroy(owner: LifecycleOwner) {
            listeners.remove(listener)
        }
    }

在被观察者处使用:

object RoomsMeetingConnector : IListenerHolder<RoomsMeetingConnector.Delegate> by ConcurrentListenerHolder() {
	fun joinMeetingWithOptions(
        meetingOptions: IRseJoinMeetingOptions,
        joinCallback: ActionCallback,
        newSolution: Boolean
    ) {
        // ...
		// 通知订阅者
        notifyEachListener {
            onHostStatusChange(HostMeetingStatus.CONNECTING, meetingType)
        }
    }
}

可以看到现在我们被观察者不再需要写这些重复的registerListener, unregisterListener,foreach等模板代码,也减少了UT cases,既使代码变得更简洁也提高了编码效率。

2.2.2.3 核心代码

  • com.ringcentral.rooms.common.utils.IListenerHolder
  • com/ringcentral/rooms/common/utils/ConcurrentListenerHolder.kt
  • com.ringcentral.rooms.common.utils.ConcurrentWeakRefSet

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

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

相关文章

Linux Shell 实现一键部署ovirt4

ovirt 前言 oVirt是一个开源分布式虚拟化解决方案&#xff0c;旨在管理您的整个企业基础设施。oVirt使用可信的KVM管理程序&#xff0c;并基于其他几个社区项目构建&#xff0c;包括libvirt、Gluster、PatternFly和Ansible。 Ovirt仅支持系统Centos / Redhat ovirt download…

npm利用verdaccio工具发布到私有仓库的教程

文章目录 概要安装方式运行方式相关的配置淘宝源修改开发访问地址设置用户删除用户更换源设置发布当前包 概要 提示&#xff1a;用于将可复制和常用的方法打包发布 例如&#xff1a; 可以将我们的公共组件和工具类以及SDK等核心的代码发布到外网中&#xff0c;需要我们常见私…

小白到运维工程师自学之路 第三十五集 (MongoDB的基本使用)

一、概述 MongoDB是一个非关系型数据库管理系统&#xff0c;它使用文档模型存储数据。MongoDB中的文档类似于JSON对象&#xff0c;可以包含键值对和嵌套文档。MongoDB提供了强大的查询语言、聚合框架、索引和直接在数据存储中运行的计算。 MongoDB被广泛应用于许多领域&#x…

MySQL开启远程访问权限

默认情况下&#xff0c;MySQL只允许本地登录&#xff0c;即只能在安装MySQL环境所在的主机下访问。 但是在日常开发和使用中&#xff0c;我们经常需要访问远端服务器的数据库&#xff0c;此时就需要开启服务器端MySQL的远程连接权限。1、生成环境&#xff0c;连接MySQL 2、查看…

STM32单片机(六)TIM定时器 -> 第七节:TIM编码器接口

❤️ 专栏简介&#xff1a;本专栏记录了从零学习单片机的过程&#xff0c;其中包括51单片机和STM32单片机两部分&#xff1b;建议先学习51单片机&#xff0c;其是STM32等高级单片机的基础&#xff1b;这样再学习STM32时才能融会贯通。 ☀️ 专栏适用人群 &#xff1a;适用于想要…

vue引入jszip下载多个图片并压缩下载

vue引入jszip下载多个图片并压缩下载 jszip官网地址 先进行jszip下载 npm install jszip然后废话不多说直接上代码 <template><div><button click"downloadImages">下载图片</button></div> </template><script> impo…

shell内置命令

目录 内置命令介绍内置命令列表alisa内置命令alias别名定义语法unalias 别名删除语法alias演示 echo内置命令echo命令介绍echo输出语法echo输出转义字符 read内置命令介绍语法options支持的参数示例1&#xff1a;多个变量赋值 exit内置命令介绍语法示例&#xff1a;Shell脚本文…

OPC AE server服务的实现

OPC AE server的实现 日前实现了OPC AE server&#xff0c;特此记录备查。 1、事件分三类&#xff0c;从下图可知每类都包含啥内容了&#xff0c;CONDITION看起来主要是一些越限&#xff0c;熟悉其它协议的朋友应该对这个不陌生&#xff0c;一般这种事件产生时&#xff0c;会…

通过Selenium实现Web自动化测试的研究

目录 前言&#xff1a; 1 自动化测试概念 1.2 自动化测试发展阶段 2 自动化测试工具Selenium简介 3 具体测试过程 4 总结 前言&#xff1a; Web自动化测试是Web应用程序测试的重要组成部分&#xff0c;它通过模拟实际用户行为&#xff0c;对Web应用程序进行自动化测试并…

chatgpt赋能python:Python拆包:提高编程效率的神器

Python拆包&#xff1a;提高编程效率的神器 在Python编程中&#xff0c;拆包&#xff08;unpacking&#xff09;是一项非常有用的技巧。它可以让你将一个可迭代对象&#xff08;iterable&#xff09;中的元素分离开来&#xff0c;赋值给多个变量。这个过程一般称为“解包”&am…

小白到运维工程师自学之路 第三十八集 (redis高可用集群)

一、概述 Redis高可用集群是一种分布式系统&#xff0c;它由多个Redis节点组成&#xff0c;可以提供高可用性和容错性。在Redis高可用集群中&#xff0c;每个节点都可以处理客户端请求&#xff0c;并且数据会在多个节点之间进行复制和同步&#xff0c;以确保数据的可靠性和一致…

Day_51-53kNN分类器

目录 Day_51&#xff1a;kNN分类器 一. 前言 二. 机器学习基本术语 三. kNN算法的原理 1. kNN算法的思想 2. kNN算法的具体实现过程 四. 代码实现 1. 导包 2. 参数初始化 3. 数据的导入 4. 数据的抽取 5. 对于测试集进行预测 6. 核心代码 Day_52&#xff1a;knn分类器&am…

LabVIEW开发微控制器控制的并行机器人的实时视觉图像处理

LabVIEW开发微控制器控制的并行机器人的实时视觉图像处理 通过相机视觉&#xff0c;以对目标物体的不同颜色进行分类&#xff0c;并与平行机器人一起拾取和放置物体。通过使用MATLAB Simulink模拟合适的机器人工作空间来研究使用相机的效率和机器人的准确性。机械臂以使用运动学…

OpenMMLab-AI实战营第二期——5-2. MMSegmentation代码课

文章目录 1. 自定义数据集1.0 整理数据集为特定格式1.1 持久化运行&#xff08;用文件定义&#xff09;1.2 运行时生效&#xff08;直接运行时定义一个class&#xff09;1.3 注意事项 2. 配置文件3. 运行训练和测试X. 其他语义分割数据集 视频链接&#xff1a;MMSegmentation代…

ThreadPoolExecutor源码

介绍 ThreadPoolExecutor 是 Java 中实现线程池的一个类&#xff0c;它是 ExecutorService 接口的一个实现类。线程池可以用来优化线程的使用&#xff0c;避免频繁地创建和销毁线程&#xff0c;以及限制并发线程的数量&#xff0c;从而提高应用程序的性能。 public class Thr…

读发布!设计与部署稳定的分布式系统(第2版)笔记06_用户

1. 系统的人类用户天生就具备进行创造性破坏的本事 1.1. 用户会消耗内存 1.2. 用户会做奇怪和随机的事情 1.2.1. fuzzing工具箱、基于属性的测试或模拟测试 1.3. 恶意用户总是存在的 1.3.1. 灾祸总会发生&#xff0c;坏人肯定存在 1.4. 用户会合伙对付你 2. 难伺候的用户…

【linux】opencv修改摄像头分辨率

起因 发现linux系统下调用opencv修改摄像头分辨率&#xff0c;一直修改不成功。 原本 正常在window下面读取摄像头数据是如下代码&#xff1a; # capture from web camcap cv2.VideoCapture(0)# set widthcap.set(3, 1280)# set heightcap.set(4, 720)while self._run_flag…

武职302303笔记-day01

这里写自定义目录标题 开发永和小票开发步骤1、对页面进行需求分析 使用CSS的方式 Order.html问题&#xff1a;html代码和css样式代码交织 idea开发后端程序使用chatGPT给我们打工QRCreate.java 开发永和小票 开发步骤 1、对页面进行需求分析 页面是很多文字组成&#xff0c;…

虹科分享丨如何保证我们的数据安全?|数据保护

一、数据安全 你可能已经正在寻找加密服务来帮助保护你的组织的数据&#xff0c;但如果你是一个不熟悉实践或管理数据的人员&#xff0c;"数据加密 "这个短语可能会感觉像技术术语或只是像一个模糊的概念&#xff0c;没有具体的解决方案。 但事实上&#xff0c;数据…

git、axios、模块化导出引入、promise、webpack、

GIT总结 git config --global user.name "你的用户名" git config --global user.email "你的邮箱" git config -l 命令来查看配置信息 git init 仓库初始化 touch index.html 创建index.html文件 git add index.html 文件加入暂存区 git add -A 将全部文件…