ExoPlayer架构详解与源码分析(3)——Timeline

news2024/12/23 9:27:35

系列文章目录

ExoPlayer架构详解与源码分析(1)——前言
ExoPlayer架构详解与源码分析(2)——Player


文章目录

  • 系列文章目录
  • 前言
  • Timeline
    • 单文件或者点播流媒体
    • 文件播放列表或者点播流列表
    • 有限可播的直播流
    • 无限可播的直播流
    • 有多个Period的直播流
    • 先点播后直播流
    • 带有插播广告的点播流
  • Timeline的实现
    • Timeline
    • SinglePeriodTimeline
    • PlaceholderTimeline
    • ForwardingTimeline
    • MaskingTimeline
    • AbstractConcatenatedTimeline
    • PlaylistTimeline
  • 总结


前言

如果播放器就是一只火箭,那么火箭发射就必须要有一个基于时序的发射计划,火箭在运行过程中通过获取当前时间点的发射计划就会知道当前的时序状态,以及决定下一步该干什么,如需要在什么时候点火、发动机什么时候停机、各个阶段的姿态调整等等。

所以设计了播放器还是不够的,还需要描述出媒体的时序结构,但是播放器播放的媒体种类有很多,可以是一个播放列表、一个mp4文件、一个网络的url,一段视频的流,反正千奇百怪。如何设计一个数据结构可以灵活的表示出上面各种的媒体在不同时间点的时序结构呢。ExoPlayer给出的答案是 Timeline(时间线)。Timeline贯穿在整个的Exoplayer源码中,后续系列文章提到的Player、MediaSource、LoadControl、TrackSelector等等都会使用到Timeline,所以有必要将Timeline提前了解下。

Timeline

Timeline 是媒体时序结构的灵活表示。因为只是用来获取状态,所以Timeline是一个不可变的对象,所有的属性都是不可变的(final),这样设计也保证了多线程下的数据安全。对于动态媒体(例如直播流),Timeline 表示是当前状态的快照。
Timeline 由 一个或多个Window(窗口) 和 Period(时段) 组成。

  • Window :通常对应一个播放列表的子项。 它可能会跨多个 Period 并且定义了这些 Period 中可播放的区域。Window 还包含一些其他信息,如当前Window 是否可以Seek,可以开始播放的默认位置等。
    在这里插入图片描述
    上图中window1横跨了2个period,Window包含以下属性:

    • uid Window的唯一标识符。单Window必须使用 Window.SINGLE_WINDOW_UID。
    • firstPeriodIndex 记录横跨的第一个Period的索引。
    • lastPeriodIndex 记录横跨的最后一个Period的索引。
    • durationUs 表示Window的时长。
    • defaultPositionUs 默认开始播放位置(图中黑点)相对于Window开始时间的差值。
    • positionInFirstPeriodUs Window开始时间相对于第一个Period开始时间的位置的差值,因为Window开始时间肯定大于等于第一个Period的开始时间,这个值一定是大于0的。
    • mediaItem 与窗口关联的mediaItem,mediaItem 是在Player setMediaItem时,创建Timeline并设置到其中的Window里。同样Player在获取当前mediaItem时首先获取当前的Timeline,然后在Timeline里获取当前的Window,最后在Window里获取mediaItem。
    • manifest Window的清单,播放单个文件的时候为null,当在播放HLS流时,会将HLS流的索引文件相关信息设置进去。
    • windowStartTimeMs Window的开始时间。
    • isSeekable 是否可以Seek。
    • isDynamic 是否是动态的Window,当Timeline 随着时间变化时,Window是否也会随着变化。
    • isLive 是否为直播流。
    • liveConfiguration 直播流的相关配置。
    • elapsedRealtimeEpochOffsetMs 本地时间和服务器时间偏差,用于Timeline.getCurrentUnixTimeMs获取当前实际时间。
    • isPlaceholder用于标记当前Window是否为占位的Window,因为当前正式数据还未加载,因此只包含初始值的占位信息,其中mediaItem在播放前已知,PlaceholderTimeline会将此值设置为true。
  • Period:定义了媒体的单个逻辑块,如一个视频文件。它还可以定义插入到视频里的广告组,还记录这些广告是否已经加载和播放。
    在这里插入图片描述
    上图包含了2个Period,指向同一个Window,Period包含以下属性:

    • uid Period的唯一标识符。
    • windowIndex Period 所属Window的索引。
    • durationUs 当前Period的时长,Period结束时间相对于Period开始时间的差值,对于直播流(图中period2)就没有值。
    • positionInWindowUs 该Period的开始时间相对于其所属Window开始时间的差值。如果该Period的开始时间在Window左侧,则为负值,如上图的period1,对比Window的图可以看到他们都是指示的同一线段只是方向不一致,也就是这个值的绝对值是和Window的positionInFirstPeriodUs是相等的。
    • adPlaybackState 插入到Period中的广告相关信息。

