编程导航移动开发话题讨论

移动开发

12 参与
分享

快来分享你的内容吧~

点击登录,快来和大家讨论吧~
表情
图片
话题
打卡
综合
交流
文章
问答

有跟我一样从事安卓开发的兄弟不,有几个问题想咨询一下

现状:安卓开发,外包,负责车载的UI模块 问题: 1、询问一下有没有安卓的学习及深造路线,想请教一下如何在安卓这条道路上进一步深造; 2、大家都是如何把AI和安卓开发结合在一起,以提升工作效率的,或者说有没有什么相关的建议; 3、有没有推荐的书籍或网上教程协助安卓相关知识学习。如果能有复杂一点的项目,结合安卓最新前沿科技以及AI技术,那就更好了。

大厂安卓开发 Offer,迷茫是选择安卓还是继续找后端开发

大佬们好,被offer上的问题困扰到了,想劳烦各位给个建议。 ### Offer 描述 目前签了某大厂的安卓开发岗,正常白菜价。 ### 个人情况和职业目标 我是双非本硕非科班,无实习。硕士期间跟着鱼皮哥学的java后端相关知识。目前也没有太明确的职业发展规划,但是还是希望能越干越好的,可能最理想的就是一直在互联网或制造业企业工作。 ### 疑问与困惑 在网上了解到安卓开发可能有点太劝退(个人的片面了解),因此不知道是否备战春招后端岗位。但是最近要着手准备毕业论文方面的工作了,搞项目刷算法背八股的时间应该不会有秋招这么充分,估计春招也很难找到比现在这家更大企业,所以也有点迷茫。 ### 期望建议 总结一下还是有很多渴望大佬帮忙解惑的点的: 1、在安卓大方向不太稳定的情况下,大厂安卓值得去吗? 2、如果选定安卓开发了,进企业后内部转岗转到后端的可能性大吗? 3、如果转不了后端,安卓开发的未来发展大概要朝向哪方面努力比较好呢? 4、若是春招找到了一个中小厂的后端开发,最后该选择大厂安卓还是选择中小厂的后端呢?

【鸿蒙实战开发】ArkTS多线程的多线程系列(二):基于Sendable共享对象实现跨线程通信及UI状态刷新

主线程启动文件上传、下载、持久化等耗时操作时,往往都需要通过子线程完成相关操作,为了让UI体验更好,文件下载或者报错的过程中往往都需要提供进度条提示以提升用户感知。同时对于文件列表类型的上传、下载往往还会提供类似暂停\继续的能力,类似如下场景: <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/ysipOxVuomiQVQ8i.webp" alt="" width="359px" /> 本案例将使用Sendable共享对象实现以下两个主要功能: 1、子线程的计算结果刷新UI(进度通知、下载结果通知)。 2、主线程控制子线程业务逻辑(暂停下载、接续下载)。 **方案介绍** 1. 通过Sendable构建可跨线程共享的对象DownloadVideoInfo; 2. 主线程通过构建new taskpool.Task(downloadVideo, this.dvi)将DownloadVideoInfo共享对象同步给子线程(this.dvi是DownloadVideoInfo对象实例,downloadVideo是通过@Concurrent修饰的多线程任务); 3. 当点击启动时,通过taskpool.execute()方法启动子线程; 4. 主线程通过的onReceiveData()注册下载进度更新回调,已实现下载进度的UI刷新。 * 子线程在共享对象中更新下载进度,并通过taskpool.Task.sendData("UPDATE_DOWNLOAD_PROGRESS")通知主线程从共享对象中更新下载进度。 * 主线程通过this.downloadTask.onReceiveData()方法注册子线程发送来的消息回调,此处通过监听"UPDATE_DOWNLOAD_PROGRESS"消息,实现UI进度条更新。 5. 下载命令控制,暂停/继续下载时需要通过主线程想子线程发送消息,由于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; } } ``` ## 写在最后 **如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:** * 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力; * 关注小编,同时可以期待后续文章ing🚀,不定期分享原创知识; * **想要获取更多完整鸿蒙最新学习知识点,可关注B站:码牛课堂鸿蒙开发;**

【鸿蒙实战开发】ArkTS多线程的多线程系列(一):ArkTS多线能力入门

HarmonyOS应用的UI操作必须在主线程执行(如修改UI控件,更新视图这些操作必须在UI线程中进行),如果主线程出现阻塞,那么UI界面就会出现明显的卡顿。因此为了解决此类问题,我们需要将一些耗时的操作例如加载网络数据、查询本地文件、数据等放到子线程中,以提升应用的响应速度和性能。 **多线程能力介绍** **进程与线程** 在单线程中执行的代码都是串行的,即按顺序执行,直到执行完成后,程序才会退出。当程序需要执行多个任务时,每个任务必须等待前一个任务执行完成后才能继续执行,这使得程序的性能非常低下。多线程技术通过使用多个线程来充分利用CPU资源,同时执行多个任务,从而提高程序执行的效率。每个线程都是相互独立的,并能够单独执行、暂停、继续和停止。进程(Process):是操作系统进行资源分配的最小单元。线程(Thread):是操作系统进行运算调度的最小单元,它被包含在进程之中,是进程中的实际运作单位。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/bseMIaJVriK74pW2.webp" alt="" width="100%" /> **多线程的使用场景** 多线程的应用场景包括但不限于以下场景: 1. CPU密集型:数据处理、图像处理。当需要大量的数据处理时,可以使用多线程,以提高处理效率 2. I/O密集型:文件读写、网络请求。当需要发起大量的I/O请求时,可以使用多线程,以避免卡主线 3. 后台任务:自动化作业处理,比如需要定期完成特定任务。 **ArkTS的多线程解决方案** 在HarmonyOS的ArkTS侧为多线程提供了两种方式:TaskPool和Worker,应用可以结合自身业务诉求,选择对应的实现方案。 **TaskPool简介** 任务池(TaskPool)作用是为应用程序提供一个多线程的运行环境,降低整体资源的消耗、提高系统的整体性能,且开发者无需关心线程实例的生命周期。TaskPool提供了多种不同的任务能力: | 能力 | 任务构建 | 任务执行 | 场景描述 | | --- | --- | --- | --- | | 普通任务 | new taskpool.Task() | taskpool.execute() | 立即执行的短时任务,耗时不能超过3分钟。 | | 延时任务 | new taskpool.Task() | taskpool.executeDelayed() | 为了不影响应用启动的性能,一些不影响启动的初始化类任务往往期望放在延时任务中执行,如拉取线上的配置信息等。 | | 长时任务 | new taskpool.LongTask() | taskpool.execute() | 希望长时运行的任务一直保持执行,已为其他模块提供特定的服务,比如日志埋点,后台长链接保活等。 | | 串行任务 | new taskpool.SequenceRunner(),new taskpool.Task() | SequenceRunner. execute() | 用于执行一组需要串行执行的任务 | | 依赖任务 | task1.addDependency(task2), task1.removeDependency(task2) | taskpool.execute() | 任务之间存在先后依赖关系 | | 任务的优先级设置 | 待支持 | 待支持 | 在应用运行时,期望可以给设置空闲时任务,当应用处于闲时(比如冷启动完成后停留在首页)做一些相应的预加载动作(如预加载其他页面),从而提升应用整体性能。 | **TaskPool注意事项** * 实现任务的函数需要使用装饰器@Concurrent标注,且仅支持在.ets文件中使用。 * 由于不同线程中上下文对象是不同的,因此TaskPool工作线程只能使用线程安全的库,例如UI相关的非线程安全库不能使用,具体请见多线程安全注意事项。 * 序列化传输的数据量大小限制为16MB。 **推荐使用场景** TaskPool的工作线程会绑定系统的调度优先级,并且支持负载均衡(自动扩缩容) * 需要设置优先级的任务。 * 大量或者调度点较分散的任务。 * 需要频繁取消的任务。 **TaskPool线程池扩容策略** 总体算法TaskPool需要的工作线程数由任务平均执行时间和当前任务数共同决定。任务池会根据过往的执行数据计算出一个预期线程数,但是各个任务的执行时间并不能准确衡量TaskPool当前的负载,因此需要通过任务数来反映。 扩容机制TaskWorker线程创建有一定耗时。为了优化启动阶段的性能和加快任务执行,TaskPool默认创建和预留了一个线程。结合JS的async/await机制和TaskPool的线程复用特性,当任务较少时,一个线程能够正常处理所有任务,此时并不一定会触发扩容机制。当首次执行且任务执行耗时较长的时候,上述算法的平均执行时间并不能立刻得出,因此有新的任务时,将会额外新建两个线程,避免线程池拥塞。当任务执行完成后,仍会采用上述算法。 正常流程下,每当开发者向任务池中抛任务时,都会触发一次扩容检测。扩容检测首先会判断当前的空闲线程数是否大于任务数,若大于,则说明线程池存在空闲线程,不需要扩容即可执行完新的任务。否则通过算法判断需要的线程数进而创建线程到指定数目。 缩容机制当任务耗时且较多时,TaskPool将会新建多个TaskWorker线程。但在空闲时仍保留这么多线程将会导致资源浪费和内存无法下降。因此TaskPool使用了定时器,定时检测TaskPool的负载,负载计算方式仍然采用上述算法。但是考虑到频繁创建和销毁带来的开销,缩容并不会立刻缩到计算出的数目,而是整体沿用了急涨缓停的思想,即立刻扩容到指定数目,但在缩容阶段采用阶梯式下降的方式。空闲时每个检测阶段会尝试释放2个线程(当前策略)。由于涉及到资源释放,需要确保不会因为错误的释放带来野指针等crash行为,缩容阶段仍会去检测当前空闲线程是否可以释放。仅有满足条件的线程才能够被释放。 **Worker简介** Worker主要作用是为应用程序提供一个多线程的运行环境,可满足应用程序在执行过程中与主线程分离,在后台线程中运行一个脚本操作耗时操作,极大避免类似于计算密集型或高延迟的任务阻塞主线程的运行。 **Worker注意事项** * Worker的创建和销毁耗费性能,建议开发者合理管理已创建的Worker并重复使用。 * Worker存在数量限制,支持最多同时存在64个Worker。当Worker数量或者内存超出限制时,会抛出相应错误。 **推荐的使用场景** * 常驻后台的线程任务 **TaskPool与Worker对比** 本节将从实现特点和适用场景两个方面来进行TaskPool与Worker的比较。 | 实现 | TaskPool | Worker | | --- | --- | ---| | 内存模型 | 线程间隔离,内存不共享。 | 线程间隔离,内存不共享。 | | 参数传递机制 | 采用标准的结构化克隆算法(Structured Clone)进行序列化、反序列化,完成参数传递。支持ArrayBuffer转移、SharedArrayBuffer共享、sendable共享。 | 采用标准的结构化克隆算法(Structured Clone)进行序列化、反序列化,完成参数传递。支持ArrayBuffer转移和SharedArrayBuffer共享。 | | 参数传递 | 直接传递,无需封装,默认进行transfer。 | 消息对象唯一参数,需要自己封装。 | | 方法调用 | 直接将方法传入调用。 | 在Worker线程中进行消息解析并调用对应方法。 | | 返回值 | 异步调用后默认返回。 | 主动发送消息,需在onmessage解析赋值。 | | 生命周期 | TaskPool自行管理生命周期,无需关心任务负载高低。 | 开发者自行管理Worker的数量及生命周期。 | | 任务池个数上限 | 自动管理,无需配置。 | 同个进程下,最多支持同时开启64个Worker线程,实际数量由进程内存决定。 | | 任务执行时长上限 | 3分钟(不包含Promise和async/await异步调用的耗时,例如网络下载、文件读写等I/O任务的耗时),长时任务无执行时长上限。 | 无限制。 | | 设置任务的优先级 | 支持配置任务优先级。 | 不支持。 | | 执行任务的取消 | 支持取消已经发起的任务。 | 不支持。 | | 线程复用 | 支持。 | 不支持。 | | 任务延时执行 | 支持。 | 不支持。 | | 设置任务依赖关系 | 支持。 | 不支持。 | | 串行队列 | 支持。 | 不支持。 | | 任务组 | 支持。 | 不支持。 | **多线程开发常见场景&解决方案** **创建&停止线程** **TaskPool线程的创建&停止** 通过taskpool.Task或者taskpool.LongTask构建TaskPool线程池任务,构造方法如let task: taskpool.Task = new taskpool.Task(taskName, taskFun, args);。 * taskName为任务名称,此任务名无法在子线程中获取,如果子线程需要使用任务名,则需要将任务名作为参数传递给子线程。 * taskFun为要执行的逻辑函数,该函数必须使用@Concurrent装饰器装饰。 * args为任务执行函数的入参。默认值为undefined。 ``` import taskpool from '@ohos.taskpool'; @Concurrent function add(num1: number, num2: number): number { return num1 + num2; } async function ConcurrentFunc(): Promise<void> { try { let task: taskpool.Task = new taskpool.Task(add, 1, 2); console.info("taskpool res is: " + await taskpool.execute(task)); } catch (e) { console.error("taskpool execute error is: " + e); } } @Entry @Component struct Index { build() { Row() { Column() { Text('Do Something in Taskpool') .onClick(() => { ConcurrentFunc(); }) } } } } ``` TaskPool任务销毁:对于长时任务(LongTask),除了通过启动外,开发者还需要在任务完成后调用terminateTask方法终止此任务,系统不会主动回收此任务。 ``` let longTask: taskpool.LongTask = new taskpool.LongTask(longTask, 1000); // 1000: sleep time taskpool.execute(longTask).then((res: Object)=>{ taskpool.terminateTask(longTask); }); ``` **Worker线程的创建&停止** 通过new worker.ThreadWorker('entry/ets/workers/MyWorker.ets')的方式加载Worker。HAP中的Worker加载,路径规则为:{moduleName}/ets/{relativePath}。 ``` import worker from '@ohos.worker'; const workerThreadHAP: worker.ThreadWorker = new worker.ThreadWorker('entry/ets/workers/worker.ets'); ``` HSP中的Worker加载,路径规则为:{moduleName}/ets/{relativePath}。 ``` import worker from '@ohos.worker'; const workerThreadHSP: worker.ThreadWorker = new worker.ThreadWorker('hsp/ets/workers/worker.ets'); ``` HAR中的Worker加载,路径规则为:@{moduleName}/ets/{relativePath}。 ``` import worker from '@ohos.worker'; const workerThreadHAR: worker.ThreadWorker = new worker.ThreadWorker('@har/ets/workers/worker.ets'); ``` Worker线程销毁 Worker的生命周期需要开发者自行进行管理,也就是说创建出Worker线程后,开发者需要管理Worker线程实例,因此当不需要再使用该线程后,需要显示调用Worker的terminate()方法,将Worker线程实例销毁掉。 ``` const workerInstance = new worker.ThreadWorker("entry/ets/workers/worker.ets"); workerInstance.terminate(); ``` **线程间通信数据类型说明** 此处只做简单说明,具体规格参考相关文档 **Sendable类型共享数据类型** Sendable协议定义了ArkTS的可共享对象体系及其规格约束。符合Sendable协议的数据(以下简称 Sendable 数据)可以在ArkTS并发实例间传递。默认情况下,Sendable数据在ArkTS并发实例(主线程、TaskPool、Worker)间通过引用传递。同时,ArkTS也支持Sendable数据在ArkTS并发实例间的拷贝传。 Sendable对象可以支持的属性类型: 1. 属性限制:包含基础类型(boolean, number,string,bigint,null,undefined)、其他共享对象、枚举、@arkts.collections下的容器。 2. 方法限制:可以传递共享对象中的方法。 当前Sendable对象使用限制较多,如: 1. Sendable class不能使用除了@Sendable的其他装饰器 2. 不能使用字面量初始化Sendable类型 3. 非Sendable类型不可以as成Sendable类型 更多的规则可参考 Sendable使用规则 Sendable对象的使用: * taskpool中使用Sendable对象构建Task时传递参数是,通过args参数将Sendable对象的引用传递给子线程new taskpool.Task(taskName, taskFun, args)。 * worker中使用Sendable对象通过workerPort.postMessageWithSharedSendable()方法,将Sendable对象的引用传递给子线程。 **普通数据类型** 普通对象传输采用标准的结构化克隆算法(Structured Clone)进行序列化,此算法可以通过递归的方式拷贝传输对象,相较于其他序列化的算法,支持的对象类型更加丰富。 序列化支持的类型包括:除Symbol之外的基础类型、Date、String、RegExp、Array、Map、Set、Object(仅限简单对象,比如通过“{}”或者“new Object”创建,普通对象仅支持传递属性,不支持传递其原型及方法)、ArrayBuffer、TypedArray。 **可转移对象** 可转移对象(Transferable object)传输采用地址转移进行序列化,不需要内容拷贝,会将ArrayBuffer的所有权转移给接收该ArrayBuffer的线程,转移后该ArrayBuffer在发送它的线程中变为不可用,不允许再访问。 **可共享对象** 共享对象SharedArrayBuffer,拥有固定长度,可以存储任何类型的数据,包括数字、字符串等。 共享对象传输指SharedArrayBuffer支持在多线程之间传递,传递之后的SharedArrayBuffer对象和原始的SharedArrayBuffer对象可以指向同一块内存,进而达到内存共享的目的。 SharedArrayBuffer对象存储的数据在同时被修改时,需要通过原子操作保证其同步性,即下个操作开始之前务必需要等到上个操作已经结束。 **Native绑定对象** 当前支持序列化传输的Native绑定对象主要包含:Context、RemoteObject和PixelMap。

