队列概念描述
我所讲的任务队列,是指异步的任务队列,而不是代码从上至下顺序执行后的按序执行任务.简单讲几个需求的场景
1.直播间礼物动画,直播间同时有5个人刷礼物,APP允许的最大同时存在礼物展示行数只允许2行,剩下3个就需要等待这两人其中一人礼物播放结束,然后再进入下一个人,先进先出,如同排队一样
2.APP好友上线提醒,最多允许一个,APP要求后上线好友的提醒必须等待之前的好友播放动画结束才可以展示(如果是那种直接覆盖型的当然是没有这种问题的)
还有非常非常多的需求场景,各位稍加思索,应该就能想起这种队列的必要性.
需求设计
Swift的异步任务队列,可以通过原生的API DispatchQueue支持实现,具体实现方式,我已在上次写过, 戳这,(不过swift的原生实现并不太优雅,其实可以通过自定义operation的didChangeValue(forKey: "isFinished")
等实现,不过这是后话,后面有时间,我会写一个更加优雅的TaskProtocol),这里,我要讲的是,flutter的任务队列, dart原生没有队列相关的API,这里我们可以手动实现一个,接下来我们想想我们需要设计的功能需求:
- 队列并发数(允许任务同时执行的任务数上限)
- 队列完成回调控制(控制每个任务何时完成,应允许手动控制)
- 任务等待自动取消(加入到任务队列里的任务等待一段时间后自动取消)
- 任务等待自动完成(执行中的任务等待一段时间后自动完成)
- 任务权重,任务优先级高的任务优先插入到队列前面
自动取消,自动完成都是我项目开发中碰到的任务需求,简单举几个例子
自动取消: 私聊消息提醒并不是一个非常重要的功能,上线后收到的一堆历史消息,应只展示最近的几条,后续的消息提示展示,不用再展示给用户看,所以后面的消息展示任务应在等待一段时间后自动消息掉.用户可以前往消息列表自己查看
自动完成: APP内弹窗点击,比如上线后,用户有时候会收到一堆的弹窗点击,签到弹窗,青少年弹窗,引导弹窗等等,如果不加队列控制,弹窗会一股脑全部弹出,我们希望用户再处理完上次的弹窗点击后,我们再弹出下一个弹窗,但是,如果用户一直不点击弹窗,会导致后续累计的队列任务越来越多,所以这里,可以增加一个自动完成,在执行A弹窗任务时,等待一段时间,如果用户不手动完成,我们自动完成掉A任务,弹出B弹窗覆盖在A弹窗之上,这样就可以保证队列中的任务不会被累积.
设计实现
我们对每一个执行的任务都可以封装成对象,并且,我们需要给予每一个任务有完成掉这个任务的能力,所以,任务的Function需要设计成这个样子
///任务封装
class TaskItem extends LinkedListEntry {
final TaskDetailFunc taskDetailFunc;
final TaskCallback callback;
final String uuid;
/// 自动完成该任务 任务正在进行时
final Duration? autoComplete;
TaskItem({
required this.uuid,
required this.taskDetailFunc,
required this.callback,
this.autoComplete,
});
}
typedef TaskCallback = void Function();
typedef TaskDetailFunc = void Function(TaskCallback taskCallback);
TaskDetailFunc是我们的任务对象,传入一个TaskCallback的参数,taskCallback的唯一作用,就是执行callBack来结束掉该任务,并且进行一系列的判断,开启下一个任务执行, TaskFutureFuc是我们的外部任务, TaskCallback是我们的内部任务流转逻辑,这么说可能有点绕,实际上就是抛出一个结束标志,让外部在动画结束后调用.
参数定义
我们不需要定太多的逻辑,有用的参数仅仅下面几个,maxConcurrentOperationCount用于定义任务队列最大并发,_currentTaskCount用于控制并发数,_isCanTaskRun用于判断是否可以执行下一个任务,_taskList用于存储我们需要的任务列表,如果要做细的话,甚至也可以定制最大的缓存任务数,任务权重等,这个就由各位发挥了.
int maxConcurrentOperationCount = 1;
int _currentTaskCount = 0;
bool get _isCanTaskRun => _currentTaskCount < maxConcurrentOperationCount;
LinkedList _taskList = LinkedList();
初始化
TaskQueueUtil({required this.maxConcurrentOperationCount}) {
assert(this.maxConcurrentOperationCount > 0, "❌ 任务并发数不能太小");
assert(this.maxConcurrentOperationCount <= 5, "❌ 任务并发数不能太大");
}
没啥好说的,随便稍微限制一下
核心逻辑
我们设置一个addTask方法,在这个方法里,new出一个task对象,增加callBack逻辑判断,调用执行任务,这里callBack回调可以稍微细说一下: 受益于Completer的complete方法,当一个Completer()被执行complete后,后续再执行complete会抛出异常,我们可以直接利用这一api特性,而省去自己写防重复完成逻辑,这样自动完成,自动取消逻辑都十分好写了.
Future addTask(
TaskDetailFunc futureFunc, {
Duration? autoCancel,
Duration? autoComplete,
}) {
Completer completer = Completer();
String uuid = randomString();
TaskItem taskItem = TaskItem(
uuid: uuid,
taskDetailFunc: futureFunc,
autoComplete: autoComplete,
callback: () {
try {
completer.complete();
} catch (e) {
psdllog("❌ 重复调用完成事件");
return;
}
// 这里可能有问题, 考虑要不要给事件+个id,通过id去移除这个事件
// _taskList.removeWhere((element) => element.uuid == uuid);
_currentTaskCount -= 1;
//递归任务
_doTask();
},
);
_taskList.add(taskItem);
_doTask();
/*
* 如果未执行 自动取消该任务
* */
if (autoCancel != null) {
Future.delayed(autoCancel).then((value) {
final bool isInTask = _taskList.contains(taskItem);
if (isInTask) { // 不在任务列表的第一个就自动取消掉该任务
psdllog("❎ taskId: ${taskItem.uuid} 自动取消任务");
_taskList.remove(taskItem);
}
});
}
return completer.future;
}
执行下个任务
_doTask() async {
if (!_isCanTaskRun) return;
if (_taskList.isEmpty) return;
//获取先进入的任务
TaskItem task = _taskList.first;
_taskList.remove(task);
_currentTaskCount += 1;
try {
//执行任务
task.taskDetailFunc(task.callback);
/*
* 自动完成该任务
* */
if (task.autoComplete != null) {
Future.delayed(task.autoComplete!).then((value) {
psdllog("❎ taskId: ${task.uuid} 自动完成任务");
task.callback.call();
});
}
} catch (_) {
task.callback.call();
}
}
剩余的注释我都写在代码注释里,由于我懒得写任务权重,并且为了加快数组添加删减,_taskList使用的是LinkedList定义,
任务这里还需要集成自class TaskItem extends LinkedListEntry
使用
for (var i in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) {
taskQueue.addTask(
(taskCallback) {
Future.delayed(Duration(seconds: 5)).then((value) {
psdllog("第$i次");
taskCallback.call();
});
},
autoCancel: Duration(seconds: 5),
// autoComplete: Duration(seconds: 2),
);
}
结果:
各种自动取消 自动完成 手动完成我均已测过 各位都可以自己去尝试.
使用注意
如果你没有用到自动取消,自动完成,那么taskCallback.call();
一定要在逻辑结束时调用过一次,否则,整个队列都会因为你某个任务的不调用taskCallback.call();
而处在任务永远结束不了的情况,既然我把完成任务的权利交给了每个任务,这每个任务就有责任必须调用到taskCallback.call(),我在代码中加入了如果task.taskDetailFunc(task.callback);
出现执行异常,会执行掉任务的完成,但是如果你的某个弹窗,想用这个队列,但是又不执行call(),就会出问题.
PS
致谢: Flutter:使用Completer实现自定义任务队列,是这篇文章给予了我初期思路
下一篇暂时没想好写什么,可能是flutter音视频项目的总结,也可能是Swift相关,之前swift项目系列立了很多的flag,都没实现,唉~~~