Timeline是不可变的,是当前播放的一个静态快照,从这个角度对比火箭发射(播放器播放),发射火箭的时段可能是连续的几天(时段),但是可以发射(可以播放)的窗口期可能就在这一天中的某1个小时,具体在这个小时的哪个时间点发射(播放)就对应Window的defaultPositionUs(小黑点),而这个窗口期可能正好在23:30-1:00,跨越2天(时段)。

下面列举出各种媒体 Timeline 的表示

单文件或者点播流媒体

在这里插入图片描述这类媒体包含一个Period和一个Window。 Window和 Period 一样长,Window的默认播放位置就在Period 起点。这个很好理解,当你播放本地的一个视频文件时,由于是单个文件可以理解为只有一个文件的播放列表,这个文件可以从头播放播放到结束,由于文件只有一个所以 Period 只有一段。像单个视频文件或者点播类的HLS就是用的这种方式抽象的 Timeline,一个文件或者点播流就对应一个 Period。

文件播放列表或者点播流列表

在这里插入图片描述
这类媒体包含多个Window和多个Period,每个Period 都有一个自己的Window与之对应,Window默认播放位置就在每个Period的开始,这类媒体可以想象成将上面的单个文件添加到一个播放列表。这类媒体只有在列表里播放到相应的项才能获取到Window和Period。ExoPlayer 针对这种结构,其实是通过将上面的单个Timeline组合起来,抽象出一个新的ListTimeline来实现的,也就是上图相当于3个Timeline。

有限可播的直播流

在这里插入图片描述
因为是直播内容是实时产生的,随着时间不断增多,所以Period总时长是未知的。因为是有限的,仍然可播放内容时间只占 Period 的一段,所以Window就定义了这段可播放范围,开始播放播放位置也不一定在Window的开头。此时Window的 isLive=true,当Window改变时isDynamic将被设置为true。这类媒体的默认播放位置一般在Window的边缘,接近于当前时间,如上图的黑点。像直播类的DASH或者HLS都属于这类。举个例子,当你看一个直播时,你可以回看2分钟之前到现在的视频,这个2分钟到现在就是一个Window,随着时间的推移Window也在向右平移,那么这个Window就是动态的,isDynamic=true,而打开这个直播默认的播放位置往往是最接近当前时间的点,同时也在Window的右侧边缘。

无限可播的直播流

在这里插入图片描述和上面有限可播的直播流类似,唯一不同的是Window的起点固定在Period的开头,也就是可以播放之前已播的所有直播内容。

有多个Period的直播流

在这里插入图片描述
这类将直播流分成了多个Period,和有限可播的直播流类似,只是Window可能跨一个或多个Period。

先点播后直播流

在这里插入图片描述
这类将点播流和直播流结合,当点播流播放结束的时候直播流将在Window靠近当前时间的一侧开始播放。这种可以当作将点播文件和直播文件放到一个播放列表里。

带有插播广告的点播流

在这里插入图片描述

