首页 最新 热门 推荐

  • 首页
  • 最新
  • 热门
  • 推荐

HarmonyOS开发实战( Beta5版)ArrayBuffer序列化拷贝和转移最佳实践规范

  • 25-03-03 06:42
  • 2934
  • 6744
blog.csdn.net

场景示例

在应用开发中,会遇到需要进行图片处理的场景(比如需要调整一张图片的亮度、饱和度、大小等),为了避免阻塞主线程,可以将图片传递到子线程中执行这些操作。下面将分别通过拷贝和转移的方式,将图片传递到子线程中,并对比两种方式的性能数据。

使用拷贝方式传递

在ArkTS中,TaskPool传递ArrayBuffer数据时,默认使用转移的方式,通过调用task.setTransferList([])接口,可以切换成拷贝的方式。

首先,实现一个需要在Task中执行的用于调整饱和度的接口。

  1. // code/Performance/PerformanceLibrary/feature/ThreadDataTransfer/src/main/ets/utils/TreadUtil.ets
  2. @Concurrent
  3. function adjustImageValue(arrayBuffer: ArrayBuffer, lastAdjustData: number, currentAdjustData: number): ArrayBuffer {
  4. return execColorInfo(arrayBuffer, lastAdjustData, currentAdjustData);
  5. }

然后,通过拷贝的方式将图片数据传递到Task中,并在Task中将图片进行饱和度调整。

  1. // code/Performance/PerformanceLibrary/feature/ThreadDataTransfer/src/main/ets/utils/TreadUtil.ets
  2. // 创建Task,传入数据
  3. function createImageTask(arrayBuffer: ArrayBuffer, lastAdjustData: number, currentAdjustData: number, isParamsByTransfer: boolean): taskpool.Task {
  4. let task: taskpool.Task = new taskpool.Task(adjustImageValue, arrayBuffer, lastAdjustData, currentAdjustData);
  5. if (!isParamsByTransfer) { // 是否使用转移方式
  6. task.setTransferList([]);
  7. }
  8. return task;
  9. }
  10. // ...
  11. // 创建taskNum个Task
  12. for (let i: number = 0; i < taskNum; i++) {
  13. let arrayBufferSlice: ArrayBuffer = arrayBuffer.slice(arrayBuffer.byteLength / taskNum * i, arrayBuffer.byteLength / taskNum * (i + 1));
  14. // 使用拷贝方式传入ArrayBuffer,所以isParamsByTransfer是false
  15. taskPoolGroup.addTask(createImageTask(arrayBufferSlice, lastAdjustData, currentAdjustData, isParamsByTransfer));
  16. }
  17. let start: number = new Date().getTime();
  18. // 执行Task
  19. taskpool.execute(taskPoolGroup).then((data: ArrayBuffer[]) => {
  20. if (callback !== undefined) {
  21. let end : number = new Date().getTime();
  22. AppStorage.set<String>('timeCost', util.format('%s s', ((end - start) / 60).toFixed(2).toString()));
  23. callback(concatenateArrayBuffers(data));
  24. }
  25. }).catch((e: BusinessError) => {
  26. Logger.error(e.message);
  27. })
  28. // ...

最后,主线程接收到Task执行完毕后返回的ArrayBuffer数据,转换为PixelMap后在Image组件上显示。

  1. // code/Performance/PerformanceLibrary/feature/ThreadDataTransfer/src/main/ets/view/AdjustImageView.ets
  2. // ...
  3. // 将处理后的ArrayBuffer转换为PixelMap,并在Image组件上显示
  4. pixelMapProcessByTaskPool(this.pixelMap, this.lastAdjustData, this.currentAdjustData, this.currentTaskNum,
  5. this.isParamsByTransfer, (data: ArrayBuffer) => {
  6. if (this.pixelMap !== undefined) {
  7. const newPixel: image.PixelMap = this.pixelMap;
  8. newPixel.writeBufferToPixels(data).then(() => {
  9. this.pixelMap = newPixel;
  10. this.lastAdjustData = Math.round(value);
  11. this.isPixelMapChanged = !this.isPixelMapChanged;
  12. this.deviceListDialogController.close();
  13. this.postState = true;
  14. });
  15. }
  16. });
  17. // ...

编译运行后,通过脚本工具抓取Trace并在SmartPerf Host中查看,如图1所示。其中,All Heap表示应用占用的内存,BeforePassParameter表示ArrayBuffer开始从主线程传递到子线程,AfterPassParameter表示子线程收到完整的ArrayBuffer数据。

图1 拷贝方式Trace泳道图

image-20240702184652708

图1中可以看到,ArrayBuffer传递到TaskPool时,内存有2.5M的上升,耗时是19ms。

使用转移方式传递

