主线程启动文件上传、下载、持久化等耗时操作时,往往都需要通过子线程完成相关操作,为了让UI体验更好,文件下载或者报错的过程中往往都需要提供进度条提示以提升用户感知。同时对于文件列表类型的上传、下载往往还会提供类似暂停\继续的能力,类似如下场景:
本案例将使用Sendable共享对象实现以下两个主要功能:
1、子线程的计算结果刷新UI(进度通知、下载结果通知)。
2、主线程控制子线程业务逻辑(暂停下载、接续下载)。
方案介绍
-
通过Sendable构建可跨线程共享的对象DownloadVideoInfo;
-
主线程通过构建new taskpool.Task(downloadVideo, this.dvi)将DownloadVideoInfo共享对象同步给子线程(this.dvi是DownloadVideoInfo对象实例,downloadVideo是通过@Concurrent修饰的多线程任务);
-
当点击启动时,通过taskpool.execute()方法启动子线程;
-
主线程通过的onReceiveData()注册下载进度更新回调,已实现下载进度的UI刷新。
- 子线程在共享对象中更新下载进度,并通过taskpool.Task.sendData(“UPDATE_DOWNLOAD_PROGRESS”)通知主线程从共享对象中更新下载进度。
- 主线程通过this.downloadTask.onReceiveData()方法注册子线程发送来的消息回调,此处通过监听"UPDATE_DOWNLOAD_PROGRESS"消息,实现UI进度条更新。
-
下载命令控制,暂停/继续下载时需要通过主线程想子线程发送消息,由于taskpool不支持该能力,因此通过共享对象实现此能力。
核心代码
Step1:构建@Sendable对象DownloadVideoInfo、VideoInfo、ActorInfo。此处需要注意,DownloadCommandEnum枚举需要使用const进行修饰。
- DownloadVideoInfo共享类中,使用了基础类型和其他共享对象作为属性,并携带成员方法。
// src\main\ets\model\DownloadVideoInfo.ets
import { DownloadStateEnum } from './DownloadStateEnum';
import { VideoInfo } from './VideoInfo';
@Sendable
export class DownloadVideoInfo {
videoUrl: string = "URL";
downloadProgress: number = 0;
command: DownloadCommandEnum = DownloadCommandEnum.CONTINUE_DOWNLOADING;
videoInfo : VideoInfo = new VideoInfo();
constructor(videoUrl : string) {
this.videoUrl = videoUrl;
}
getDownloadProgress(): number {
return this.downloadProgress;
}
increaseDownloadProgress20Percent() {
this.downloadProgress += 20;
}
}
-
由于TaskPool没有主线程给子线程发送消息的接口,因此此处定义DownloadCommandEnum用来实现主线给子线程下发具体的Download命令(暂停下载、继续下载等)。
枚举必须使用const进行修饰
// src\main\ets\model\DownloadCommandEnum.ets
export const enum DownloadCommandEnum {
STOP_DOWNLOADING,
CONTINUE_DOWNLOADING,
CANCEL_DOWNLOADING
}
- VideoInfo共享类中的Array容器来自于@arkts.collections,容器类中的ActorInfo也必须是Sendable类型
// src\main\ets\model\VideoInfo.ets
import collections from '@arkts.collections';
@Sendable
export class VideoInfo {
long: number = 0;
actors : collections.Array<ActorInfo> = new collections.Array<ActorInfo>()
}
ActorInfo类必须是Sendable类型
// src\main\ets\model\ActorInfo.ets
import { Sex } from './SexEnum';
@Sendable
export class ActorInfo {
name : string = "";
sex : Sex = Sex.MALE;
...
}
Step2:将共享对象this.dvi作为Task的构造函数传入构造函数new taskpool.Task(downloadVideo, this.dvi),这样父、子线程就都将持有DownloadVideoInfo对象实例的引用。downloadData为组件入参。
@Concurrent
function downloadVideo(dvi : DownloadVideoInfo) {
...
}
@Component
struct DownloadComponent {
@Require downloadData !: DownloadData;
private dvi !: DownloadVideoInfo
private downloadTask !: taskpool.Task
aboutToAppear(): void {
this.dvi = new DownloadVideoInfo(this.downloadData.downloadUrl);
this.downloadTask = new taskpool.Task(downloadVideo, this.dvi)
}
...
}
Step3:点击启动按钮时,通过 taskpool.execute(this.downloadTask)启动子线程,并结合then方法,在子线程任务完成后,将状态UI显示内容置为“完成”
Button(this.buttonValue, { type: ButtonType.Normal, stateEffect: true, buttonStyle: ButtonStyleMode.TEXTUAL })
.onClick( ent => {
if (this.buttonValue === '完成') {
return;
} else {
this.start = !this.start;
// 通过点击按钮,实现对暂停下载和继续下载的命令控制
this.buttonValue = this.start ? '暂停' : '继续'
if (this.start) {
console.info("==== start task")
// 启动下载或者接续下载,整个任务始终使用一个共享变量。
this.dvi.command = DownloadCommandEnum.CONTINUE_DOWNLOADING;
this.task = taskpool.execute(this.downloadTask);
this.task.then(() => {
if(this.dvi.downloadProgress == 100) {
this.buttonValue = '完成'
this.downloadData.long = this.dvi.videoInfo.long;
this.showDetail = true;
}
})
} else {
// 暂停下载
this.dvi.command = DownloadCommandEnum.STOP_DOWNLOADING;
}
}
})
}
Step4:当子线程需要更新下载进度条数据,将下载进度设置到DownloadVideoInfo共享对象中,并给主线程发送"UPDATE_DOWNLOAD_PROGRESS"事件,主线程接收到事件后从共享对象中获取下载进度,并刷新UI。
4.1 子线程:模拟每间隔一秒将共享对象中的下载进度downloadProgress增加20,并通过sendDate接口,将更新下载进度消息(“UPDATE_DOWNLOAD_PROGRESS”)发送给主线程
@Concurrent
export function downloadVideo(dvi : DownloadVideoInfo) {
console.info("==== execute task")
let start : number;
while (dvi.downloadProgress < 100) {
start = Date.now();
while (Date.now() - start < 1000) {
// 模拟等待1秒
}
console.info("==== sendData from task")
dvi.increaseDownloadProgress20Percent();
taskpool.Task.sendData("UPDATE_DOWNLOAD_PROGRESS")
// 线程收到停止下载通知
if (dvi.command == DownloadCommandEnum.STOP_DOWNLOADING) {
return;
}
}
dvi.videoInfo.long = 120
}
4.2 主线程通过onReceiveData接收子线程消息,当判断消息是需要更新下载进度(“UPDATE_DOWNLOAD_PROGRESS”)时,将共享对象中的下载进度赋值给状态变量,更新UI。
struct DownloadComponent {
@Require downloadData !: DownloadData;
...
aboutToAppear(): void {
...
this.downloadTask.onReceiveData((msg : string) => {
switch (msg){
case "UPDATE_DOWNLOAD_PROGRESS" :
this.downloadData.downloadProgress = this.dvi.getDownloadProgress();
break;
default :
console.error("==== switch default");
}
})
}
...
Step5:下载过程中若主线程要给子线程发送消息(比如停止下载、继续下载)在taskpool中可以通过共享对象变量实现。当需要暂停或者继续时,将命令下发给共享对象DownloadVideoInfo.command属性,子线程中在业务合适的时机对参数就行判断,若条件满足则执行相关逻辑即可。主线程:
this.dvi.command = DownloadCommandEnum.STOP_DOWNLOADING;
子线程:此处子线程感知共享对象的下载命令变为STOP_DOWNLOADING时,结束子线程,在实际业务中需要根据业务诉求设置状态判断点以及业务处理逻辑。
// 线程收到停止下载通知
if (dvi.command == DownloadCommandEnum.STOP_DOWNLOADING) {
return;
}
当点击接续下载是,只需要在主线程再次启动子线程进行下载即可,前一个线程的下载状态会保存在共享对象中。此处需要将command的状态修改为接续下载CONTINUE_DOWNLOADING,否则子线程会退出。
if (this.start) {
this.dvi.command = DownloadCommandEnum.CONTINUE_DOWNLOADING;
this.task = taskpool.execute(this.downloadTask);
this.task.then(() => {
if (this.dvi.downloadProgress == 100) {
this.buttonValue = '完成'
this.downloadData.long = this.dvi.videoInfo.long;
this.showDetail = true;
}
}
补充的知识
此处的状态变量是通过@ObservedV2和@Trace进行标注的,因此在代码中看不到@State。@Request表示downloadData参数必须通过父组件赋值。DownloadData对象申明如下。当long和downloadProgress属性发送变化时,UI会跟随刷新,如前面代码提到的this.downloadData.long = this.dvi.videoInfo.long;和this.downloadData.downloadProgress = this.dvi.getDownloadProgress();
@ObservedV2
export class DownloadData {
videoName !: string;
downloadUrl !:string;
@Trace long !:number;
@Trace downloadProgress : number = 0;
constructor(videoName : string, downloadUrl : string) {
this.videoName = videoName;
this.downloadUrl = downloadUrl;
}
}