这类在单个点播流中插入了广告(上图灰色)。通过查询当前的Period可以获取广告组或者广告的信息。

对于一些动态的媒体,比如说播放一个直播流,随着时间的推移不同时间点的Timeline(播放快照)对应Period的时长或者数量是不断增加的,不同时间点的Timeline对应的Window是不停改变的,其中包括Window的 开始时间、结束时间、时长等等都在不停的变化,而非直播流这些又是相对固定的。

小结下特点

  • Timeline 里可能包含多个Period或者Window
  • 多个Period都是连续的,而多个Window可能是不连续的
  • Window时长小于等于所有Period的时长和
  • 一个Window可能跨域多个Peroid
  • Window开始时间大于等于第一个Period的开始时间
  • Window的默认播放位置不是固定的
  • Period可以随着时间没有右边界,但Window是一定有右边界的,也就是有确定的durationUs
  • 对于静态媒体。在不同时间获取的Timelin对应的Period和Window是相对固定的。
  • 对于动态媒体。在不同时间获取的Timelin对应的Period和Window是相对变化的。

Timeline的实现

说完结构设计,看下代码具体是怎么实现上述设计的,先看下整体架构
在这里插入图片描述

ExoPlayer 播放各种媒体时,主要通过这几个实现类来描述Timeline
来看下各自的作用

Timeline

这里没有定义任何属性,主要定义实现了以下几个功能

  • 查询Window和Period
//使用指定Window索引的数据填充Window
public final Window getWindow(int windowIndex, Window window)
public abstract int getWindowCount();
//获取下个Window,媒体列表的循环模式最终就是在这里实现,这里其实也是填充Window容器
public int getNextWindowIndex(int windowIndex, @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled));
public final Period getPeriod(int periodIndex, Period period);
  • 通过已知的Window的位置,查找到对应的Period的位置,这里重点看下,加深下Winodw和Period之间关系的理解

在这里插入图片描述

这个函数作用是获得Window中黑点(windowPositionUs)对应的Period红点(periodPositionUs)位置,这个位置是相对于Period开始位置的差值。

  • 对于动态媒体periodPositionUs = windowPositionUs-period2.positionInWindowUs
  • 对于静态媒体period.positionInWindowUs = 0,periodPositionUs = windowPositionUs

看下源码实现

public final Pair<Object, Long> getPeriodPositionUs(
      Window window,
      Period period,
      int windowIndex,
      long windowPositionUs,
      long defaultPositionProjectionUs) {
    Assertions.checkIndex(windowIndex, 0, getWindowCount());
    getWindow(windowIndex, window, defaultPositionProjectionUs);//获取当前的Window
    if (windowPositionUs == C.TIME_UNSET) {
      windowPositionUs = window.getDefaultPositionUs();//windowPositionUs 没有设置,获取默认开始播放位置
      if (windowPositionUs == C.TIME_UNSET) {//没有设置则返回
        return null;
      }
    }
    int periodIndex = window.firstPeriodIndex;
    getPeriod(periodIndex, period);
    while (periodIndex < window.lastPeriodIndex//从第一个period开始查找到最后一个
        && period.positionInWindowUs != windowPositionUs//查找到第一个开始时间=Window位置或者结束时间(下一个period开始时间)>Window位置的period
        && getPeriod(periodIndex + 1, period).positionInWindowUs <= windowPositionUs) {
      periodIndex++;
    }
    getPeriod(periodIndex, period, /* setIds= */ true);
    long periodPositionUs = windowPositionUs - period.positionInWindowUs;//用Window当前位置减去period开始位置(这2个位置都是相对于Window开始时间的),结果就是相对于当前period开始位置的period当前位置,参考上图
    // The period positions must be less than the period duration, if it is known.
    if (period.durationUs != C.TIME_UNSET) {
      periodPositionUs = min(periodPositionUs, period.durationUs - 1);//确保不要超出period 总时长
    }
    // Period positions cannot be negative.
    periodPositionUs = max(0, periodPositionUs);
    return Pair.create(Assertions.checkNotNull(period.uid), periodPositionUs);
  }