【鸿蒙实战开发】基于List的滑动丢帧性能问题分析思路&案例

## **1. 场景导入** 基于ArkUI的List组件实现的滚动列表视图,在手指抛滑场景下,通过分析掉帧情况来判断List滑动是否流畅,保障用户极致流畅体验。 ## **2. 性能指标** 最大连续丢帧数:指从页面开始有响应变化到页面结束刷新的过程中,由于显示器画面刷新频率低于预设的画面帧率而未能正常呈现的最大连续帧数。一般而言,当连续值超过3时,用户可以明显感知到卡顿掉帧,数值越大卡顿时间越长。最大连续丢帧数越接近于0,用户流畅性体验越好。 **2.2 性能衡量起止点介绍** 以大于300mm/s的速度,连续3次抛滑,每次半屏。抓取滑动过程Trace,查看Frame泳道中应用进程和RenderService的最大连续丢帧数。 List组件的抛滑过程,可以通过应用进程下的H:APP_LIST_FLING泳道标识。性能衡量的起点为第一次抛滑开始点,衡量的结束点为第三次抛滑的结束点。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/AaXX0jxCZoIhwjXn.webp" alt="image.png" width="532px" /> | 泳道 | 描述 | | --- | --- | | H:APP_LIST_FLING | 从手指按下开始拖动到抬手后的惯性滚动及最后尾动效的抛滑全过程。| | H:touchEventDispatch | 拖滑阶段,从手指开始拖动到抬起。| | H:TRAILING_ANIMATION | 抛滑尾动效阶段 | Tip:尾动效阶段系统会进行降帧处理,所以如果要统计FPS情况,通常只会统计从抛滑开始到尾动效起点的这一阶段 ## **3. 问题定位流程** **3.1.1 查看操作录屏辅助定位** 处理三方应用问题时,可以优先查看操作录屏,查看操作场景,看能否发现一些有助于定位的信息,比如卡顿的页面布局情况、卡顿的现象等等。 **3.1.2 Trace 抓取** 滑动帧率Trace抓取请参考【附录1: 滑动帧率Trace抓取方法】。 ### **3.2  问题定位思路** 滑动丢帧类问题的通用定位思路为先确认抛滑的起止点,然后看抛滑过程中最大连续丢帧数,如果大于0帧,则根据Trace信息进一步确认问题点,确认责任领域并对齐处理,处理流程如下图: <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/96Qxj4W3q65yDQuE.webp" alt="" width="526px" /> **3.2.1****确认起止点** 参考【2.2 性能衡量起止点介绍】 **3.2.2 找问题点** **3.2.2.1 判断丢帧进程** 首先通过Frame泳道判断丢帧进程,其中绿色代表没有丢帧,其他颜色均为丢帧。其中“粉红色”代表该帧的期望时间,“红色”标识超时部分。 **应用进程问题** 如下图,应用进程连续丢了5帧,但可以看到只有261帧、263帧、264帧耗时较长,因此只分析这3帧即可。另外发现最后一帧序号也是264,这是因为前一帧耗时较长,导致该帧和前一帧都提交到了RS的264帧上,应用帧上的序号是被提交到的RS帧序号相对应的。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/J6aa13VGrlvm6uPy.webp" alt="" width="526px" /> **RS 进程问题** RS进程丢帧可能是应用进程导致的,如上图RS侧丢了1帧,但可以看到RS侧264帧丢帧原因是由于应用进程的264帧耗时较长,提交较晚导致。所以这种情况只分析应用侧丢帧原因即可。如果应用进程中没有丢帧且每帧耗时比较均匀,但是RS侧发生丢帧,则说明不是应用侧导致丢帧,此时只分析RS进程丢帧原因即可。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/C6ZLKGaDU9DdzURW.webp" alt="" width="528px" /> **3.2.2.2 找丢帧Trace** 选中Frame泳道,点击Statistics下面的应用进程右侧图标进入Frame List <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/NT2seRCeAt5kOoy2.webp" alt="" width="527px" /> 过滤Jank Type为AppDeadlineMissed类型的帧,点击跳转应用进程。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/l65UPLsFRyyf2NYc.webp" alt="" width="527px" /> 详细分析丢帧Trace <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/I1XEq0WPMxX34VfP.webp" alt="" width="526px" /> **3.2.3 根因分析方法** 应用侧的渲染流程如下图所示,了解ArkUI的渲染流程有助于我们定位应用侧的卡顿问题出现在哪个环节 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/skbFPwtjuUh55Iu2.webp" alt="" width="531px" /> | 阶段 | 描述 | | --- | --- | | Animation | 动画阶段,在动画过程中会对相应的组件标记脏区 | | Events | 事件处理阶段,比如手势事件处理。在手势处理过程中也会对组件标记脏区 | | UpdateUI | 组件在首次创建或状态变量变更时会标记为需要rebuild状态,在下一次Vsync过来时会通过View的方法生成相应的组件树结构和属性样式修改任务。 | | Measure | 执行组件的大小测算任务。 | | Layout | 执行组件的布局任务。 | | Render | 执行绘制任务,执行完成后会标记请求刷新RSNode绘制 | | SendMessage | 将绘制数据提交到RS侧,请求刷新界面绘制 | **应用进程丢帧分析** 跟据Trace图,初步分析耗时较长阶段。261帧由于懒加载组件预创建耗时较长导致丢帧;263帧由于组件复用耗时长丢帧;264帧由于组件结构复杂嵌套层级多导致丢帧。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/apQ62s57ISH4psst.webp" alt="" width="532px" /> | 序号 | 所属泳道 | Trace点 | 描述 | | --- | --- | --- | --- | | 1 | 应用进程 | H:LazyForEach predict | LazyForEach预处理 | | 2 | 应用进程 | H:CustomNode:BuildRecycle 自定义组件名 | 自定义组件的复用,包含执行aboutToReuse方法的耗时 | | 3 | 应用进程 | H:CreateTaskMeasure[组件名][self:组件id][parent:父组件id] && H:Measure[组件名][self:组件id][parent:父组件id] | 执行组件的布局测量任务 | 通过ArkTS CallStack泳道,可以看到应用侧具体调用栈,进一步分析定位问题原因。如下图通过调用栈可以分析出:组件复用时会组件树进行递归,这个过程耗时较长,可以看下组件数是否组件嵌套层级过深;updateDirtyElement耗时长,应用侧可以分析下是否存在冗余节点被触发更新;aboutToReuse耗时长可以看下应用侧该回调中是否存在耗时逻辑。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/U15EjQ8aWv1JZ8Ly.webp" alt="" width="527px" /> 应用UI组件树的嵌套情况,可以通过ArkUI Inspector查看。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/hvSEOqYhMbAgc6zt.webp" alt="" width="526px" /> **经验总结:** 应用进程丢帧通常是组件结构嵌套层级深、耗时应用业务逻辑阻塞UI线程等问题导致。如果是UI结构复杂问题可以让应用通过减少嵌套层级、使用组件复用等方式优化。如果是有耗时业务逻辑,则可以通过将耗时逻辑放到Taskpool或Worker中优化。 **RS 进程丢帧分析** RS进程丢帧一般是由于界面结构过于复杂或者GPU负载过大等原因导致的。如果应用侧没有丢帧且每帧耗时比较平均,则可以初步判断应用侧没有问题,同时也可以通过应用侧Trace中H:SendCommands下的H:MarshRSTransactionData cmdCount查看应用提交的绘制指令树是否过多。如下图RS侧丢帧原因是由于RS侧的H:RSUniRender:FlushFrame阶段耗时较长,此时可以找图形子系统进一步确认耗时根因。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/8yWfSFFjAg1EjH1F.webp" alt="" width="527px" /> 经验总结: RenderService侧丢帧通常是应用侧UI线程阻塞提交绘制指令较慢导致,此时应当初步定位应用侧耗时长原因。如果应用侧无丢帧情况,绘制指令正常提交,则可以找图形子系统协助进一步分析丢帧原因。 ## **4. 典型问题** ### **4.1 耗时任务阻塞UI 主线程** Stage模型下的线程主要有三类:主线程、TaskPool、Worker。主线程主要用于执行UI绘制、处理应用代码逻辑,TaskPool和Worker的作用是为应用程序提供一个多线程的运行环境,用于处理耗时的计算任务或其他CPU密集型任务。当主线程存在**耗时的计算任务**时,会使**主线程阻塞**,导致应用丢帧。 ### **4.1.1 问题根因分析** 应用进程中间有一段大段“空白”,UI线程未提交任何绘制指令,同时CPU10大核却处于Running状态,表示此时应用侧正在执行ArkTS的业务代码。按每帧8.3ms算,这里阻塞了75.9ms,丢了9帧左右。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/yN6Ryes38BijpjHN.webp" alt="" width="527px" /> 通过ArkTS Callstack泳道,可以看到耗时点主要在三个文件:FlowApi.ets、BaseFeedFlowListVM.ets、Mapi.ets。 与伙伴确认业务逻辑主要为:在列表滑动过程中,List将要到达底部时,会通过网络请求会获取到一个博文的列表数据,数据量较大。然后在FlowApi.ets、Mapi.ets中会对数据进行转换处理,处理后的数据通过BaseFeedFlowListVM.ets文件再转换成 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/12n9W4EXCr1rqDkj.webp" alt="" width="527px" /> **4.1.2 优化方案** 在List滑动过程中对数据进行处理耗时较长,占用大量CPU资源,导致主线程被阻塞,这部分数据处理的相关业务逻辑与UI绘制无关,但却长时间占用CPU资源,导致UI线程被阻塞丢帧。可以将该数据处理逻辑放到TaskPool中利用多核并行化处理优化。 除应用侧的耗时逻辑外,某些与UI绘制无关的耗时系统接口调用也可以放到TaskPool中优化。 **4.2 @Prop 传参深拷贝耗时长** **4.2.1 问题根因分析** 观察应用Trace发现Measure阶段的H:CustomNode:BuildItem [MediaCard]耗时较长4ms 661μs,通过观察ArkUI Component泳道,得出自定义组件MediaCard构建耗时较长。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/cJ04SwkpSUMv8n6M.webp" alt="" width="526px" /> 通过ArkTS CallStack观察应用ArkTS调用栈。其中observeComponentCreation2为@Observed传参时相关逻辑调用栈。resetLocalValue、copyObject、deepCopyObject、deepCopyObjectInternal、getDeepCopyOfObjectRecursive为@Prop接收参数时拷贝数据的相关调用栈。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/mghhRVDGgagzkfuM.webp" alt="" width="527px" /> 由此分析得出:应用侧MediaCardComponent.ets文件中的自定义组件MediaCard通过@Observed+@State方式声明了某个对象类型的数据,并将该变量传给了子组件MediaImage,但在MediaImage中并未使用@ObjectLink接收该变量,而是使用@Prop接收,导致该状态变量发生了深拷贝,深拷贝过程耗时较长3ms 339μs。 **4.2.2 优化方案** @Prop装饰器存在性能问题,@Prop装饰的变量会对父组件传入状态值进行深拷贝,如果@Prop装饰器装饰的变量为复杂Object、class或其类型数组时,会增加状态创建时间以及占用大量内存。如果需要观察嵌套类对象的深层属性变化,推荐选择@State+@Observed+@ObjectLink组合方案。 **4.3 UI 复杂导致单帧超长** **4.3.1 问题根因分析** List滑动场景中,应用侧单帧耗时较长91.8ms,导致应用丢帧。通过Trace可以看到在这一帧中创建了大量组件。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/TBimBxqiRw0HR5tt.webp" alt="" width="529px" /> **4.3.1.1 组件复用失效** 首先,观察Trace发现,在ListItem的Measure阶段,出现了大量H:CustomNode:BuildItem,说明此时发生了大量自定义组件重新创建,而没有被复用。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/anKt3dHI4kF6iXqh.webp" alt="" width="526px" /> 经与应用讨论分析,应用的单个ListItem包含4个部分(头像、文本、九宫格、底部按钮)。ListItem的结构非固定的,其子组件可能会出现多种情况,如图文、视频、纯文本等等,动态性较高。同时又因为其复用是以整个ListItem为单位,所以在进入复用池时ListItem会存在多种可能性。导致在新ListItem期望复用创建时,在复用池中可能未找到对应的可被复用组件,自定义组件被重新创建,组件复用失效。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/283SGh1eCq4bChfs.webp" alt="" width="531px" /> **4.3.1.2 嵌套层级深** 通过ArkUI Inspector观察UI组件树结构,可以发其中存在大量自定义组件的__Common__节点(如下图红框),且存在容器之间组件冗余嵌套的情况(如下图绿框)。冗余的嵌套会带来不必要的组件节点,加深组件树的层级,在创建和布局阶段会产生较大的性能开销。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/NGyAEORGbOTl9iUJ.webp" alt="" width="527px" /> **4.3.1.3 冗余的状态变量** 框选该帧并筛选Trace点:H:ViewPU.viewPropertyHasChanged,该Trace点表示状态变量发生了更新,其中后三个参数分别为自定义组件名、自定义的状态变量名、该状态变量更新后影响的组件数量。可以发现这里有大量为“0”的Trace点,表示该状态变量更新时未触发任何组件刷新,即该状态变量未绑定UI组件,因此可以将其改为普通变量。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/UMrkdaKMdOyWqS17.webp" alt="" width="531px" /> **4.3.2 优化方案** **4.3.2.1 组件复用失效优化** 细化组件复用的颗粒度,将原来对ListItem的复用改为对ListItem中各部分子组件复用,提高组件复用成功率。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/2mfmg1HMx6iGNWE9.webp" alt="" width="532px" /> 相关修改代码参考: <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/8LpI6JSeIPeKn2xB.webp" alt="" width="528px" /> **4.3.2.2 嵌套层级深优化** **方案一:** 当自定义组件设置通用属性后,UI组件树就会产生__Common__节点,可以通过属性内移解决。将自定义组件上设置的属性内移到自定义组件中的第一层系统组件上。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/Ui5glw90ipT9CBw3.webp" alt="" width="527px" /> **方案二:** 避免冗余的嵌套,对于这类冗余的容器,应该尽量优化,减少嵌套深度。建议采用相对布局RelativeContainer进行扁平化布局,有效减少容器的嵌套层级,减少组件的创建时间。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/BNriIfHHxkPzsk4C.webp" alt="" width="530px" /> **方案三:** 使用@Builder代替@Component自定义组件。通过@Component声明的组件在创建时会产生额外耗时,建议尽量使用@Builder声明组件。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/pFeIWMMocLNjU3XZ.webp" alt="" width="525px" /> **4.3.2.3 冗余的状态变量优化** 将没有跟UI组件绑定的状态变量改为普通变量。@State、@Prop等装饰器修饰的状态变量在创建、Get、Set时都会产生耗时,因此应该尽量减少冗余的状态变量,避免性能损耗。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/faKXefBjMQxkLL7F.webp" alt="" width="526px" /> **附录1:滑动帧率Trace抓取方法** **Step1:电脑连接上设备,在DevEco Studio上打开Profiler <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/2rO12wWu2FzZhe0Q.webp" alt="" width="529px" /> **Step2 :** 设备上运行需要测试的应用,在设备列表选择设备,选择要测试的应用,和主进程 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/waaejyiPNlW5n39q.webp" alt="" width="525px" /> **Step3:** 创建Frame模板,并点击录制,待所有泳道都进入到recording状态后 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/s0tQWZSTaf5WPRXq.webp" alt="" width="526px" /> **Step4:** 执行相关滑动操作 **Step5:** 操作完成,点击结束录制,待分析完成后,可以在泳道上看到trace数据 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/Ay0695cJDqfadLlz.webp" alt="" width="528px" /> **Step6: ** trace的路径点击Help -> Show Log in Explorer <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/NdgPS9Uw8gocABPx.webp" alt="" width="424px" /> 返回到上一层,找到.insight文件下 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/y8yY4WPC1p5axthR.webp" alt="" width="527px" /> **附录2:List滑动场景通用Trace点说明** **基础List滑动** 以一个最基础的List Demo为例,通过脚本抛滑并使用IDE Profiler工具抓取Trace。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/V8mMg24LneKDjzX8.webp" alt="" width="528px" /> **拖动阶段** 选取拖动阶段某一Trace如下: <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/rqkc8TThQcKyILwe.webp" alt="" width="528px" /> | 序号 | Trace | 描述 | 参数说明 | | --- | --- | --- | --- | | 1 | H:client dispatch touchId:32903 | 系统派发touch事件 | 事件id | | 2 | H:OnVsyncEvent now: [时间戳] | 收到Vsync信号,渲染流程开始 | 时间戳--纳秒级 | | 3 | H:FlushVsync | 处理用户输入、刷新视图同步事件、计算帧信息、提交绘制渲染等 | | | 4 | H:DispatchTouchEvent id:0, pointX=[x坐标] pointY=[y坐标] type=2 | 处理拖拽手势事件 | 触摸点的xy坐标信息,type=2表示手势事件类型为移动 | | 5 | H:HandleDragUpdate | 执行拖拽更新任务 | | | 6 | H:AddDirtyLayoutNode[List][self:7][parent:6] | 标记List组件为脏区 | 组件名、组件ID、父组件ID | | 7 | H:UITaskScheduler::FlushTask | 刷新UI界面,包括布局计算、渲染和提交等 | | | 8 | H:FlushLayoutTask | 执行布局任务 | | | 9 | H:CreateTaskMeasure[List][self:7][parent:6] && H:Measure[List][self:7][parent:6] | 执行List组件的布局测量任务 | 组件名、组件ID、父组件ID | | 10 | H:ListLayoutAlgorithm::MeasureListItem:27 && H:Measure[ListItem][self:10][parent:7][key:] | 计算ListItem列表项的布局尺寸 | 列表项索引、组件名、组件ID、父组件ID | | 11 | H:SkipMeasure | 组件大小布局未发生变化,跳过measure过程 | | | 12 | H:CreateTaskLayout[List][self:7][parent:6] && H:Layout[List][self:7][parent:6] | 执行List组件布局任务 | 组件名、组件ID、父组件ID | | 13 | H:Layout[ListItem][self:10][parent:7][key:] | 执行ListItem组件布局任务 | 组件名、组件ID、父组件ID | | 14 | H:SyncGeometryNode[List][self:7][parent:6][key:] | 同步几何节点 | 组件名、组件ID、父组件ID | | 15 | H:FlushRenderTask 1 && H:FrameNode[List][id:7]::RenderTask | 执行渲染绘制任务 | 当前页面需要绘制的节点数量、需绘制的组件名和ID | | 16 | H:FlushMessages && H:SendCommands | 通知图形侧进行渲染 | | | 17 | H:MarshRSTransactionData cmdCount:14 transactionFlag:[24390,75] | 向图形侧发送绘制指令 | 绘制指令数量、应用进程号、指令序列号 | | 18 | H:OnIdle, targettime:222549467458334 | Vsync中的空闲,一般会用来做预加载之类的操作,当H:OnVsyncEvent时间小于某值时触发该事件 | 时间戳 | **惯性滑动阶段** 惯性滑动阶段Trace点与拖动阶段基本一致,唯一区别点在于拖动阶段是通过手势事件标脏,而惯性滑动阶段是通过动画触发的组件标脏。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/ouh430JBw08QHNw2.webp" alt="" width="525px" /> | 序号 | Trace | 描述 | 参数说明 | | --- | --- | --- | --- | | 1 | H:RunningCustomAnimation num:[1] | 自定义动画,RSModifierManager管理的在UI线程运行的动画 | num表示动画的数量,如果大于0,则表示有动画在运行 | | 2 | H:AddDirtyLayoutNode[List][self:7][parent:6] | 标记List组件为脏区 | 组件名、组件ID、父组件ID | | 3 | H:FlushDirtyNodeUpdate | 更新被标脏的节点 | | **尾动效阶段** 尾动效阶段相较其他两阶段的区别点,在于尾动效阶段如果移动距离小于1像素则不会向RS提交H:SendCommands。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/tguJXUd2ymesmqVM.webp" alt="" width="525px" /> 选取一个应用进程未提交帧,放大后可以看到H:SendCommands下方并无H:MarshRSTransactionData的Trace点。无该Trace点时说明ArkUI中没有需要绘制的内容,因此没有提交绘制指令到RenderService侧。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/5WjWUtFZKtozSEOj.webp" alt="" width="533px" /> **懒加载场景滑动** 基于上文List Demo改造,实现懒加载效果。List懒加载场景下滑动的Trace点与上文基础List基本一致,其区别点主要在于懒加载的滑动场景下会出现组件创建和销毁过程,因此本章节主要介绍创建和销毁组件的关键Trace点。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/KCWEuPnnwJb79KCR.webp" alt="" width="528px" /> **创建组件** 当List组件的cachedCount属性设为0时,ListItem的创建会发生在H:FlushLayoutTask阶段。如果不为0,则会在H:OnIdle阶段预创建组件。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/vOamHzh61goXgrvs.webp" alt="" width="530px" /> | 序号 | Trace | 描述 | 参数说明 | | --- | --- | --- | --- | | 1 | H:Builder:BuildLazyItem [4] | 创建一个LazyItem项目 | 创建的项目索引 | | 2 | H:Create[Child][self:28] | 创建一个自定义组件 | 自定义组件名、组件ID | | 3 | H:CustomNode:OnAppear && H:aboutToAppear | 执行自定义组件的aboutToAppear 方法 | | | 4 | H:CustomNode:BuildItem [Child][self:28][parent:27] | 执行自定义组件的build方法 | 自定义组件名、组件ID、父组件ID | | 5 | H:Create[Text][self:31] | 创建一个Text组件 | 组件名、组件ID | | 6 | H:Measure[Text][self:31][parent:27][key:] | 计算Text组件布局 | 组件名、组件ID、父组件ID | | 7 | H:LazyForEach predict | LazyForEach预处理 | | | 8 | H:List predict | List组件预处理 | | **销毁组件** 组件销毁只会发生在H:OnIdle空闲阶段。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/CSvHn7mZxwadw04i.webp" alt="" width="529px" /> | 序号 | Trace | 描述 | 参数说明 | | --- | --- | --- | --- | | 1 | H:LazyForEach predict | LazyForEach预处理 | | | 2 | H:aboutToDisappear | 执行自定义组件的aboutToDisappear方法 | | | 3 | H:aboutToBeDeleted | 删除组件 | | **组件复用场景滑动** 基于懒加载场景Demo改造,实现组件复用效果。与懒加载场景相比,在滑动过程中不会发生组件的销毁和创建,而是会在组件将要销毁时,将其放入缓存池中,在需要创建时再从缓存池中取出,并重新赋值。因此本章节主要介绍Reuse阶段和Recycle阶段关键Trace点。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/VkaoAZagZLMlfxWS.webp" alt="" width="532px" /> **Reuse****阶段** <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/8s1jVlwun97OKpQ9.webp" alt="" width="530px" /> | 序号 | Trace | 描述 | 参数说明 | | --- | --- | --- | --- | | 1 | H:CustomNode:BuildRecycle Child | 自定义组件复用,执行aboutToReuse方法 | 复用的组件名 | | 2 | H:Create[Text][self:12] | 创建Text组件 | 组件名、组件ID | | 3 | H:AddDirtyLayoutNode[ListItem][self:61][parent:0] | 标记ListItem为脏区 | 组件名、组件ID、父组件ID | Recycle阶段 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/Dx6xWtXTkfEy4Z8e.webp" alt="" width="526px" /> | 序号 | Trace | 描述 | | --- | --- | --- | | 1 | H:LazyForEach predict | LazyForEach预处理 | | 2 | H:aboutToRecycleInternal | 标识组件进入复用池并调用组件的aboutToRecycle方法 | | 3 | H:ViewFunctions::ExecuteRecycle | 执行组件回收 |