在TaskPool中,传递ArrayBuffer数据,是默认使用转移方式的,所以在上面示例的基础上,去除task.setTransferList([])接口就可以实现。

  1. // code/Performance/PerformanceLibrary/feature/ThreadDataTransfer/src/main/ets/utils/TreadUtil.ets
  2. // 创建Task,传入数据
  3. function createImageTask(arrayBuffer: ArrayBuffer, lastAdjustData: number, currentAdjustData: number, isParamsByTransfer: boolean): taskpool.Task {
  4. let task: taskpool.Task = new taskpool.Task(adjustImageValue, arrayBuffer, lastAdjustData, currentAdjustData);
  5. if (!isParamsByTransfer) { // 是否使用转移方式
  6. task.setTransferList([]);
  7. }
  8. return task;
  9. }
  10. // ...
  11. // 创建taskNum个Task
  12. for (let i: number = 0; i < taskNum; i++) {
  13. let arrayBufferSlice: ArrayBuffer = arrayBuffer.slice(arrayBuffer.byteLength / taskNum * i, arrayBuffer.byteLength / taskNum * (i + 1));
  14. // 使用转移方式传入ArrayBuffer,所以isParamsByTransfer是true
  15. taskPoolGroup.addTask(createImageTask(arrayBufferSlice, lastAdjustData, currentAdjustData, isParamsByTransfer));
  16. }
  17. let start: number = new Date().getTime();
  18. // 执行Task
  19. taskpool.execute(taskPoolGroup).then((data: ArrayBuffer[]) => {
  20. if (callback !== undefined) {
  21. let end : number = new Date().getTime();
  22. AppStorage.set<String>('timeCost', util.format('%s s', ((end - start) / 60).toFixed(2).toString()));
  23. callback(concatenateArrayBuffers(data));
  24. }
  25. }).catch((e: BusinessError) => {
  26. Logger.error(e.message);
  27. })
  28. // ...

编译运行后,通过脚本工具抓取Trace并在SmartPerf Host中查看,如图2所示。

图2 转移方式Trace泳道图

image-20240726120720219

在图2中可以看到,ArrayBuffer传递到TaskPool时,内存并没有明显的变化,耗时只有5.2ms。

性能对比

通过对比上面两个场景中的Trace数据可以发现,相对于拷贝,转移的内存占用明显变少,耗时也减少了72%。

使用拷贝方式传递数据时,会将ArrayBuffer对象拷贝一次。不仅会多占用了一部分内存,还会消耗一定的时间进行拷贝对象的序列化操作。而通过转移的方式传递数据时,并不需要将传递的对象拷贝一次,而是通过地址转移进行序列化,将ArrayBuffer的内存资源从原始的缓冲区分离出来,附加到子线程TaskPool创建的缓冲区对象中,也就是将ArrayBuffer的所有权从主线程移交给了子线程。

所以,使用转移方式,可以减少线程间传递数据时的内存占用和CPU耗时。

使用建议

在ArkTS的多线程中传递数据时,应保证线程间的通信数据量尽可能的小,输入输出都要简单,避免传输影响耗时;而且并发任务要相对独立,不要频繁跨线程交互。上一部分的例子中,虽然将图片处理的操作放在了TaskPool子线程中,但是由于图片数据较大,处理时间还是较长,如图3所示。其中,BeforePassParameter表示ArrayBuffer开始从主线程传递到子线程,AfterPassParameter表示子线程收到完整的ArrayBuffer数据,AfterImageDarken表示计算(图片饱和度调节)结束。在图中可以看到,有5.8s的耗时,虽然在这段时间内并不会阻塞主线程中的操作,但是对于用户来说,等待时间依然较长。

图3 子线程计算耗时泳道图

img

下面将通过示例代码说明如何进一步优化图片处理的时间。详细代码请参考ThreadDataTransfer。

  1. // code/Performance/PerformanceLibrary/feature/ThreadDataTransfer/src/main/ets/utils/TreadUtil.ets
  2. // ...
  3. // 根据传入的taskNum,创建对应数量的Task
  4. export async function pixelMapProcessByTaskPool(pixelMap: image.PixelMap, lastAdjustData: number, currentAdjustData: number, taskNum: number, isParamsByTransfer: boolean, callback?: Callback<ArrayBuffer>): Promise<void> {
  5. let arrayBuffer: ArrayBuffer = await convertPixelMapToArrayBuffer(pixelMap);
  6. let taskPoolGroup: taskpool.TaskGroup = new taskpool.TaskGroup();
  7. for (let i: number = 0; i < taskNum; i++) {
  8. let arrayBufferSlice: ArrayBuffer = arrayBuffer.slice(arrayBuffer.byteLength / taskNum * i, arrayBuffer.byteLength / taskNum * (i + 1));
  9. taskPoolGroup.addTask(createImageTask(arrayBufferSlice, lastAdjustData, currentAdjustData, isParamsByTransfer));
  10. }
  11. let start: number = new Date().getTime();
  12. taskpool.execute(taskPoolGroup).then((data: ArrayBuffer[]) => {
  13. if (callback !== undefined) {
  14. let end : number = new Date().getTime();
  15. AppStorage.set<String>('timeCost', util.format('%s s', ((end - start) / 60).toFixed(2).toString()));
  16. // 将Task处理完成的数据合并在一个ArrayBuffer中
  17. callback(concatenateArrayBuffers(data));
  18. }
  19. }).catch((e: BusinessError) => {
  20. Logger.error(e.message);
  21. })
  22. }