Timeline 实现类中其实并不包含Window和Period成员属性,而是保存了可以组装出这2个对象的数据,通过定义获取方法对传入的Window和Period对象填充来获取这个2个对象。简单说Window和Period就相当于一个获取数据容器,用于盛放数据,向上层提供数据。

SinglePeriodTimeline

这是一个只包含一个Period和一个静态Window的Timeline实现。

  • 包含了用于组装Window和Period的必要数据
  private final long presentationStartTimeMs;//用于媒体裁剪,可以先不用管
  private final long windowStartTimeMs;//对应Window属性
  private final long elapsedRealtimeEpochOffsetMs;//对应Window属性取当前实际时间
  private final long periodDurationUs;//对应Period属性
  private final long windowDurationUs;//对应Window属性
  private final long windowPositionInPeriodUs;//对应Window positionInFirstPeriodUs属性,因为这个值是以Window为出发点计算的,所以取负数就是以Period为出发点计算,-positionInFirstPeriodUs则对应Period的positionInWindowUs属性,
  private final long windowDefaultStartPositionUs;//对应Window属性
  private final boolean isSeekable;//对应Window属性
  private final boolean isDynamic;//对应Window属性
  private final boolean suppressPositionProjection;
  private final Object manifest;//对应Window属性
  private final MediaItem mediaItem;//对应Window属性
  private final MediaItem.LiveConfiguration liveConfiguration;//对应Window属性
  • 实现了单个Window和Period的填充方法,也就是Timeline里定义的虚函数getWindow和getPeriod,这里实现就很简单因为只有一个Window和Period,直接将定义的属性设置进去
@Override
  public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) {
    Assertions.checkIndex(windowIndex, 0, 1);
    long windowDefaultStartPositionUs = this.windowDefaultStartPositionUs;
    if (isDynamic && !suppressPositionProjection && defaultPositionProjectionUs != 0) {
      if (windowDurationUs == C.TIME_UNSET) {
        // Don't allow projection into a window that has an unknown duration.
        windowDefaultStartPositionUs = C.TIME_UNSET;
      } else {
        windowDefaultStartPositionUs += defaultPositionProjectionUs;
        if (windowDefaultStartPositionUs > windowDurationUs) {
          // The projection takes us beyond the end of the window.
          windowDefaultStartPositionUs = C.TIME_UNSET;
        }
      }
    }
    return window.set(
        Window.SINGLE_WINDOW_UID,
        mediaItem,
        manifest,
        presentationStartTimeMs,
        windowStartTimeMs,
        elapsedRealtimeEpochOffsetMs,
        isSeekable,
        isDynamic,
        liveConfiguration,
        windowDefaultStartPositionUs,
        windowDurationUs,
        /* firstPeriodIndex= */ 0,
        /* lastPeriodIndex= */ 0,
        windowPositionInPeriodUs);//正值
  }
  @Override
  public Period getPeriod(int periodIndex, Period period, boolean setIds) {
    Assertions.checkIndex(periodIndex, 0, 1);
    @Nullable Object uid = setIds ? UID : null;
    return period.set(/* id= */ null, uid, 0, periodDurationUs, -windowPositionInPeriodUs);//取负值设置Period
  }

PlaceholderTimeline

一个占位的Timeline,通常用于播放器perpared前占位,因为在播准备前Window和Period都是动态不确定的,只有mediaItem是确定的

ForwardingTimeline

这个类就很简单了,直接构造的时候传入一个Timeline,直接将有方法转发给这个Timeline

MaskingTimeline