【鸿蒙实战开发】冷启动响应时延问题分析思路&案例

## **1.场景导入** 应用启动可以分为冷启动和热启动: **冷启动:** 当应用启动时,后台没有该应用的进程,这时系统会重新创建一个新的进程分配给该应用, 这种启动方式就叫做冷启动; **热启动:** 当应用程序已经在后台运行,此时用户再次打开应用程序时,应用程序仍然在内存中,可以直接从内存中加载并继续之前的状态,而不需要重新初始化和加载资源,这种称为热启动。 **冷启动响应时延:** 应用冷启动时,从点击应用离手开始到桌面应用图标发生变化(通常指图标变大)的这一段时间称为冷启动响应时延。 ## **2. 性能指标** ### **2.1 性能指标介绍** 冷启动响应时延推荐时间:85ms ### **2.2 性能衡量起止点介绍** <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/2xRl9d29hdMyloRB.webp" alt="" width="515px" /> 应用冷启动响应时延的性能衡量的起点为用户点击应用图标离手帧时间,止点为桌面应用图标开始发生变化的首帧时间。 ## **3. 问题定位流程** ### **3.1 常规定位前置流程** **3.1.1、确认效果** 处理三方应用问题前首先需要先和三方应用及测试确认当前问题场景的预期效果: 1. 三方应用:和三方应用确认问题场景是否认可该标准,如不认可,相关问题需评审关闭。 2. 测试:和测试确认是否按照预期效果执行的测试,测试步骤和性能衡量是否准确。 **3.1.2 查看操作录屏辅助定位** 处理三方应用问题时,可以优先查看操作录屏,查看操作场景,看能否发现一些有助于定位的信息,比如应用启动是否存在横竖屏切换,是否存在页面跳转,是否包含网络加载等等。 **3.1.3 Trace 抓取** 冷启动Trace抓取请参考【附录1: 冷启动Trace抓取方法】。 ### **3.2 问题定位思路** 冷启动响应时延类问题的通用定位思路为先确认时延起止点,然后看起止点时延是否超60ms,未超过则说明达标,超过则根据Trace信息进一步确认问题点,确认责任领域并对齐处理(通常冷启动时延类问题责任领域都在大桌面),处理流程如下图: <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/TfNt8JE9Qv4Rq4R0.webp" alt="" width="527px" /> **注:图中耗时基线均为参考值,仅供定界参考,实际各子系统是否超标需和对应子系统接口人确认。** **3.2.1 确认起止点** **冷启动响应时延起点确认:** <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/vceOjd1w0mItJlaY.webp" alt="" width="533px" /> 起点Trace查找顺序:H:DispatchTouchEvent type=1(大桌面ohos.sceneboard) -> CPU Running Trace(多模子系统mmi_service)-> H:service report(多模子系统mmi_service) 1. 在大桌面泳道(ohos.sceneboard)搜索H:DispatchTouchEvent并且type=1(0,1,2分别代表按下,抬起,移动)的Trace点,该Trace点代表大桌面收到点击离手事件的Trace; 2. 然后找到多模子系统泳道(mmi_service),找到H:DispatchTouchEvent前的一个CPU Running Trace,该Trace下有一个H:service report touchId:{id}, type: up [id: 0, x:{X}, y:{Y}]的Trace点,该Trace点的X,Y坐标和H:DispatchTouchEvent是对应的,且类型也是up,代表的是多模子系统收到点击离手事件的时间,H:service report这个Trace开始位置就是起点。 **冷启动响应时延止点确认:** <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/0t3Srxhb5sXYTHVc.webp" alt="" width="526px" /> **止点Trace 查找顺序:** H:DispatchTouchEvent type=1(大桌面ohos.sceneboard) -> H:FlushMessages(大桌面ohos.sceneboard)-> H:SendCommands(大桌面ohos.sceneboard)-> H:MarshRSTransactionData(大桌面ohos.sceneboard) -> H:RSMainThread::ProcessCommandUni(RS服务render_service)-> H:ReceiveVsync(RS服务render_service) -> H:FlushBuffer(RS服务render_service)-> H:RSHardwareThread(RS送显线程RSHardwareThrea) 1. 在H:DispatchTouchEvent type=1Trace的末尾找到H:FlushMessages(图中1)-> H:SendCommands(图中2)->H:MarshRSTransactionData(图中3)的系列Trace,这些Trace代表大桌面提交图形渲染请求到render_service(RS图形渲染服务)。 2. 选中H:MarshRSTransactionData(图中3)后可以在详情界面点击箭头跳转到render_service泳道对应的Trace H:RSMainThread::ProcessCommandUni(图中4),这个代表render_service收到大桌面渲染请求的点。(H:MarshRSTransactionData后面会有个参数transactionFlag:[2664,523],括号中两个数字分别代表提交请求的进程号和提交的序号,在H:RSMainThread::ProcessCommandUni也会有一个[2664,523]与其对应,两个Trace是通过这个进程号和序号关联起来的。) 3. 然后继续找H:RSMainThread::ProcessCommandUni(图中4)所在的H:ReceiveVsync(图中5)的Trace,接着找该Trace下的H:FlushBuffer(图中6),这里代表render_service渲染完成并刷新数据到缓冲区。 4. 接着找到RS送显线程泳道RSHardwareThrea,找到根据时间顺序找到H:FlushBuffer后面第一个H:RSHardwareThread::CommitAndReleaseLayers,这里提交后就上屏显示了,这个Trace结束就是终点。(大桌面泳道的H:ReceiveVsync和RSHardwareThrea泳道的H:RSHardwareThread::CommitAndReleaseLayers的now字段也是对应的,也可以通过这个字段值直接找到H:ReceiveVsync对应的H:RSHardwareThread::CommitAndReleaseLayers)。 **3.2.2 找问题点** <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/2GNOo0K7EXkOeph9.webp" alt="" width="527px" /> | 序号 | 所属泳道 | Trace点 | 描述 | | --- | --- | --- | --- | | 1 | mmi_service | H:service report(开始) | 多模子系统收到硬件传递过来的点击离手信号 | | 2 | ohos.sceneboard | H:DispatchTouchEvent(开始) | 大桌面收到点击离手事件 | | 3 | ohos.sceneboard | H:FlushMessages(结束) | 大桌面提交图形渲染请求完成 | | 4 | render_service | H:ReceiveVsync(开始) | RS图形渲染服务收到同步信息 | | 5 | RSHardwareThrea | H:RSHardwareThread::CommitAndReleaseLayers(结束) | RS送显线程提交送显完成 | *** | 阶段 | 起点 | 终点 | 耗时基线(参考) | | --- | --- | --- | --- | | 冷启动响应时延 | 上图1 | 上图5 | 60ms | | 多模子系统时延 | 上图1 | 上图2 | 8ms | | 大桌面时延 | 上图2 | 上图3 | 25ms | | 图形子系统时延 | 上图4 | 上图5 | 20ms | 冷启动响应时延类问题找问题点方式如下: 1. 通过Trace确认完冷启动响应时延起止点后,就可以圈出冷启动响应时延的耗时区间(图中1->5),如果耗时未超过60ms,则说明达标,不需要继续分析。如果超过60ms,则需要进一步分析是哪里耗时。 2. 通过H:DispatchTouchEvent,H:FlushMessages,H:ReceiveVsync这几个关键Trace点可以进一步将冷启动响应时延划分为**多模子系统时延,大桌面时延,图形子系统时延**三个部分,然后看每个部分耗时是否超过耗时基线,如果超过则找相应的子系统团队确认处理。 **经验总结:** 就目前来看绝大部分时延超标问题都发生在大桌面时延部分,多模子系统时延和图形子系统时延很少出现,这点从上图中的Trace划分也可以看出来,大桌面时延部分的Trace区间是最长的,因此确认冷却启动响应时延超标后,通常可以直接找大桌面进一步分析超标根因。 **备注:** 冷启动响应时延预预期85ms,其中包含了硬件显示耗时25ms~30ms(硬件耗时包含点击信号从硬件传输到多模子系统及RS提交渲染数据后数据显示到屏幕上这一段时间),这里按25ms算,减去硬件耗时剩下的才是软件耗时60ms。 **3.2.3  根因分析方法** 确认耗时Trace点后,圈中该Trace范围,在下方详情中可以看到方法调用栈及耗时,层层展开就可以定位到耗时的函数,之后可以和相关领域确认函数功能及耗时是否正常。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/A9jwKv28AU0cewUo.webp" alt="" width="527px" /> 由于冷启动响应时延类问题不涉及应用进程(冷启动响应时延期间,应用进程还未开始启动,这段时间主要做的事情是大桌面拉起应用图标),因此这类问题只需要根据3.2.2章节内容定位到问题点确认系统侧责任领域即可,更进一步的根因分析通常由系统侧进行。 如果想了解更详细的冷启动时延范围内的Trace流程解读,见【附录2:冷启动响应时延Trace点解读】 ## **4. 常见根因归档** ### **4.1  因横竖屏转换启动动效导致冷启动响应时延不满足预期** ### **4.1.1  问题根因分析** <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/033JMDv2qGFMV4E4.webp" alt="" width="527px" /> 圈出大桌面时延区间,发现耗时106.6ms,远超性能基线25ms,进一步查看Trace信息发现,H:JSAnimateTo耗时29.3ms,占据了整个耗时区间的三分之一,猜测应该是问题点,再对比正常应用的H:JSAnimateTo,大约在2ms左右,所以基本可以确认H:JSAnimateTo存在问题。联系大桌面分析后得出结论是因为应用添加了横竖屏切换动效导致H:JSAnimateTo变长(通过视频也可以确认存在应用启动存在横竖屏切换)。 正常应用启动H:JSAnimateTo耗时2ms <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/JzYsETPwDBERvcPd.webp" alt="" width="526px" /> **4.1.2 优化方案** 竖屏应用启动时不需要横竖屏转换动效,建议应用侧删除横横竖屏转换动效。 ### **4.2 因启动页图标分辨率过大导致冷启动响应时延不满足预期** **4.2.1 问题根因分析** <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/sqj8y7suBR0RKVmN.webp" alt="" width="528px" /> 圈出大桌面时延区间,发现耗时106.6ms,远超性能基线25ms,进一步查看Trace信息发现,H:JSAnimateToImmediately耗时44.1ms,占据了整个耗时区间近一半的时间,猜测应该是问题点,再对比正常应用的H:JSAnimateToImmediately,大约在13ms左右,所以基本可以确认H:JSAnimateToImmediately存在问题,联系大桌面分析后得出结论是因为应用启动页图标过大(超过256*256)导致H:JSAnimateToImmediately变长。通过Trace也可以看到H:JSAnimateToImmediately下面有个Trace H:ssm:GetStartupPage耗时32.8ms,这个就是加载启动页图标的Trace。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/FfOnE1K3GNu9Cbom.webp" alt="" width="528px" /> 冷启动响应时延问题分析思路&案例 正常H:JSAnimateToImmediately耗时13ms <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/T0ssM439H6q5S8s6.webp" alt="" width="526px" /> **4.2.2 优化方案** 建议使用不超过256*256分辨率的图片走位启动页面图标,以减少图片解码带来的时延。 附录1:冷启动Trace抓取方法 **Step1:** 打开DevEco Studio的Profiler(下图1) -> 选择设备进程(下图2)-> Frame模式(下图3)-> Create Session(下图4)-> 启动录制(下图5)。 **注意:第二步选择进程时不要选择要录制的进程,因为要录制的进程需要处于未启动状态,这里是通过录制其他进程间接录制目标进程的启动Trace 信息。** <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/1B9Lexfap0kX8q2i.webp" alt="" width="527px" /> **Step2:** 启动录制后,在设备上点击应用图标启动要录制的目标应用,等到应用首页加载后,点击停止结束录制。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/f9dxzZ0FyzAnL2tA.webp" alt="" width="525px" /> **Step3 :** 录制完成后会工具会分析展示Trace数据。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/W7Z5yNujuUngfQp6.webp" alt="" width="529px" /> **附录2:冷启动响应时延Trace点解读** 冷启动响应时延的定义是从离手帧到应用图标发生变化的首帧耗时,这其中还包含了硬件耗时25ms-30ms,除去这部分耗时外,剩下的就是软件耗时,软件耗时的大概流程区间如下图: <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/LsxtPlkkIKN5iyIo.webp" alt="" width="527px" /> 抽象流程如下: <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/9n2sMLJkhQGALZ0P.webp" alt="" width="525px" /> 冷启动响应时延的Trace区间为多模子系统H:service report type=up Trace的开始到RS送显线程H:RSHardwareThread Trace的结束。 详细流程如下: 1. 多模子系统mmi_service收到屏幕的点击离手信号Trace点H:service report type=up(type参数类型有down,up,move分别代表按下,抬起,移动),这里是冷启动响应时延的Trace起点。 2. 桌面进程收到多模子系统传递过来的点击事件,Trace点为H:DispatchTouchEvent type=1(type值0,1,2分别代表按下,抬起,移动)。 3. H:DispatchTouchEvent type=1的Trace点下包含H:JSAnimateTo和H:JSAnimateToImmediately两个关键Trace点。这两个Trace也是常见的出问题比较多的Trace点。 * H:JSAnimateTo表示显示动画,包含启动动效加载处理(常见问题是应用启动增加旋转动画导致时间变长) * H:JSAnimateToImmediately包含启动图标的加载,页面组件创建布局刷新(这里常见问题是应用启动图标像素过大导致时间变长) H:JSAnimateToImmediately这个Trace点的最后还包含H:FlushMessages->H:SendCommands->H:MarshRSTransactionData的Trace点,这里是大桌面提交渲染请求给RS渲染服务的流程。H:MarshRSTransactionData Trace点中包含transactionFlag:[2664,523]属性,括号中两个值分别表示RS服务的进程id和提交序号,从H:MarshRSTransactionData可以直接点击箭头跳转到RS服务进程中的H:RSMainThread::ProcessCommandUni[2664,523] Trace点。也可以通过搜索[2664,523]查找(这里只会找到两个关联Trace,一个是大桌面进程中H:MarshRSTransactionData,一个是RS服务进程中的H:RSMainThread::ProcessCommandUni) <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/WQr9TVXIHmLXfSFT.webp" alt="" width="531px" /> 4. 到RS服务中的H:RSMainThread::ProcessCommandUni Trace点表示RS服务收到了大桌面提交的渲染请求,会开始进行图形渲染之后会提交渲染好的图形数据到缓冲区,关键Trace包含H:RSUniRender:FlushFrame->H:FlushBuffer 5. 执行H:FlushBuffer Trace后就会由RS服务送显线程RSHardwareThread执行送显提交,在RSHardwareThrea泳道找到H:FlushBuffer后的第一个Trace H:RSHardwareThread,这里H:Commit后就将渲染好的图像送显上屏了。 | 序号 | 所属泳道 | 关键Trace点 | Trace点说明 | | --- | --- | --- | --- | | 1 | mmi_service | H:service report touchId:[id值], type: up [id: 0, x:[x坐标] , y:[y坐标]] | 手势事件,id代表touch事件id,pointX,pointY表示x,y坐标;type标识手势类型,值有down,up,move分别代表按下,抬起,移动 | | 2 | ohos.sceneboard | H:DispatchTouchEvent id:[id值], pointX=[x坐标] pointY=[y坐标] type=1 | 手势事件,id代表事件id,同一个操作可能会有多个事件,但是id是相同的;pointX,pointY表示x,y坐标;type标识手势类型,值有0,1,2分别代表按下,抬起,移动 | | 3 | ohos.sceneboard | H:JSAnimateTo | 表示显示动画,包含启动动效的加载处理 | | 4 | ohos.sceneboard | H:JSAnimateToImmediately | 启动图标加载,组件创建页面布局刷新 | | 5 | ohos.sceneboard | H:FlushDirtyNodeUpdate | 用来标识由于变量更新,触发了某一个组件的标脏,需要对它进行刷新 | | 6 | ohos.sceneboard | H:UITaskScheduler::FlushTask | 组件创建布局和测量,这一块可以确认是什么组件在创建或者复用 | | 7 & 8 & 9 | ohos.sceneboard | H:FlushMessages & H:SendCommands & H:MarshRSTransactionData cmdCount:[num] transactionFlag:[发送进程id,序列号] | 发送渲染请求到Render_Service图形渲染服务 | | 10 | render_service | H:RSMainThread::ProcessCommandUni:[发送进程id,序列号] | Render_Service图形渲染服务收到渲染消息,可以和发送进程的Trace对应起来 | | 11 & 12 | render_service | H:RSUniRender:FlushFrame & H:FlushBuffer | 刷新图形数据到缓冲区 | | 13 & 14 | RSHardwareThrea | H:RSHardwareThread & H:Commit | 提交渲染的帧数据 | 下面针对页面布局刷新Trace点做一些补充描述: <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/CIgSsBtfwJgl2TQs.webp" alt="" width="528px" /> 由于页面布局刷新属于通用Trace信息,描述表格不添加泳道信息: | 序号 | 关键Trace点 | Trace点说明 | | --- | --- | --- | | 1 | H:FlushDirtyUpdate | 用来标识由于变量更新,触发了某一个组件的标脏,需要对它进行刷新。例如上图中SCBScenePanel被标脏了 | | 2 | H:CustomNodeUpdate [组件] | 表示什么组件发生了变化 | | 3 & 4 & 5 | H:UITaskScheduler::FlushTask & H:FlushLayoutTask & H:CreateTaskMeasure | 组件创建刷新,页面布局测量 | | 6 | H:CustomNode:BuildItem [组件] | 表示新创建了什么组件,如上图新创建了SCBSceneContainer组件,该Trace的下发还可以看到创建组件的生命周期 | | 7 | H:aboutToAppear | 标识对应组件的初始化的生命周期 | | 8 | H:ssm:GetStartupPage | 加载启动页图标的Trace |

