flutter 异步任务队列

队列概念描述

我所讲的任务队列,是指异步的任务队列,而不是代码从上至下顺序执行后的按序执行任务.简单讲几个需求的场景
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<TaskItem> {
  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<TaskItem> _taskList = LinkedList<TaskItem>();

初始化

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<TaskItem> {

使用

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,都没实现,唉~~~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,904评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,581评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,527评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,463评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,546评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,572评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,582评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,330评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,776评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,087评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,257评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,923评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,571评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,192评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,436评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,145评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,127评论 2 352