继承自ForwardingTimeline,在已有Timeline覆盖一层,主要服务于MaskingMediaSource,在MaskingMediaSource创建时如果没有Timeline则在占位的PlaceholderTimeline上覆盖一层。

  • 在播放器媒体没有prepared前,创建MaskingTimeline覆盖在PlaceholderTimeline上来占位。
    public static MaskingTimeline createWithPlaceholderTimeline(MediaItem mediaItem) {
      return new MaskingTimeline(
          new PlaceholderTimeline(mediaItem),
          Window.SINGLE_WINDOW_UID,
          MASKING_EXTERNAL_PERIOD_UID);
    }
  • 在prepared后,创建MaskingTimeline时传入需要被替换成原始ID的PeriodUid和WindowUid,当获取这些指定的ID时将返回原始ID(Window.SINGLE_WINDOW_UID或者MASKING_EXTERNAL_PERIOD_UID)。
    private final Object replacedInternalWindowUid;
    private final Object replacedInternalPeriodUid;
    public static MaskingTimeline createWithRealTimeline(
        Timeline timeline, @Nullable Object firstWindowUid, @Nullable Object firstPeriodUid) {
      return new MaskingTimeline(timeline, firstWindowUid, firstPeriodUid);
    }

    @Override
    public Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) {
      timeline.getWindow(windowIndex, window, defaultPositionProjectionUs);
      if (Util.areEqual(window.uid, replacedInternalWindowUid)) {
        window.uid = Window.SINGLE_WINDOW_UID;
      }
      return window;
    }
  • 创建MaskingTimeline的过程主要在创建MaskingMediaSource时完成。
  public MaskingMediaSource(MediaSource mediaSource, boolean useLazyPreparation) {
    super(mediaSource);
    this.useLazyPreparation = useLazyPreparation && mediaSource.isSingleWindow();
    window = new Timeline.Window();
    period = new Timeline.Period();
    @Nullable Timeline initialTimeline = mediaSource.getInitialTimeline();
    if (initialTimeline != null) {
      timeline =
          MaskingTimeline.createWithRealTimeline(
              initialTimeline, /* firstWindowUid= */ null, /* firstPeriodUid= */ null);
      hasRealTimeline = true;
    } else {
      timeline = MaskingTimeline.createWithPlaceholderTimeline(mediaSource.getMediaItem());
    }
  }

AbstractConcatenatedTimeline

将一个或多个Timeline按照一定次序串联成一个新的Timeline的抽象基类。

  • 定义了一个播放顺序的分发器用于支持按照指定顺序播放列表,和一个标记当前所有的子Timeline列表否是原子的,不可拆分的,必须作为一个整体进行重复播放,并且不支持指定顺序。
  private final ShuffleOrder shuffleOrder;
  private final boolean isAtomic;
    @Override
  public int getNextWindowIndex(
      int windowIndex, @Player.RepeatMode int repeatMode, boolean shuffleModeEnabled) {
    if (isAtomic) {
      // Adapt repeat and shuffle mode to atomic concatenation.
      repeatMode = repeatMode == Player.REPEAT_MODE_ONE ? Player.REPEAT_MODE_ALL : repeatMode;//必须作为一个整体进行重复播放
      shuffleModeEnabled = false;//不支持指定顺序
    }
   .....
  }
  private int getNextChildIndex(int childIndex, boolean shuffleModeEnabled) {
    return shuffleModeEnabled
        ? shuffleOrder.getNextIndex(childIndex)//使用指定次序代替列表顺序
        : childIndex < childCount - 1 ? childIndex + 1 : C.INDEX_UNSET;
  }
  • 因为包含了多个Timeline,实现或者重写了原来Timeline里关于Window和Period获取的相关方法。大致流程就是先去查询Timeline列表获取当前的Timeline(这部分Timeline的管理是虚函数由子类实现),然后从当前的Timeline里获取指定的Window或者Period。