【鸿蒙实战开发】基于子窗口实现应用内悬浮窗

## **场景描述** app应用会使用悬浮窗/悬浮球的方式来给用户展示一些应用重要&便捷功能的入口,类似android和iOS应用中常见的应用内可拖拽的悬浮球和小窗口视频悬浮窗,点击悬浮窗修改悬浮窗样式和响应事件跳转页面,在跳转页面后依然可以显示在屏幕中上个页面拖拽后的固定位置等。 应用经常会遇到如下的业务诉求: **场景一**:通过事件添加和移除悬浮窗,悬浮窗样式可定制(暂定两种,无白边圆球形和小视频播放窗口类型),可代码修改位置和布局。 **场景二**:创建悬浮窗后,主窗口的系统侧滑返回事件可正常使用。 **场景三**:可响应正常点击事件,可通过触发拖动使悬浮窗的移动,根据最后手势停留位置,做动画靠屏幕左或靠右显示,跳转和返回上级页面后悬浮窗依然存在,且相对手机屏幕位置不变。 **场景四**:悬浮窗内组件事件触发主窗口的页面跳转(Router和Navigation两种都要有)。 **场景五**:悬浮窗的窗口大小自适应组件,子窗口中页面设置了宽高,需要让子窗口自适应页面组件大小。 **场景六**:支持控制悬浮窗隐藏和销毁。 **场景七**:视频类应用主动调用画中画完成后台播放,以及返回桌面时自动启动画中画。 ## **方案描述** ### **场景一:** 通过事件添加和移除悬浮窗,悬浮窗样式可定制(暂定两种,无白边圆球形和小视频播放窗口类型),可代码修改位置和布局。 **效果图** <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/GOIBgfx6QwEJeoqZ.webp" alt="" width="432px" /> **方案** 通过子窗口创建windowStage.*createSubWindow*('mySubWindow'),和windowClass.setWindowLayoutFullScreen去除白边。 **核心代码** ``` 在EntryAbility中获取WindowStage。 onWindowStageCreate(windowStage: window.WindowStage): void {   // Main window is created, set main page for this ability   hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate');   windowStage.loadContent('pages/Page', (err, data) => {   if (err.code) {   hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');   return; } // 保存窗口管理器 AppStorage.setOrCreate("windowStage", windowStage); hilog.info(0x0000, 'testTag', 'Succeeded in loading the content. Data: %{public}s', JSON.stringify(data) ?? ''); }); } ``` 创建子窗口,子窗口样式由子窗口加载的页面组件样式决定。 ``` this.windowStage.createSubWindow("mySubWindow", (err, windowClass) => {   if (err.code > 0) {     console.error("failed to create subWindow Cause:" + err.message)     return;   }   try {     // 设置子窗口加载页     windowClass.setUIContent("pages/MySubWindow", () => {       windowClass.setWindowBackgroundColor("#00000000")     });     // 设置子窗口左上角坐标     windowClass.moveWindowTo(0, 200)     // 设置子窗口大小     windowClass.resize(vp2px(75), vp2px(75))     // 展示子窗口     windowClass.showWindow();     // 设置子窗口全屏化布局不避让安全区     windowClass.setWindowLayoutFullScreen(true);   } catch (err) {     console.error("failed to create subWindow Cause:" + err)   } }) ``` ## **场景二:** 创建悬浮窗后,主窗口的系统侧滑返回事件可正常使用。 **效果图** <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/Pa5H5hVZkhFt3gjN.webp" alt="" width="424px" /> **方案** 通过window.*shiftAppWindowFocus*转移窗口焦点实现创建子窗口后,主窗口依然可以响应事件。**核心代码** 在子窗口中将焦点转移到主窗口。 ``` onPageShow(): void {   setTimeout(() => {   // 获取子窗口ID   let subWindowID: number = window.findWindow("mySubWindow").getWindowProperties().id   // 获取主窗口ID   let mainWindowID: number = this.windowStage.getMainWindowSync().getWindowProperties().id   // 将焦点从子窗口转移到主窗口   window.shiftAppWindowFocus(subWindowID, mainWindowID) }, 500) } ``` ### **场景三:** 可响应正常点击事件,可通过拖动触发悬浮窗的拖拽移动,根据最后手势停留位置,做动画靠屏幕左或靠右显示,跳转和返回上级页面后悬浮窗依然存在,且相对手机屏幕位置不变。 **效果图** <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/Rg2iQeca6WtrhAbU.webp" alt="" width="424px" /> **方案** 通过设置手势顺序模式识别PanGesture,实现拖拽悬浮窗。 **核心代码** 创建Position。 ``` interface Position {   x: number,   y: number } ``` 设置拖拽选项。 ``` private panOption: PanGestureOptions = new PanGestureOptions({ direction: PanDirection.All }); ``` 通过在子窗口父组件绑定拖拽动作完成悬浮窗坐标移动。 ``` .gesture(   // 声明该组合手势的类型为Sequence类型   PanGesture(this.panOption)     .onActionStart((event: GestureEvent) => {       console.info('Pan start');     })// 发生拖拽时,获取到触摸点的位置,并将位置信息传递给windowPosition     .onActionUpdate((event: GestureEvent) => {       this.windowPosition.x += event.offsetX;       this.windowPosition.y += event.offsetY;       this.subWindow.moveWindowTo(this.windowPosition.x, this.windowPosition.y)     })     .onActionEnd((event: GestureEvent) => {       // 贴边判断       if (event.offsetX > 0) {         this.windowPosition.x = display.getDefaultDisplaySync().width - this.subWindow.getWindowProperties()           .windowRect           .width;       } else if (event.offsetX < 0) {         this.windowPosition.x = 0;       }       this.subWindow.moveWindowTo(this.windowPosition.x, this.windowPosition.y)       console.info('Pan end');     }) ) ``` ## **场景四:** 悬浮窗内组件事件触发主窗口的页面跳转(Router和Navigation两种都要有)。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/3u0f6xjmS4y7fu6R.webp" alt="" width="424px" /> <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/RdtQFOpK3bxXUCI4.webp" alt="" width="424px" /> **方案** 通过获取窗口上下文,实现在悬浮窗点击后,实现主窗口Router跳转。 通过配置NavPathStack全局变量,实现主窗口navigation跳转 。 **核心代码** 通过windowStage获取主窗口的Router,实现主窗口的Router跳转。 ``` .onClick((event: ClickEvent) => {   this.windowStage.getMainWindowSync()     .getUIContext()     .getRouter()     .back() }) ``` 通过AppStorage获取NavPathStack,实现主窗口navigation跳转。 ``` .onClick((event: ClickEvent) => {   let navPath = AppStorage.get("pageInfos") as NavPathStack;   navPath.pushPath({ name: 'pageOne' }) }) ``` ## **场景五:** 悬浮窗的窗口大小自适应组件,子窗口中页面设置了宽高,需要让子窗口自适应页面组件大小。 **效果图** <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/f1lyPCtcfzf5OwTh.webp" alt="" width="424px" /> **方案** 通过监听通用事件ComponentObserver,设置window的resize调整窗口大小。 **核心代码** 查找子窗口。 ``` @State subWindow: window.Window = window.findWindow("mySubWindow"); ``` 注册监听事件。 ``` //监听id为COMPONENT_ID的组件回调事件listener: inspector.ComponentObserver = inspector.createComponentObserver('COMPONENT_ID'); 通过onClick()事件,实现对组件变化的监听。 if (this.flag) {   Image($r("app.media.voice2"))     .id("COMPONENT_ID")     .borderRadius(5)     .width(75)     .height(75)     .onClick(() => {       // 设置图标切换标识       this.flag = !this.flag       this.listener.on('layout', () => {         // 监听布局变更后调整子窗大小         this.subWindow.resize(componentUtils.getRectangleById("COMPONENT_ID").size.width,           componentUtils.getRectangleById("COMPONENT_ID").size.height)       })     }) } else {   Image($r("app.media.voice"))     .id("COMPONENT_ID")     .borderRadius(50)     .width(100)     .height(100)     .onClick(() => {       this.flag = !this.flag       this.listener.on('layout', () => {         this.subWindow.resize(componentUtils.getRectangleById("COMPONENT_ID").size.width,           componentUtils.getRectangleById("COMPONENT_ID").size.height)       })     }) ``` ## **场景六:** 支持控制悬浮窗隐藏和销毁。 **效果图** <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/aaaFXKX3ywLhVjx3.webp" alt="" width="424px" /> **方案** 通过设置窗口windowClass.minimize和windowClass.destroyWindow,实现悬浮窗的隐藏和销毁。 **核心代码** 通过调用minimize,实现子窗口最小化。 ``` .onClick((event: ClickEvent) => {   this.subWindow.minimize() }) ``` 通过实现destroyWindow,实现子窗口的资源销毁。 ``` // 通过查找子窗口名称对子窗口进行销毁 window.findWindow("mySubWindow").destroyWindow() ``` **场景七:** 视频类应用主动调用画中画完成后台播放,以及返回桌面时自动启动画中画。 **效果图** <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/YdI2gwWdE6sLoFvF.webp" alt="" width="424px" /> **方案** 1.通过pipController.startPiP()完成主动调用画中画功能。 2.通过pipController.setAutoStartEnabled(true)在返回桌面时完成全局画中画播放。 **核心代码** 创建XComponent组件。 ``` XComponent({ id: 'pipDemo', type: 'surface', controller: this.mXComponentController })   .onLoad(() => {     this.surfaceId = this.mXComponentController.getXComponentSurfaceId();     // 需要设置AVPlayer的surfaceId为XComponentController的surfaceId     this.player = new AVPlayerDemo(this.surfaceId);     this.player.avPlayerFdSrcDemo();   })   .onDestroy(() => {     console.info(`[${TAG}] XComponent onDestroy`);   })   .size({ width: '100%', height: '800px' }) 创建pipWindowController和startPip方法。 startPip() {   if (!pipWindow.isPiPEnabled()) {     console.error(`picture in picture disabled for current OS`);     return;   }   let config: pipWindow.PiPConfiguration = {     context: getContext(this),     componentController: this.mXComponentController,     // 当前page导航id     navigationId: this.navId,     // 对于视频通话、视频会议等场景,需要设置相应的模板类型     templateType: pipWindow.PiPTemplateType.VIDEO_PLAY,     // 可选,创建画中画控制器时系统可通过XComponent组件大小设置画中画窗口比例     contentWidth: 800,     // 可选,创建画中画控制器时系统可通过XComponent组件大小设置画中画窗口比例     contentHeight: 600,   };   // 步骤1:创建画中画控制器,通过create接口创建画中画控制器实例   let promise: Promise<pipWindow.PiPController> = pipWindow.create(config);   promise.then((controller: pipWindow.PiPController) => {     this.pipController = controller;     // 步骤1:初始化画中画控制器     this.initPipController();     // 步骤2:通过startPiP接口启动画中画     this.pipController.startPiP().then(() => {       console.info(`Succeeded in starting pip.`);     }).catch((err: BusinessError) => {       console.error(`Failed to start pip. Cause:${err.code}, message:${err.message}`);     });   }).catch((err: BusinessError) => {     console.error(`Failed to create pip controller. Cause:${err.code}, message:${err.message}`);   }); } ``` 初始化pipWindowController。 ``` initPipController() {   if (!this.pipController) {     return;   }   // 通过setAutoStartEnabled接口设置是否需要在应用返回桌面时自动启动画中画,注册stateChange和controlPanelActionEvent回调   this.pipController.setAutoStartEnabled(true/*or true if necessary*/); // 默认为false   this.pipController.on('stateChange', (state: pipWindow.PiPState, reason: string) => {     this.onStateChange(state, reason);   });   this.pipController.on('controlPanelActionEvent', (event: pipWindow.PiPActionEventType) => {     this.onActionEvent(event);   }); } ``` 完成画中画播放使用stopPip方法停止。 ``` stopPip() {   if (this.pipController) {     let promise: Promise<void> = this.pipController.stopPiP();     promise.then(() => {       console.info(`Succeeded in stopping pip.`);       this.pipController?.off('stateChange'); // 如果已注册stateChange回调,停止画中画时取消注册该回调       this.pipController?.off('controlPanelActionEvent'); // 如果已注册controlPanelActionEvent回调,停止画中画时取消注册该回调     }).catch((err: BusinessError) => {       console.error(`Failed to stop pip. Cause:${err.code}, message:${err.message}`);     });   } } ``` **其他常见问题** Q:windowStage怎么获取? A:WindowStage需要在EntryAbility中的onWindowStageCreate中用AppStorage.setOrCreate()获取。 Q:子窗口可以用于应用外么? A:子窗口只能在应用内使用。 Q:子窗口的默认大小是多大? A:子窗口默认不设置大小的话是除安全区外的屏幕区域。 Q:UIExtension可以用子窗口么? A:UIExtension不是窗口对象,没有办法调用窗口接口。 Q:Har和Hsp中可以使用子窗口么? A:只要能获取到windowStage就能创建并使用子窗口。 ## 写在最后 **如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙**: * 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。 * 关注小编,同时可以期待后续文章ing🚀,不定期分享原创知识。 * **想要获取更多完整鸿蒙最新学习知识点,请移步前往小编:[`https://gitee.com/MNxiaona/733GH/blob/master/qita.md`](https://gitee.com/MNxiaona/733GH/blob/master/qita.md)** <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/dYF038ZRZeP2lmAz.webp" alt="" width="100%" />

