弹幕暂存和分发:LNDanmakuDispatcher

846

这个文章的前置文章:iOS弹幕组件LNDanmakuMaster

Dispatcher工作方式

Dispatcher的工作方式非常像配货站,通常有闲置卡车的司机会将自己的卡车信息登记在配货站,需要运送屋子的雇主把货物、目的地等信息登记在配货站,然后由专人将一些顺路的货物分配到一辆卡车上,这辆卡车装满了就发车;这种配货方式可以使运输资源得到最大限度利用。

Dispatcher的工作方式与配货站十分类似,一个Dispatcher通常会管理多条轨道,内置一个队列存储弹幕,弹幕在这里就代表了货物,轨道代表了卡车;Dispatcher会在时钟的驱动下不断check那些空闲的轨道,当某个轨道同时符合空闲度要求和Dispatcher策略时,队首的弹幕会被添加到这条轨道上。

Dispatcher同时兼顾了自身的分配策略和轨道的空闲度,优先级:空闲度 > 分配策略,例如:在宽松策略下,同时有多个轨道都可以播放队首的弹幕时,Dispatcher会选择当前最空闲的那个轨道来放置这条弹幕,这个空闲度数值是由TrackController根据当前自身的状态决定,如:条形轨道的最后一条弹幕的剩余存活时间就代表了这条轨道的空闲度。

Queue

LNDanmakuQueue是Dispatcher使用到的存储结构,为了保证按照一定顺序添加的弹幕也会按照一定顺序播放,同时也提供一定的容错能力,让那些因为轨道拥塞而不能播放的弹幕也不会被立即丢弃掉。 以下是LNDanmakuQueue对外提供的方法列表,包含了一个Queue结构常规的push、pop、top、空判断、最大容量等方外,也包括了额外的清空、包含判断和移除判断,提供一个代理来向外界传达自己的操作时机。

@interface LNDanmakuDispatchQueue : NSObject
@property (nonatomic, weak) id <LNDanmakuDispatchQueueDelegate> delegate;
@property (nonatomic, assign) NSInteger maxCapacity;
- (void)push:(LNDanmakuAbstractAttributes *)attributes;
- (void)push:(LNDanmakuAbstractAttributes *)attributes priority:(LNDanmakuDispatchQueuePriority)priority;
- (LNDanmakuAbstractAttributes *)top;
- (LNDanmakuAbstractAttributes *)pop;
- (NSArray<LNDanmakuAbstractAttributes *> *)clearQueue;
- (BOOL)contains:(LNDanmakuAbstractAttributes *)attributes;
- (void)remove:(LNDanmakuAbstractAttributes *)attributes;
- (BOOL)isEmpty;
@end

与常规队列提供的push方法稍有不同,这个队列提供了额外的一种按优先级的push,使弹幕可以以不同的优先级push进这个队列,高优的弹幕会先于默认优先级弹幕播放。内部依靠两个子队列实现了这种优先级:

@interface LNDanmakuDispatchQueue ()
@property (nonatomic, strong) LNDanmakuDispatchSubQueue *highQueue;
@property (nonatomic, strong) LNDanmakuDispatchSubQueue *defaultQueue;
@end

在pop的时候,我们会优先检查高优子队列的空状态,如果高优队列不为空则pop高优队列,否则pop默认优先级队列,这里截取了pop方法:

- (LNDanmakuAbstractAttributes *)pop
{
    if ([self.highQueue top]) {
        return [self.highQueue pop];
    }
    return [self.defaultQueue pop];
}

这种优先级队列的用途:我们考虑展示用户自己发送的弹幕在本地展示的情景,用户希望自己发出的弹幕在第一时间得到展示,如果将这条弹幕和普通弹幕一样插入到队列中,它可能会在当前队列中已存在的弹幕都被播完之后才会显示出来;反之插在队首,如果有两条或以上的高优弹幕被插入队首,那么对这些高优弹幕而言,这个结构就变成了栈。因此,我们提供了额外的高优队列来解决这个问题。 每个子队列内部实现在此不过多赘述,本质上就是封装了NSMutableOrderedSet,然后对外暴露方法。

Dispatcher

LNDanmakuAbstrackDispatcher抽象类中规定了一个Dispatcher需要支持的所有能力,以下是抽象类的定义:

@interface LNDanmakuAbstractDispatcher (Override)
//private use
- (void)dispatchNewAttributesToFreeTracks:(NSArray<LNDanmakuAbstractTrackController *> *)trackControllerArray;
//public use
- (void)insertNewAttributes:(NSArray <LNDanmakuAbstractAttributes *> *)newAttributesArray;
- (void)insertHighPriorityAttributes:(NSArray <LNDanmakuAbstractAttributes *> *)highPriorityAttributesArray;
- (void)clear;
- (BOOL)containsAttributes:(LNDanmakuAbstractAttributes *)attributes;
- (void)removeAttributes:(LNDanmakuAbstractAttributes *)attributes;
@end
  • -(void)dispatchNewAttributesToFreeTracks 方法被标记了private,意思是,这个方法只能被LNDanmakuMaster框架内部的类调用,这个方法翻译成中文:“我这里有一些轨道,如果你有需要播放的弹幕,请从这些轨道中挑选出一个放置你的弹幕”。因此,这个方法只会被Player和TrackGroup调用,Player和TrackGroup是轨道列表的持有者。
  • 其他的方法和队列的方法是大体对应的,它们都被标记为public,也就是使用者可调用的方法,当然这个界限并不是绝对的,因此总是有我们预料不到的场景需要特殊处理。

QA:为什么为dispatcher提供抽象类,虽然大部分场景下的弹幕都是按照顺序分发的,但产品经理的意识总会超出常人预料,我们假设他们提出一种从给定的弹幕池子里随机抽取弹幕进行分发的需求,我们重新实现一种新的Dispatcher接入后仍然可以让这个框架的其他部分正常工作。

梳理一下Dispatcher的工作原理:

  • 使用者通过insertNewAttributes方法将弹幕插入Dispatcher的队列。
  • Player受Clock驱动,调用dispatchNewAttributesToFreeTracks方法让Dispatcher做出选择并放置弹幕。
  • Dispatcher从给定的TrackController列表中挑选出空闲且符合分发策略的轨道,把队首的弹幕放上去。
  • 弹幕被添加到TrackController后,走TrackController的播放流程。

后续会单独有一个文章介绍三种分发策略:

typedef NS_ENUM(NSInteger, LNDanmakuDispatchStrategy)
{
    LNDanmakuDispatchStrategyDefault = 0, //Find the first track to insert.
    LNDanmakuDispatchStrategyLowDensity, //Find the most free track to insert.
    LNDanmakuDispatchStrategyMostFastDisplay //Find the track with shortest waiting time.
};

typedef NS_ENUM(NSInteger, LNDanmakuRecoverDispatchStrategy)
{
    LNDanmakuRecoverDispatchStrategyDefault = 0,
    LNDanmakuRecoverDispatchStrategyLowDensity,
    LNDanmakuRecoverDispatchStrategyMostFastDisplay,
};