@Override
  public final Window getWindow(int windowIndex, Window window, long defaultPositionProjectionUs) {
    int childIndex = getChildIndexByWindowIndex(windowIndex);//通过windowIndex获取子Timeline的索引
    int firstWindowIndexInChild = getFirstWindowIndexByChildIndex(childIndex);//获取子Timeline的第一个Window在所有Timeline中的索引
    int firstPeriodIndexInChild = getFirstPeriodIndexByChildIndex(childIndex);//获取子Timeline的第一个Period在所有Timeline中的索引
    getTimelineByChildIndex(childIndex)//获取子Timeline
        .getWindow(windowIndex - firstWindowIndexInChild, window, defaultPositionProjectionUs);//用当前windowIndex-firstWindowIndexInChild获取指定Window索引在子Timeline里的索引
    Object childUid = getChildUidByChildIndex(childIndex);//获取子Timeline的UID
    //如果当前Window的UID为SINGLE_WINDOW_UID,则直接使用Timeline的UID,因为此时的Timeline里只包含这一个Window
    window.uid =
        Window.SINGLE_WINDOW_UID.equals(window.uid)
            ? childUid
            : getConcatenatedUid(childUid, window.uid);//否则用子Timeline的UID和当前Window的UID合成一个新的UID
    window.firstPeriodIndex += firstPeriodIndexInChild;//更新第一个Period的索引
    window.lastPeriodIndex += firstPeriodIndexInChild;
    return window;
  }

PlaylistTimeline

继承了AbstractConcatenatedTimeline,实际管理了多个Timeline,获取指定索引的子Timeline。ExoPlayer默认会将所有的媒体都封装成PlaylistTimeline。
具体数据结构参考下图
在这里插入图片描述
这里每个Window或者Priod都有2个Index,一个是在PalylistTimeline中的索引,一个是在当前Timeline中的索引,firstPeriodInChildIndices记录了每个子Timeline中第一个Window或者Period在PlaylistTimeline中的索引,firstWindowInChildIndices类似,看下代码实现

  private final int windowCount;//Window总数
  private final int periodCount;//PeriodCount总数
  private final int[] firstPeriodInChildIndices;
  private final int[] firstWindowInChildIndices;
  private final Timeline[] timelines;//所有Timeline的数组
  private final Object[] uids;//可以理解成Timeline的UID数组
  private final HashMap<Object, Integer> childIndexByUid;//包含UID对应的子Timeline索引的MAP
  
  private PlaylistTimeline(Timeline[] timelines, Object[] uids, ShuffleOrder shuffleOrder) {
    super(/* isAtomic= */ false, shuffleOrder);//这里播放列表不具有原子性
    int childCount = timelines.length;
    this.timelines = timelines;
    firstPeriodInChildIndices = new int[childCount];
    firstWindowInChildIndices = new int[childCount];
    this.uids = uids;
    childIndexByUid = new HashMap<>();
    int index = 0;
    int windowCount = 0;
    int periodCount = 0;
    for (Timeline timeline : timelines) {
      this.timelines[index] = timeline;
      firstWindowInChildIndices[index] = windowCount;//保存
      firstPeriodInChildIndices[index] = periodCount;
      windowCount += this.timelines[index].getWindowCount();//累加索引
      periodCount += this.timelines[index].getPeriodCount();
      childIndexByUid.put(uids[index], index++);
    }
    this.windowCount = windowCount;
    this.periodCount = periodCount;
  }

总结

  • Timeline只是对媒体播放状态的一种描述方式,方便播放器查询当前的播放状态的快照,用于指导播放器播放。
  • Timeline里并不包含用来给播放渲染音视频的媒体数据,mediaItem也只是保存了媒体的描述。
  • 实际在播放不同媒体结构时,Timeline的结构并不是上面某个单一的数据结构,而是上面这些类型结构的组合,后续系列文章会提到,如播放单文件Timeline结构,播放列表Timeline结构。

版权声明 ©
本文为CSDN作者山雨楼原创文章
转载请注明出处
原创不易,觉得有用的话,收藏转发点赞支持

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

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

相关文章

机器学习---RBM、KL散度、DBN