【鸿蒙实战开发】基于ArkUI现有能力实现自定义弹窗封装方案

## **场景描述** 自定义弹窗是应用开发需要实现的基础功能,包括但不限于HarmonyOS开发者文档中定义的模态、半模态、Toast等形式,封装一个好用且和UI组件解耦的弹窗组件是开发者的高频诉求 自定义弹窗通常的使用场景有: ### **场景一:在公共逻辑中触发弹窗** 登录提示弹窗、全屏广告弹窗、网络请求与其他操作行为的提示、异常弹窗 ### **场景二:侧滑手势拦截** 隐私弹窗的拦截,退出登录时的确认弹窗 ### **场景三:切换页面弹窗不消失** 隐私弹窗和二级页面中的半模态弹窗 ### **场景四:自定义弹出、关闭动画** 从下往上的抽屉式弹出、关闭时从上往下收回 ### **场景五:透明、模态、半模态背景** 应用实现自定义的背景颜色 ## **方案描述** **1. 使用Navigation.Dialog** 基于Navigation.Dialog的透明页面特性,可以用于实现弹窗效果 而且Navigation.Dialog存在于路由栈中,天然可以实现切换页面弹窗不消失 **当前限制:** 弹窗组件中的动效建议开发者自行实现 Navigation.Dialog自身无颜色,需要开发者自行实现模态遮罩,以及手势事件。 **演示效果:** <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/5mKY4x0r0rM8ITDJ.webp" alt="" width="320px" /> <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/y6duoEFoUHx6qORW.webp" alt="" width="320px" /> <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/EDvOzYeCEwBILTKG.webp" alt="" width="320px" /> <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/FLyFNuPgSvUoFlmD.webp" alt="" width="320px" /> 对于少量弹窗的实现,可以直接使用Navigation来进行路由跳转,参考 Navigation常见场景及解决方案 其他`Navigation`的使用也可参考上述文章 ## **步骤一:封装路由工具类,并注册自定义弹窗组件** 定义路由工具类AppRouter,并创建路由栈NavPathStack ``` export class AppRouter {   private static instance = new AppRouter();   private pathStack: NavPathStack = new NavPathStack();  // 初始化路由栈   public static getInstance(): AppRouter {     return AppRouter.instance;   }   public getPathStack(): NavPathStack {     return this.pathStack;   }   ... } ``` 在根页面中注册NavPathStack ``` @Entry @Component struct Index {   build() {     Navigation(AppRouter.getInstance().getPathStack()) {       ...     }   } } ``` 在.navDestination注册封装的自定义弹窗组件DefaultDialog ``` @Builder PageMap(name: string) {   if (name === CommonConstants.DEFAULT_DIALOG) {     DefaultDialog()   }   ... } Navigation(AppRouter.getInstance().getPathStack()) {   ... }.navDestination(this.PageMap) ``` 进阶用法:可以参考动态路由案例实现动态路由, HarmonyOS NEXT应用开发案例集 - Gitee.com ### **步骤二:封装弹窗UI 组件** 定义弹窗选项类AppDialogOption ``` export class AppDialogOption {   view?: WrappedBuilder<Object[]> // 自定义弹窗内容组件   buildParams?: Object  // 自定义弹窗内容参数   params?: Object  // 打开时传递参数   autoClose?: number  // 自动关闭时间   onPop?: (data: PopInfo) => void  // 接收上一个弹窗关闭时的参数回调   onBackPressed?: () => boolean  // 侧滑返回拦截   styles?: AppDialogStyle = new AppDialogStyle()  // 弹窗样式   animation?: TransitionEffect  // 弹窗动画   instance?: AppDialog  // 弹窗操作对象 } 定义弹窗样式类AppDialogStyle export class AppDialogStyle {   transparent: boolean = false   background: string = 'rgba(0,0,0,0.5)'   radius: Length = 5   align: Alignment = Alignment.Center } ``` 创建自定义弹窗组件DefaultDialog 通过Stack布局及2个Column容器实现模态遮罩和自定义弹窗内容,通过NavDestinationMode定义页面类型 ``` @Component export struct DefaultDialog {   private dialogOptions?: AppDialogOption;   build() {     NavDestination() {       Stack() {         Column() {           // 模态遮罩         }         Column() {           // 弹窗内容         }       }       .width("100%")       .height("100%")     }     .mode(NavDestinationMode.DIALOG)  // 页面类型为dialog   } } ``` 通过.backgroundColor设置模态遮罩的背景颜色 ``` ... Stack() {   Column() {     // 模态遮罩   }   .backgroundColor(this.dialogOptions?.styles?.transparent ? Color.Transparent : this.dialogOptions?.styles?.background) // 背景颜色   Column() {     // 弹窗内容   } } ``` 通过Stack.alignContent设置弹窗定位 ``` Stack({   alignContent: this.dialogOptions?.styles?.align }) {   Column() {     // 模态遮罩   }   Column() {     // 弹窗内容   } } ``` ### **步骤三:封装弹窗控制器,与UI 组件解耦** 提供链式调用的Api ``` export class AppDialog {   static indexArr: number[] = [];   private stackIndex: number = 0;   private options?: AppDialogOption;   public static buildWithOptions(options?: AppDialogOption): AppDialog {     let instance: AppDialog = new AppDialog();     // 获取并保存弹窗的路由栈序号     let index: number = AppRouter.getInstance().getPathStack().size() - 1;     AppDialog.indexArr.push(index);     instance.stackIndex = index;     instance.options = options;     options!.instance = instance;     return instance;   }   public static build(builder: WrappedBuilder<Object[]>): AppDialog {     let options: AppDialogOption = new AppDialogOption();     options.view = builder;     return AppDialog.buildWithOptions(options);   }   public static toast(msg: string): AppDialog {     let options: AppDialogOption = new AppDialogOption();     options.view = AppDialog.toastBuilder;     options.buildParams = msg;     return AppDialog.buildWithOptions(options);   }   public static closeAll(): void {     AppRouter.getInstance().getPathStack().removeByName(CommonConstants.DEFAULT_DIALOG);   }   public static closeLast(params?: Object): void {     let lastIndex = AppDialog.indexArr.pop()     if (!lastIndex) {       AppDialog.closeAll();     } else if (lastIndex && AppRouter.getInstance().getPathStack().size() > lastIndex) {       AppRouter.getInstance().getPathStack().popToIndex(lastIndex, params);     }   }   public open(): AppDialog {     AppRouter.getInstance()       .getPathStack()       .pushPathByName(CommonConstants.DEFAULT_DIALOG, this.options, this.options!.onPop!, true);     return this;   }   public close(params?: Object): void {     if (AppRouter.getInstance().getPathStack().size() > this.stackIndex) {       AppRouter.getInstance().getPathStack().popToIndex(this.stackIndex, params);     }   }   public buildParams(buildParams: Object): AppDialog {     this.options!.buildParams = buildParams;     return this;   }   public params(params: Object): AppDialog {     this.options!.params = params;     return this;   }   public onBackPressed(callback: () => boolean): AppDialog {...}   public onPop(callback: (data: PopInfo) => void): AppDialog {...}   public animation(animation: TransitionEffect): AppDialog {...}   public autoClose(time: number): AppDialog {...}   public align(align: Alignment): AppDialog {...}   public transparent(transparent: boolean): AppDialog {...} } ``` ### **步骤四:页面与弹窗,弹窗与弹窗之间传递参数** 通过路由跳转NavPathStack.pushPathByName传递参数 在弹窗组件的.onReady事件中获取路由跳转参数。 ``` @Component export struct DefaultDialog {   private dialogOptions?: AppDialogOption;   build() {     NavDestination() {       ...     }     .onReady((ctx: NavDestinationContext) => {       console.log("onReady")       this.dialogOptions = ctx.pathInfo.param as AppDialogOption;     })   } } ``` 使用NavPathStack中的onPop回调来接收上一个弹窗返回的参数。 ``` onPop = (data: PopInfo) => {   console.log("onPop")   // 更新状态变量   this.params[index] = JSON.stringify(data.result) } navPathStack.pushPathByName(CommonConstants.DEFAULT_DIALOG, this.options, this.options!.onPop!, true) ``` 上一个弹窗在关闭时传入参数 ``` navPathStack.popToIndex(this.stackIndex, params); ``` ### **步骤五:实现弹窗自定义动画** 通过.transition属性分别实现背景和内容的转场动画 ``` ... Stack() {   Column() {     // 模态遮罩   }   .transition(  // 转场动画     TransitionEffect.OPACITY.animation({       duration: 300,       curve: Curve.Friction     })   )   Column() {     // 弹窗内容   }   .transition(  // 转场动画     this.dialogOptions?.animation ?       this.dialogOptions?.animation :     TransitionEffect.scale({ x: 0, y: 0 }).animation({       duration: 300,       curve: Curve.Friction     })   ) } ``` 通过监听模态遮罩的点击事件实现关闭动画 ``` ... Stack() {   Column() {     // 模态遮罩   }   .opacity(this.opacityNum)   .onClick(() => {     animateTo({       duration: 200,       curve: Curve.Friction,       onFinish: () => {         this.dialogOptions?.instance?.close();       }     }, () => {       this.opacityNum = 0  // 修改模态遮罩的透明度       if (this.dialogOptions?.styles?.align === Alignment.Bottom) {         this.translateY = "100%"       }     })   })   Column() {     // 弹窗内容   }   .translate({ x: 0, y: this.translateY }) } ``` ### **步骤五:实现自定义弹窗内容** 在弹窗内容的Column容器中传入WrappedBuilder来实现动态的自定义弹窗内容。 ``` Stack() {   Column() {     // 模态遮罩   }   Column() {     // 弹窗内容     this.dialogOptions?.view?.builder(this.dialogOptions);   } } ``` 定义弹窗内容组件 ``` @Builder export function DialogViewBuilder(dialogOptions: AppDialogOption) {   DialogView({ options: dialogOptions }) } @Component struct DialogView {   private options?: dialogOptions ;   build() {     Column() {     }     ...   } } ``` ### **步骤六:侧滑手势拦截** 在弹窗组件的.onBackPressed事件中进行拦截 ``` @Component export struct DefaultDialog {   private dialogOptions?: AppDialogOption;   build() {     NavDestination() {       ...     }     .onBackPressed((): boolean => {       // true为拦截       if (this.dialogOptions?.onBackPressed) {         return this.dialogOptions?.onBackPressed()       } else {         return false;       }     })   } } ``` **使用效果:** 使用弹窗控制器即可在非UI业务逻辑中打开弹窗 ``` export class AppService {   buzz(): void {     setTimeout(() => {       AppDialog         .toast("登录成功")         .onBackPressed(() => true)         .autoClose(2000)         .transparent(true)         .open();     }, 1000)  // 模拟业务接口调用耗时   } } AppDialog.toastBuilder = wrapBuilder(ToastViewBuilder) @Builder export function ToastViewBuilder(dialogOptions: AppDialogOption) {   ToastView({ msg: dialogOptions.buildParams as string }) } @Component struct ToastView {   private msg?: string;   build() {     Column() {       Text(this.msg)         .fontSize(14)         .fontColor(Color.White)         .padding(10)     }     .backgroundColor("rgba(0,0,0,0.8)")     .justifyContent(FlexAlign.Center)     .borderRadius(12)     .width(100)   } } ``` 关闭弹窗 ``` // 全局使用 AppDialog.closeLast(); AppDialog.closeAll(); // 弹窗页面中使用 this.dialogOptions?.instance?.close(); ``` ## 写在最后 **如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:** * 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力; * 关注小编,同时可以期待后续文章ing🚀,不定期分享原创知识; * **想要获取更多完整鸿蒙最新学习知识点,可关注B站:码牛课堂鸿蒙开发;**