在这段代码中,将图片转换为ArrayBuffer后,并没有直接传递到子线程中,而是先进行了切片处理,分成taskNum个独立的ArrayBuffer。之后分别传入到taskNum个Task中,并通过TaskGroup统一管理。在图片饱和度调节完成后,再将taskNum个处理结果拼接成一个ArrayBuffer,并转换为PixelMap在Image组件上显示。此示例代码也是使用了转移方式进行传递,因为上一部分的示例已经说明了转移比拷贝的方式更加高效,所以这里不再进行重复的对比。编译运行代码后,通过脚本工具抓取Trace并在SmartPerf Host中查看,如图4所示。

图4 图片切成3份处理后的泳道图

image-20240702185135759

通过图4可以看到,将原ArrayBuffer切成3段,分别放在3个子线程中处理,图片饱和度调节的操作耗时只有3.4s,计算时间减少了44%。因为有3个线程在同时进行操作,大大减少了计算的耗时。但是,这并不代表可以随意增加Task的数量。由于在TaskPool中,并不是创建了多少个Task,就会有多少个Task在同时执行。随着Task的增多,同时执行的线程也会越多,并发执行的任务就会增多,CPU的占用率就会升高,从而影响到其他需要CPU计算的任务。虽然在硬件配置高的设备上表现不明显,但是对于低配置的设备,影响会较大。下面的表格,列出了示例中不同数量Task时的CPU耗时供开发者参考。

任务数实际子线程数序列化时间(ms)计算时间(s)
113.307923.967
222.623452.646
336.112581.875
437.252952.579
532.70322.266
633.2131.970
735.2602.096
832.7042.067
933.9862.116
1053.8111.844
1552.8792.079
2053.5262.015

因此,在分段传输大数据时,需要合理调整子线程的数量和传入子线程的数据量,加快子线程处理速度,同时减少对其他功能的影响,提升用户的应用体验。

最后

小编在之前的鸿蒙系统扫盲中,有很多朋友给我留言,不同的角度的问了一些问题,我明显感觉到一点,那就是许多人参与鸿蒙开发,但是又不知道从哪里下手,因为资料太多,太杂,教授的人也多,无从选择。有很多小伙伴不知道学习哪些鸿蒙开发技术?不知道需要重点掌握哪些鸿蒙应用开发知识点?而且学习时频繁踩坑,最终浪费大量时间。所以有一份实用的鸿蒙(HarmonyOS NEXT)文档用来跟着学习是非常有必要的。 

为了确保高效学习,建议规划清晰的学习路线,涵盖以下关键阶段:


鸿蒙(HarmonyOS NEXT)最新学习路线

​

该路线图包含基础技能、就业必备技能、多媒体技术、六大电商APP、进阶高级技能、实战就业级设备开发,不仅补充了华为官网未涉及的解决方案

路线图适合人群:

IT开发人员:想要拓展职业边界
零基础小白:鸿蒙爱好者,希望从0到1学习,增加一项技能。
技术提升/进阶跳槽:发展瓶颈期,提升职场竞争力,快速掌握鸿蒙技术

2.视频学习教程+学习PDF文档

HarmonyOS Next 最新全套视频教程

  纯血版鸿蒙全套学习文档(面试、文档、全套视频等)       

​​

总结

参与鸿蒙开发,你要先认清适合你的方向,如果是想从事鸿蒙应用开发方向的话,可以参考本文的学习路径,简单来说就是:为了确保高效学习,建议规划清晰的学习路线

鸿蒙NEXT全套学习资料
微信名片
注:本文转载自blog.csdn.net的让开,我要吃人了的文章"https://blog.csdn.net/weixin_55362248/article/details/141822390"。版权归原作者所有,此博客不拥有其著作权,亦不承担相应法律责任。如有侵权,请联系我们删除。
复制链接
复制链接
相关推荐
发表评论
登录后才能发表评论和回复 注册

/ 登录

评论记录:

未查询到任何数据!
回复评论:

分类栏目

后端 (14832) 前端 (14280) 移动开发 (3760) 编程语言 (3851) Java (3904) Python (3298) 人工智能 (10119) AIGC (2810) 大数据 (3499) 数据库 (3945) 数据结构与算法 (3757) 音视频 (2669) 云原生 (3145) 云平台 (2965) 前沿技术 (2993) 开源 (2160) 小程序 (2860) 运维 (2533) 服务器 (2698) 操作系统 (2325) 硬件开发 (2492) 嵌入式 (2955) 微软技术 (2769) 软件工程 (2056) 测试 (2865) 网络空间安全 (2948) 网络与通信 (2797) 用户体验设计 (2592) 学习和成长 (2593) 搜索 (2744) 开发工具 (7108) 游戏 (2829) HarmonyOS (2935) 区块链 (2782) 数学 (3112) 3C硬件 (2759) 资讯 (2909) Android (4709) iOS (1850) 代码人生 (3043) 阅读 (2841)

热门文章

101
推荐
关于我们 隐私政策 免责声明 联系我们
Copyright © 2020-2024 蚁人论坛 (iYenn.com) All Rights Reserved.
Scroll to Top