1. RBM 1.1 BM BM是由Hinton和Sejnowski提出的一种随机递归神经网络&#xff0c;可以看做是一种随机生成的 Hopfield网络&#xff0c;是能够通过学习数据的固有内在表示解决困难学习问题的最早的人工神经网络之 一&#xff0c;因样本分布遵循玻尔兹曼分布而命名为BM。BM由二…

平台项目列表页实现(二)

这里写目录标题 一、顶部盒子设计1. 顶部盒子包含项目列表和添加项目、退出登录2个按钮 二、项目列表盒子设计三、添加项目盒子设计四、退出登录功能实现五、路由导航守卫实现六、展示项目信息七、bug修复1、当项目名称太长或者项目负责人太长&#xff0c;需要一行展示&#xf…

一文详解动态链表和静态链表的区别

1、引言 本文主要是对动态链表和静态链表的区别进行原理上的讲解分析&#xff0c;先通过对顺序表和动态链表概念和特点的原理性介绍&#xff0c;进而引申出静态链表的作用&#xff0c;以及其概念。通过这些原理性的概述&#xff0c;最后总结归纳出动态链表和静态链表的区别。本…

vector的介绍以及使用方式

目录 前言 1.vector的介绍 2.构造函数 3.迭代器 4.vector空间增长问题 5.vector的增删改查 6.vector迭代器失效问题 总结 前言 即我们的string之后&#xff0c;今天小编给大家要介绍一个我们stl中另外一个常用的容器vector&#xff0c;和我们的string一样我们的vector…

Vue中如何进行分布式任务调度与定时任务管理

在Vue中进行分布式任务调度与定时任务管理 分布式任务调度和定时任务管理是许多应用程序中的关键功能之一。它们用于执行周期性的、异步的、重复的任务&#xff0c;例如数据备份、邮件发送、定时报告生成等。在Vue.js应用中&#xff0c;我们可以结合后端服务实现分布式任务调度…

浏览器技巧:谷歌浏览器六个实用设置小技巧,值得收藏

目录 1、确保你的浏览器启用标准保护选项 2、使用安全DNS&#xff08;DNS over HTTPS&#xff09; 3、网站通知修改为"静态指示方式" 4、启用页面预加载提升网页加载速度 5、阻止Chrome浏览器在后台运行 6. 更改 Chrome 启动后打开方式为"上次打开的网页&…

javaWeb超市订单管理系统

一、引言 超市管理系统(smbms)作为每个计算机专业的大学生都是一个很好的练手项目&#xff0c;逻辑层次分明&#xff0c;基础功能包括用户的登录和注销&#xff0c;用户和供应商以及订单信息的增删查改的基础功能。可以帮助我们更好的加深理解三层架构的理念&#xff0c;本项目…

复习 --- QT服务器客户端

服务器&#xff1a; 头文件&#xff1a; #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include<QTcpServer> #include<QTcpSocket> #include<QMessageBox> #include<QDebug> #include<QList> #include<QListWidget> #in…

电脑数据恢复怎么操作?电脑数据恢复难点是什么

随着电脑在我们日常生活中的普及&#xff0c;数据的重要性不言而喻。然而&#xff0c;在某些情况下&#xff0c;我们可能会不小心删除或因其他原因导致丢失了重要的电脑数据&#xff0c;这时候就需要进行数据恢复操作。下面我们一起来了解下电脑数据恢复的操作方法&#xff0c;…

【全3D打印坦克——基于Arduino履带式机器人】

【全3D打印坦克——基于Arduino履带式机器人】 1. 概述2. 设计机器人平台3. 3D 模型和 STL 下载文件3.1 3D打印3.2 组装 3D 打印坦克 – 履带式机器人平台3.3 零件清单 4. 机器人平台电路图4.1 定制电路板设计4.2 完成 3D 打印储罐组件 5. 机器人平台编程6. 测试3D打印机器人 -…

侯捷 C++ STL标准库和泛型编程【C++学习笔记】 超详细 万字笔记总结 笔记合集