【鸿蒙实战开发】基于Taskpool的多线程操作

## **场景描述** **场景一**:周期性任务处理,业务通过taskpool周期性处理业务。 **场景二**:延迟业务处理,业务一段时间后,通过taskpool处理业务。 **场景三**:串行业务处理,业务开展过程中,需要处理一系列的事务,事务处理过程中,存在先后次序。 **场景四**:业务的处理存在紧急优先次序,支持设置taskpool优先级处理。 **场景五**:ArkTS与Native协作开展业务,在ArkTS层触发业务,通过NAPI接口,传递到Native C++层,作业务管理等处理。 ## **方案描述** ### **场景一:周期性任务** 方案: 1)定时器判断周期性事务执行。 2)Taskpool来处理任务执行。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/Oy7PWV7vDzLNJfyH.webp" alt="" width="100%" /> 核心代码: ``` @Concurrent function ServiceHandle(pars: number): number {   hilog.info(0x0000, 'testTag', 'start ServiceHandle:%{public}d', pars);   // 业务处理过程,并将结果返回   let result = 0;   return result; } let count = 0; function TimerOutHandle(pars:number) {   count++;   let task: taskpool.Task = new taskpool.Task(ServiceHandle, pars);   hilog.info(0x0000, 'testTag', 'Timer handle count :%{public}d,pars %{public}d', count, pars);   taskpool.execute(task, taskpool.Priority.HIGH).then((res: object) => {     hilog.info(0x0000, 'testTag', 'ServiceHandle result :%{public}d', res);     if (g_callback != null) {       g_callback(count);     }   }); } let timerId = -1; export function TimerTest() {   count = 0;   let value = 88;   timerId = setInterval(TimerOutHandle, 3000, value); } ``` 定时器每3秒超时一次,进入TimerOutHandle函数处理,TimerOutHandle函数体中,通过taskpool创建异步并发任务执行业务。 **运行结果:** <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/oWYY1Lceok77VNxd.webp" alt="" width="100%" /> 界面上,每超时一次,会呈现运行次数: <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/koRLKLR306s6e9cy.webp" alt="" width="417px" /> ### **场景二:延迟任务** 方案: 1)通过setTimeout来延迟处理。 2) 通过executeDelayed来延迟处理。 核心代码: 1)setTimeout的处理如下: ``` @Concurrent function ServiceHandle(pars: number): number {   hilog.info(0x0000, 'testTag', 'start ServiceHandle:%{public}d', pars);   // 业务处理过程,并将结果返回   let result = 0;   return result; } let count = 0; function TimerOutHandle(pars:number) {   count++;   let task: taskpool.Task = new taskpool.Task(ServiceHandle, pars);   hilog.info(0x0000, 'testTag', 'Timer handle count :%{public}d,pars %{public}d', count, pars);   taskpool.execute(task, taskpool.Priority.HIGH).then((res: object) => {     hilog.info(0x0000, 'testTag', 'ServiceHandle result :%{public}d', res);     if (g_callback != null) {       g_callback(count);     }   }); } export function OneTimerCallTest() {   count = 0;   if (g_callback != null) {     g_callback(count);   }   let value = 99;   hilog.info(0x0000, 'testTag', 'start setTimeout');   timerId = setTimeout(TimerOutHandle, 3000, value); } ``` 定时器3秒超时(仅仅执行一次)后,就会进入TimerOutHandle函数处理,TimerOutHandle函数体中,通过taskpool创建异步并发任务执行业务。 2)executeDelayed来延迟 ``` @Concurrent function TaskDelayServiceHandle(pars: number): number {   let t: number = Date.now();   hilog.info(0x0000, 'testTag', 'enter TaskDelayServiceHandle, timer is :%{public}d', t);   // 业务处理过程,并将结果返回   let result = 0;   return result; } export function TaskPoolDelayTest() {   count = 0;   if (g_callback != null) {     g_callback(count);   }   let value = 100;   let t: number = Date.now();   hilog.info(0x0000, 'testTag', 'taskpool start time is :%{public}d', t);   let task: taskpool.Task = new taskpool.Task(TaskDelayServiceHandle, value);   taskpool.executeDelayed(3000, task).then(() => {     count++;     let t: number = Date.now();     hilog.info(0x0000, 'testTag', 'taskpool execute success, time is :%{public}d', t);     if (g_callback != null) {       g_callback(count);     }   }).catch((e: BusinessError) => {     console.error(`taskpool execute: Code: ${e.code}, message: ${e.message}`);   }) } ``` 调用executeDelayed函数3秒后,会进入TaskDelayServiceHandle函数执行,返回返回后,会进入executeDelayed后面的then的函数体中执行。 **运行结果:** 1)使用setTimeout运行结果 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/Fc84EA4Dl4BwLvdc.webp" alt="" width="100%" /> 2)使用executeDelayed运行结果 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/RezpwzM8Zh2GkzFZ.webp" alt="" width="100%" /> ### **场景三:串行任务** 方案: 1)最简单的方案就是后面任务执行时,根据前面任务的执行结果来处理。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/4qW5I7ZwqqEUKlNa.webp" alt="" width="286px" /> 2)后面任务的执行,依赖另一个任务的一些处理结果后,继续执行。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/MJyjjvOH2JYhD6It.webp" alt="" width="459px" /> 核心代码: 1)通过业务逻辑的结果来处理 ``` @Concurrent function ServiceHandle1(pars: number): number {   hilog.info(0x0000, 'testTag', 'start ServiceHandle1:%{public}d', pars);   // 业务处理过程,并将结果返回   let result = 0;   return result; } @Concurrent function ServiceHandle2(pars: number): number {   hilog.info(0x0000, 'testTag', 'start ServiceHandle2:%{public}d', pars);   // 业务处理过程,并将结果返回   let result = 1;   return result; } export function SyncHandle() {   let task1: taskpool.Task = new taskpool.Task(ServiceHandle1, 1);   hilog.info(0x0000, 'testTag', 'sync handle');   taskpool.execute(task1, taskpool.Priority.HIGH).then((res1: object) => {     hilog.info(0x0000, 'testTag', 'ServiceHandle result :%{public}d', res1);     if (g_callback != null) {       g_callback('task1 finish.');     }     if ((res1 as Number) == 0) {       let task2: taskpool.Task = new taskpool.Task(ServiceHandle2, 2);       taskpool.execute(task2, taskpool.Priority.HIGH).then((res2: object) => {         hilog.info(0x0000, 'testTag', 'ServiceHandle2 result :%{public}d', res2);         if (g_callback != null) {           g_callback('task2 finish.');         }       });     }   }); } ``` task1执行完毕后,根据if判断启动task2任务执行。 2)通过addDependency或SequenceRunner处理。 ``` @Concurrent function DependencyHandle(args: number): number {   let t: number = Date.now();   while ((Date.now() - t) < 1000) {     continue;   }   return args; } export function AddDependencyTest() {   let task1:taskpool.Task = new taskpool.Task(DependencyHandle, 100);   let task2:taskpool.Task = new taskpool.Task(DependencyHandle, 200);   let task3:taskpool.Task = new taskpool.Task(DependencyHandle, 300);   hilog.info(0x0000, 'testTag', 'dependency: add dependency start');   task1.addDependency(task2);   task2.addDependency(task3);   hilog.info(0x0000, 'testTag', 'dependency: add dependency end');   hilog.info(0x0000, 'testTag', 'dependency: start execute second');   taskpool.execute(task1).then(() => {     hilog.info(0x0000, 'testTag', 'dependency: first task1 success');     if (g_callback != null) {       g_callback('task1 finish.');     }   })   taskpool.execute(task2).then(() => {     hilog.info(0x0000, 'testTag', 'dependency: second task2 success');     if (g_callback != null) {       g_callback('task2 finish.');     }   })   taskpool.execute(task3).then(() => {     hilog.info(0x0000, 'testTag', 'dependency: third task3 success');     if (g_callback != null) {       g_callback('task3 finish.');     }   }) } ``` task1依赖task2,task2依赖task3,上面任务执行的顺序是:task3执行完毕后再执行task2,最后执行task。 ``` @Concurrent function additionDelay(delay:number): void {   let start: number = new Date().getTime();   while (new Date().getTime() - start < delay) {     continue;   } } @Concurrent function waitForRunner(finalString: string): string {   return finalString; } export async function SeqRunnerTest() {   let finalString:string = "";   let task1:taskpool.Task = new taskpool.Task(additionDelay, 3000);   let task2:taskpool.Task = new taskpool.Task(additionDelay, 2000);   let task3:taskpool.Task = new taskpool.Task(additionDelay, 1000);   let task4:taskpool.Task = new taskpool.Task(waitForRunner, finalString);   let runner:taskpool.SequenceRunner = new taskpool.SequenceRunner();   runner.execute(task1).then(() => {     finalString += 'task1 finish.';     hilog.info(0x0000, 'testTag', 'seqrunner: task1 done.');     if (g_callback != null) {       g_callback('task1 finish.');     }   });   runner.execute(task2).then(() => {     finalString += 'task2 finish.';     hilog.info(0x0000, 'testTag', 'seqrunner: task2 done.');     if (g_callback != null) {       g_callback('task2 finish.');     }   });   runner.execute(task3).then(() => {     finalString += 'task3 finish.';     hilog.info(0x0000, 'testTag', 'seqrunner: task3 done.');     if (g_callback != null) {       g_callback('task3 finish.');     }   });   await runner.execute(task4);   hilog.info(0x0000, 'testTag', 'seqrunner: task4 done, finalString is %{public}s', finalString); } ``` task1执行完毕后,执行task2,最后是task3执行完毕。 运行结果: 1)通过业务逻辑的结果来处理 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/ag2Xtr1MArKC8o7p.webp" alt="" width="100%" /> 2)通过addDependency或SequenceRunner处理 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/xSei3GWXbe23FxuU.webp" alt="" width="100%" /> ## **场景四:优先级任务** 方案: 在taskpool.execute的参数二种设置线程的优先级,优先级分三个级别:LOW、MEDIUM(默认)、HIGH。通过设置优先级来运行taskpool任务。 核心代码: ``` @Concurrent function ServiceHandle(pri: string): string {   hilog.info(0x0000, 'testTag', 'enter ServiceHandle:%{public}s', pri);   hilog.info(0x0000, 'testTag', 'end ServiceHandle:%{public}s', pri);   return pri; } export function CallPriorityHanel() {   let task1: taskpool.Task = new taskpool.Task(ServiceHandle, "LOW");   let task2: taskpool.Task = new taskpool.Task(ServiceHandle, "MEDIUM");   let task3: taskpool.Task = new taskpool.Task(ServiceHandle, "HIGH");   taskpool.execute(task1, taskpool.Priority.LOW).then((res: object) => {     hilog.info(0x0000, 'testTag', 'task return result :%{public}s', res);   });   taskpool.execute(task2, taskpool.Priority.MEDIUM).then((res: object) => {     hilog.info(0x0000, 'testTag', 'task return result :%{public}s', res);   });   taskpool.execute(task3, taskpool.Priority.HIGH).then((res: object) => {     hilog.info(0x0000, 'testTag', 'task return result :%{public}s', res);   }); } ``` 当前的设备都是多核的,并不是说将优先级设置程HIGH,该任务就会最先调度。 **运行结果:** <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/vdhpoXsJVLhBw5VZ.webp" alt="" width="100%" /> **场景五:taskpool****的Napi****调用** 方案:C++层编译的库,在ArkTS层通过import库的方式引用后,在taskpool的回调函数中调用接口。核心代码: ``` @Concurrent function ServiceHandle(pars: number): number {   hilog.info(0x0000, 'testTag', 'start ServiceHandle:%{public}d', pars);   // 业务处理过程,并将结果返回   testNapi.jsServiceHandle(88, 99);   return 0; } export function CallHandle() {   let task: taskpool.Task = new taskpool.Task(ServiceHandle, 1);   taskpool.execute(task,).then((res: object) => {     hilog.info(0x0000, 'testTag', 'printArgs result :%{public}d', res);   }); } typedef struct TestData {   int data;   int type; } TestData; static napi_value JsServiceHandle(napi_env env, napi_callback_info info) {   size_t argc = 2;   napi_value args[2] = {nullptr};   napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);   TestData testData;   napi_get_value_int32(env, args[0], &testData.data);   napi_get_value_int32(env, args[1], &testData.type);   OH_LOG_INFO(LOG_APP, "Native C++ Service handle:%{public}d,type:%{public}d", testData.data, testData.type);   return nullptr; } EXTERN_C_START static napi_value Init(napi_env env, napi_value exports) {   napi_property_descriptor desc[] = {   {"jsServiceHandle", nullptr, JsServiceHandle, nullptr, nullptr, nullptr, napi_default, nullptr} }; napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc); return exports; } EXTERN_C_END ``` **运行结果:** <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/QwLDkk5Y0LbuFqTw.webp" alt="" width="100%" />

