【免费下载链接】Launcher
This fork of Prism Launcher adds integrated support for Ely.by accounts.
Ely Prism Launcher 是一款支持 Ely.by 账号登录的 Minecraft 启动器(Prism Launcher 的 fork),它把"下载游戏文件、安装 Mod、检查 Java 环境"等复杂操作都交给一套统一的异步任务框架来驱动。本文带你逐层剖析launcher/tasks/目录下的三个核心类:Task、ConcurrentTask与SequentialTask,帮你理解启动器界面里那些进度条和"取消"按钮背后是如何工作的。
一、为什么启动器需要一套任务框架?
想象一下你点击"创建实例"按钮后发生的事:
- 先下载版本清单(JSON 文件)
- 再下载游戏本体 jar 包
- 同时并行下载几十个库文件和资源包
- 中途用户可能点击"取消"
- 界面要实时显示"正在执行第 3/10 步"
如果用一堆嵌套回调或死循环轮询来实现,代码会非常难维护。Ely Prism Launcher 的解法是:把一切耗时操作都抽象成"任务(Task)",任务有统一的状态、进度、日志和取消机制,还能像积木一样组合。
🧱 整套框架只有 6 个文件,位于 launcher/tasks/ 目录:
| 文件 | 职责 |
|---|---|
| Task.h / Task.cpp | 任务基类,定义状态机与信号 |
| ConcurrentTask.h | 并发任务容器,最多同时跑 N 个子任务 |
| SequentialTask.h | 顺序任务,并发数固定为 1 |
| MultipleOptionsTask.h | 备选任务,逐个尝试直到成功 |
二、Task:任务框架的最小积木
Task是所有任务的基类(见 Task.h),它同时继承 Qt 的QObject和QRunnable——前者提供信号槽机制,后者让它可以在独立线程中运行。
任务的五种状态
Task用一个枚举描述了任务的一生:
enum class State { Inactive, Running, Succeeded, Failed, AbortedByUser };- Inactive:刚创建,尚未开始
- Running:正在执行
- Succeeded / Failed:以成功或失败结束
- AbortedByUser:被用户取消
外部调用start()启动任务,随后进入子类的executeTask()(纯虚函数)。任务结束时必须手动调用emitSucceeded()或emitFailed()报告结果(见 Task.h),这两个方法内部还做了"不能重复结束"的防错检查。
进度与日志:界面更新的唯一通道
Task提供了三组"上报"接口,UI 只需要监听信号即可:
| 接口 | 对应信号 | 界面效果 |
|---|---|---|
setStatus() | status | 显示"正在下载 xxx" |
setProgress(current, total) | progress | 驱动进度条 |
setDetails() | details | 显示详细备注 |
此外还有abort()/setAbortable()一组取消机制(Task.h):任务自己声明是否允许取消,界面上的"取消"按钮文本甚至可以由任务动态设置。
💡给新手的记忆点:一个合格的子任务类 = 实现executeTask()+ 在结束前调用emitSucceeded()/emitFailed()+ 过程中随时setStatus()/setProgress()。
三、ConcurrentTask:带"并发上限"的任务队列
如果说Task是单个工人,那ConcurrentTask(ConcurrentTask.h)就是工头——它把多个子任务放进队列,按max_concurrent(默认 6 个)的上限并行执行。
内部就是四个"桶" + 一个队列
QQueue<Task::Ptr> m_queue; // 排队等待 QHash<Task*, Task::Ptr> m_doing; // 正在执行 QHash<Task*, Task::Ptr> m_done; // 已完成 QHash<Task*, Task::Ptr> m_failed; // 失败核心调度逻辑在executeNextSubTask()(ConcurrentTask.cpp):
- 正在执行的任务数没达到上限 → 从队列取一个启动
- 队列空且没有任务在跑 → 结算:全部成功则
emitSucceeded();有失败的则汇总所有失败原因后emitFailed()
每个子任务完成时会触发subTaskFinished(),更新统计桶并再调一次调度,形成"完成一个、补上一个"的流水线。
失败聚合与进度透传
- 多个子任务失败时,会把每份失败原因拼成一段文本一起报出,方便排查(ConcurrentTask.cpp)。
- 子任务的 status / details / progress 信号会被
ConcurrentTask转成统一的stepProgress信号向上传递,因此界面上能画出"每个步骤一行"的多步骤进度列表,这正是TaskStepProgressList数据结构(Task.h)的用途。
优雅取消
ConcurrentTask::abort()(ConcurrentTask.cpp)的做法很直观:清空待执行队列 + 逐个向正在跑的子任务喊abort();如果有任何一个子任务拒绝取消,整体按失败处理并提示"Failed to abort all running tasks."。
四、SequentialTask:并发数锁定为 1 的特例
SequentialTask的类声明只有短短几行(SequentialTask.h):
class SequentialTask : public ConcurrentTask { explicit SequentialTask(QString task_name = ""); void subTaskFailed(Task::Ptr, const QString& msg) override; void updateState() override; };它在构造函数里直接调用ConcurrentTask(task_name, 1)——把并发上限设为 1,于是队列天然变成严格串行:前一个子任务成功结束后才启动下一个。
它与父类的行为差异只有两处(SequentialTask.cpp):
- 一个失败立即整体失败:覆写
subTaskFailed(),在记录后立刻emitFailed(msg),不再等待剩余任务。因为顺序任务里后续步骤依赖前面的产出,继续跑没有意义。 - 状态文案更友好:显示"Executing task 3 out of 10"(执行第 3/10 步),而不是并发版的"同时执行 N 个"。
源码注释里有一句话很传神:"A concurrent task that only allows one concurrent task :)"—— 它本质上就是"只允许 1 个并发的并发任务"。
如何选择?一张表看懂四类任务
| 任务类 | 并发度 | 失败策略 | 典型语义 |
|---|---|---|---|
Task | 1(自己跑) | 自己决定 | 单一原子操作 |
ConcurrentTask | max_concurrent(默认 6) | 聚合所有失败原因 | 批量下载等可并行的活儿 |
SequentialTask | 1 | 失败立即终止 | 有依赖关系的步骤流水线 |
MultipleOptionsTask | 1 | 全失败才失败 | 逐个尝试的备选方案(见 MultipleOptionsTask.cpp) |
五、实战:这套框架在启动器里如何落地
场景 1:创建实例时串行下载游戏文件
InstanceCreationTask.cpp 中,创建实例的最后一步组装了一个SequentialTask:先MinecraftLoadAndCheck(在线校验版本清单),再依次追加各组件更新任务,然后start()。注意它调用了propagateFromOther(task.get())——这是Task提供的一个便利方法(Task.cpp),能把子组合任务的进度、状态原样"透传"给自己的监听者,调用方因此完全不用关心组合细节。
场景 2:网络下载是"默认并发度"的主力用户
NetJob.h 让NetJob : public ConcurrentTask,把每个网络请求包装成子任务批量执行;并发上限来自用户设置项NumberOfConcurrentTasks(默认 10,见 Application.cpp)。Java 检测(JavaInstallList.cpp)和版本列表加载(VersionList.cpp)等场景也各自复用了ConcurrentTask/SequentialTask。
场景 3:启动游戏的 LaunchTask
游戏启动流程由 LaunchTask.h 实现,它本身是一个Task,但内部把"检查 Java → 解析服务器地址 → 启动进程"拆成一串LaunchStep顺序执行,并在失败时覆写emitFailed()做特殊处理——展示了框架允许在子类里定制信号行为的灵活性。
六、这套设计的可取之处
- 信号即契约:UI 与任务之间只靠 Qt 信号通信,界面线程永远不会被阻塞,这也解释了为什么进度条即使在下载几百 MB 时也丝滑流畅。
- 继承即编排:
SequentialTask/MultipleOptionsTask几乎零代码,仅靠覆写一两个虚函数就改变了组合语义,是"以继承表达策略"的好例子。 - 防错优先:重复
start()、非运行态下结束任务等异常路径都有断言与日志保护(Task.cpp);单元测试 tests/Task_test.cpp 里甚至专门构造了上万个子任务压测栈深度。 - 可取消是一等公民:从
canAbort()到"取消按钮文案可定制",取消体验贯穿整个框架。
📌小结:Ely Prism Launcher 的任务框架用最小的Task状态机 +ConcurrentTask队列调度,派生出顺序执行与备选重试两种编排方式。理解这三个类,你就掌握了启动器几乎所有异步行为(下载、安装、启动、导出)的骨架,也学到了一个可迁移到任何 GUI 项目的异步任务设计范式。
【免费下载链接】Launcher
This fork of Prism Launcher adds integrated support for Ely.by accounts.
相关推荐
Ely Prism Launcher设置持久层设计:INIFile与SettingsObject实现完整剖析
Ely Prism Launcher设置持久层设计:INIFile与SettingsObject实现完整剖析 你是否好奇 Ely Prism Launcher(
Ely Prism Launcher如何做到任何服务器都显示皮肤?官方Authlib补丁原理解析
Ely Prism Launcher如何做到任何服务器都显示皮肤?官方Authlib补丁原理解析 一、Ely Prism Launcher 是什么?皮肤为何"看
如何快速玩转 Ely Prism Launcher 实例级配置覆盖:OverrideSetting 与 PassthroughSetting 完整指南
如何快速玩转 Ely Prism Launcher 实例级配置覆盖:OverrideSetting 与 PassthroughSetting 完整指南 Ely
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考