关于STL这部分&#xff0c;原课程将其分为了四部分&#xff0c;我做笔记时&#xff0c;会将其整合&#xff0c;使其更具有整体性 文章目录 1 STL概述1.1 头文件名称1.2 STL基础介绍1.3 typename 2 OOP vs. GP3 容器3.1 容器结构分类3.2 序列式容器3.2.1 array测试深度探索 3.2.…

Python3操作MongoDb7最新版创建文档及CRUD基本操作

Python3中类的高级语法及实战 Python3(基础|高级)语法实战(|多线程|多进程|线程池|进程池技术)|多线程安全问题解决方案 Python3数据科学包系列(一):数据分析实战 Python3数据科学包系列(二):数据分析实战 Python3数据科学包系列(三):数据分析实战 MongoDB 操作手册----文档…

Zookeeper经典应用场景实战(一)

文章目录 1、Zookeeper Java客户端实战1.1、 Zookeeper 原生Java客户端使用1.2、 Curator开源客户端使用 2、 Zookeeper在分布式命名服务中的实战2.1、 分布式API目录2.2、 分布式节点的命名2.3、 分布式的ID生成器 3、Zookeeper实现分布式队列3.1、 设计思路3.2、 使用Apache …

电脑桌面黑屏,但程序还可以正常运行

问题&#xff1a;桌面黑屏&#xff0c;程序可以正常运行操作 解决方法: 1.Ctrl Alt Del 2.点击 【任务管理器】-->【文件F】-->【运行新任务N】 3.输入 explorer.exe 回车

Docker 镜像的缓存特性

Author&#xff1a;rab 目录 前言一、构建缓存二、Pull 缓存总结 前言 首先我们要清楚&#xff0c;Docker 的镜像结构是分层的&#xff0c;镜像本身是只读的&#xff08;不管任何一层&#xff09;&#xff0c;当我们基于某镜像运行一个容器时&#xff0c;会有一个新的可写层被…

Spring的AOP开发-注解方式开发AOP

基于注解配置的AOP 注解方式AOP的基本使用 Spring的AOP也提供了注解方式配置&#xff0c;使用相应的注解替代之前的xml配置&#xff0c;xml配置AOP时&#xff0c;我们主要配置了三部分&#xff1a;目标类被Spring容器管理&#xff08;注解使用Service&#xff09;、通知类被S…

图像和视频上传平台Share Me

本文完成于 6 月&#xff0c;所以反代中&#xff0c;域名演示还是使用的 laosu.ml&#xff0c;不过版本并没有什么变化&#xff1b; 什么是 Share Me &#xff1f; Share Me 是使用 Next.js 和 PocketBase 的自托管图像和视频上传平台&#xff0c;具有丰富的嵌入支持和 API&…

基于Java的高校宿舍管理系统设计与实现(源码+lw+部署文档+讲解等)

文章目录 前言具体实现截图论文参考详细视频演示为什么选择我自己的网站自己的小程序&#xff08;小蔡coding&#xff09;有保障的售后福利 代码参考源码获取 前言 &#x1f497;博主介绍&#xff1a;✌全网粉丝10W,CSDN特邀作者、博客专家、CSDN新星计划导师、全栈领域优质创作…

【C++】unordered_map和unordered_set

哈希表 1. unordered_map1.1 概念1.2 常见接口 2. unordered_set2.1 概念2.1 常见接口 3. 底层实现3.1 哈希3.2 哈希函数3.3 闭散列和开散列3.3.1 闭散列3.3.2 开散列 3.4 模拟实现3.4.1 改造哈希桶3.4.2 模拟实现unordered_set3.4.3 模拟实现unordered_map 在C11中&#xff0c…

Promise, async, await 学习

异步编程简介&#xff1a; 介绍&#xff1a;异步编程是一种编程范式&#xff0c;旨在提高程序的性能和响应能力。该模型允许程序在执行某些任务时&#xff0c;不必等待这些任务完成就可以进行下一步操作&#xff0c;从而提高了程序的效率。 作用&#xff1a;异步编程通常用于…