【鸿蒙实战开发】基于Napi调用ArkTS/系统接口

**场景描述:** app应用在native侧调用 系统库/arkts模块的方法。 应用经常会遇到如下的业务诉求: 场景一:系统提供了ArkTS 接口,但未提供对应的NDK接口,当伙伴使用C++ 代码实现业务逻辑时,部分系统能力需要依赖系统ArkTS接口; 场景二: 系统仅提供了ArkTS 异步接口,未提供对应的NDK接口,当伙伴使用C++ 代码实现业务逻辑时,部分系统能力需要依赖系统ArkTS 异步接口; 场景三:伙伴在 TS 侧已定义接口,未实现对应的NDK接口,当伙伴使用C++ 代码实现业务逻辑时,想直接使用已有的TS 接口; **方案描述:** **场景一:**系统提供了ArkTS 接口,但未提供对应的NDK接口,当伙伴使用C++ 代码实现业务逻辑时,部分系统能力需要依赖系统ArkTS接口; 例如: 获取设备的屏幕宽高。 **方案:** 通过napi_load_module 的方式调用系统模块接口。 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/gg84tEklygBVxuK6.webp" alt="" width="100%" /> **核心代码** ``` static napi_value GetDisplaySize(napi_env env, napi_callback_info info) {   // 获取arkts侧的系统库路径   char path[64] = "@ohos.display";   size_t typeLen = 0;   napi_value string;   napi_create_string_utf8(env, path,  typeLen, &string);   // 加载系统库   napi_value sysModule;   napi_load_module(env, path, &sysModule);   // 获取系统库中的"getDefaultDisplaySync"方法   napi_value func = nullptr;   napi_get_named_property(env, sysModule, "getDefaultDisplaySync", &func);   napi_value funcResult;   napi_call_function(env, sysModule, func, 0, nullptr, &funcResult);   napi_value widthValue = nullptr;   napi_get_named_property(env, funcResult, "width", &widthValue);   double width;   napi_get_value_double(env, widthValue, &width);   OH_LOG_INFO( LOG_APP,  "width: %{public}f", width);   napi_value heightValue = nullptr;   napi_get_named_property(env, funcResult, "height", &heightValue);   double height;   napi_get_value_double(env, heightValue, &height);   OH_LOG_INFO(LOG_APP, "height: %{public}f", height);   // TODO: 业务拿到width 和 height,可以进一步处理具体业务逻辑   return nullptr; } ``` **运行结果:** <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/6KKd0M5VfSy6B0r3.webp" alt="" width="100%" /> **场景二:** 系统仅提供了ArkTS 异步接口,未提供对应的NDK接口,当伙伴使用C++ 代码实现业务逻辑时,部分系统能力需要依赖系统ArkTS 异步接口; 例如: 如何访问系统定义的异步ArkTS方法。 **方案** 通过创建线程安全函数的方式 调用系统的异步接口 <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/b8E6Hmx29eyi2HT0.webp" alt="" width="100%" /> 核心代码: 回调到JS层 ``` static void CallJs(napi_env env, napi_value jsCb, void *context, void *data) {   if (env == nullptr) {     return;   }   napi_value undefined = nullptr;   napi_value promise = nullptr;   char str[] = "wlan0";   napi_value sysModule;   napi_load_module(env, "@ohos.net.statistics", &sysModule);   napi_value param;   napi_create_string_utf8(env, str, NAPI_AUTO_LENGTH, &param);   napi_call_function(env, sysModule, jsCb, 1, &param, &promise);   napi_value thenFunc = nullptr;   if (napi_get_named_property(env, promise, "then", &thenFunc) != napi_ok) {     return;   }   napi_value resolvedCallback;   napi_value rejectedCallback;   napi_create_function(env, "resolvedCallback", NAPI_AUTO_LENGTH, ResolvedCallback, data, &resolvedCallback);   napi_create_function(env, "rejectedCallback", NAPI_AUTO_LENGTH, RejectedCallback, data, &rejectedCallback);   napi_value argv[2] = {resolvedCallback, rejectedCallback};   napi_call_function(env, promise, thenFunc, 2, argv, nullptr); } // 执行异步任务 static void ExecuteWork(napi_env env, void *data) {   CallbackData *callbackData = reinterpret_cast<CallbackData *>(data);   std::promise<double> promise;   auto future = promise.get_future();   // 调用线程安全函数   napi_call_threadsafe_function(callbackData->tsfn, &promise, napi_tsfn_nonblocking);   try {     auto result = future.get();     OH_LOG_INFO(LOG_APP, "getIfaceRxBytes Result from JS %{public}f", result);   } catch (const std::exception &e) {     // OH_LOG_INFO(LOG_APP, "XXX, Result from JS %{public}s", e.what());   } } // 异步任务完成回调 static void WorkComplete(napi_env env, napi_status status, void *data) {   CallbackData *callbackData = reinterpret_cast<CallbackData *>(data);   napi_release_threadsafe_function(callbackData->tsfn, napi_tsfn_release);   napi_delete_async_work(env, callbackData->work);   callbackData->tsfn = nullptr;   callbackData->work = nullptr; } static napi_value CallAsyncFunc(napi_env env, napi_callback_info info) {   size_t argc = 1;   napi_value jsCb = nullptr;   CallbackData *callbackData = nullptr;   napi_get_cb_info(env, info, &argc, &jsCb, nullptr, reinterpret_cast<void **>(&callbackData));   napi_value sysModule;   napi_load_module(env, "@ohos.net.statistics", &sysModule);   napi_value getIfaceRxBytesFunc ;   napi_get_named_property(env, sysModule, "getIfaceRxBytes", &getIfaceRxBytesFunc);   // 创建一个线程安全函数   napi_value resourceName = nullptr;   napi_create_string_utf8(env, "CallAsyncFunc", NAPI_AUTO_LENGTH, &resourceName);   napi_create_threadsafe_function(env, getIfaceRxBytesFunc, nullptr, resourceName, 0, 1, callbackData, nullptr, callbackData, CallJs,     &callbackData->tsfn);   // 创建一个异步任务   napi_create_async_work(env, nullptr, resourceName, ExecuteWork, WorkComplete, callbackData, &callbackData->work);   // 将异步任务加入到异步队列中   napi_queue_async_work(env, callbackData->work);   return nullptr; } ``` **运行结果** <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/cDCCv8T6eJFuV5TR.webp" alt="" width="100%" /> **场景三:** 伙伴在 ArkTS/TS 侧已定义接口,当伙伴使用C++ 代码实现业务逻辑时,想直接使用已有的TS 接口; 例如: 如何调用自定义的 ArkTS/TS方法; **方案** <img src="https://pic.code-nav.cn/post_picture/1834516631464710146/8TgxEoXr3jyoeDQ4.webp" alt="" width="100%" /> **核心代码** 步骤一: ObjectUtil.ts 导出相关接口 ``` namespace ObjectUtil {   export function isNull(obj) {     return obj === null;   }   export function isUndefined(obj) {     return obj === undefined;   }   export function isNullOrUndefined(obj) {     return isNull(obj) || isUndefined(obj);   }   export function toString(obj, defaultValue = '') {     if (this.isNullOrUndefined(obj)) {       return defaultValue;     }     else {       return obj.toString();     }   } } export default ObjectUtil; ``` 步骤二:必须在 build-profile.json5添加ets文件配置 ``` // 必须在build-profile.json5添加配置 "arkOptions": {   "runtimeOnly": {     "sources": [     './src/main/ets/common/ObjectUtil.ts',     ],     "packages": [     ]   } }, ``` 步骤三: 在native 获取ets 模块导出的变量 ``` /* * 获取某个TS/JS模块导出变量 * 入参: * path - 在工程文件夹下从ets开始的绝对路径名 * key - 待加载TS/JS模块导出变量的属性名 * 返回值:获取的TS/JS模块导出变量 */ static napi_value GetNativeModule(napi_env env, const char *modulePath, const char *key) {   napi_value module;   // 通过modulePath获取对应TS/JS模块的导出对象   napi_load_module(env, modulePath, &module);   napi_value outputObject = nullptr;   // 通过对象属性名key,从导出模块变量   napi_get_named_property(env, module, key, &outputObject );   return outputObject ; } ``` 步骤四:获取方法并调用demo示例(获取/ets/common/ObjectUtil 的isNull方法,并调用isNull方法判断一个对象是否为null) ``` /* * 调用系统库/ets/common/ObjectUtil模块导出方法 /ets/common/ObjectUtil 的isNull方法 * 返回值:标志是否加载成功的布尔值 */ static napi_status CallIsNullFun(napi_env env, napi_value inputObject) {   const char moduleName[] = "/ets/common/ObjectUtil";   const char funcName[] = "isNull";   napi_value isNullFun = GetNativeModule(env, moduleName, funcName);     napi_value inputArgs[1] = inputObject;   napi_value result;   // infoFun为"/ets/common/ObjectUtil"模块中获取的函数方法   // inputArgs为待执行方法的入参,result为出参   napi_call_function(env, undefined, isNullFun , 1, inputArgs, result);     return result ; } ```

下载 APP