type
status
date
slug
summary
tags
category
icon
password
能力模型
能力分类
省赛
全球总决赛
学习进度
中国总决赛
ArkTS是什么?
ArkTS的特性
- 声明式UI
- 状态变量与界面同步刷新(状态管理和渲染控制)
ArkTS是HarmonyOS优选的主力应用开发语言。ArkTS围绕应用开发在TypeScript(简称TS)生态基础上做了进一步扩展,继承了TS的所有特性,是TS的超集。因此,在学习ArkTS语言之前,建议开发者具备TS语言开发能力。
当前,ArkTS在TS的基础上主要扩展了如下能力:
- 基本语法:ArkTS定义了声明式UI描述、自定义组件和动态扩展UI元素的能力,再配合ArkUI开发框架中的系统组件及其相关的事件方法、属性方法等共同构成了UI开发的主体。
- 状态管理:ArkTS提供了多维度的状态管理机制。在UI开发框架中,与UI相关联的数据可以在组件内使用,也可以在不同组件层级间传递,比如父子组件之间、爷孙组件之间,还可以在应用全局范围内传递或跨设备传递。另外,从数据的传递形式来看,可分为只读的单向传递和可变更的双向传递。开发者可以灵活地利用这些能力来实现数据和UI的联动。
- 渲染控制:ArkTS提供了渲染控制的能力。条件渲染可根据应用的不同状态,渲染对应状态下的UI内容。循环渲染可从数据源中迭代获取数据,并在每次迭代过程中创建相应的组件。数据懒加载从数据源中按需迭代数据,并在每次迭代过程中创建相应的组件。
并发
并发概述
并发是指在同一时间段内,能够处理多个任务的能力。为了提升应用的响应速度与帧率,以及防止耗时任务队主线程的干扰,OpenHarmony系统提供了异步并发和多线程并发两种处理策略。
两种并发处理策略
- 异步并发:Promise和async/await
异步并发是指异步代码在执行到一定程度后会被暂停,以便在未来某个时间点继续执行,这种情况下,同一时间只有一段代码在执行。
- 多线程并发:TaskPool和Worker
多线程并发允许在同一时间段内同时执行多段代码。在主线程继续响应用户操作和更新UI的同时,后台也能执行耗时操作,从而避免应用出现卡顿。
- 并发的应用场景
- 单次I/O任务——异步
- CPU密集型任务——多线程
- I/O密集型任务——多线程
- 同步任务——多线程
使用异步并发能力进行开发
Promise和async/await提供异步并发能力,是标准的JS异步语法。异步代码会被挂起并在之后继续执行,同一时间只有一段代码执行,适用于单次I/O任务的场景开发,例如一次网络请求、一次文件读写等操作。
Promise
Promise是一种用于处理异步操作的对象,可以将异步操作转换为类似于同步操作的风格,以方便代码编写和维护。Promise提供了一个状态机制来管理异步操作的不同阶段,并提供了一些方法来注册回调函数以处理异步操作的成功或失败的结果。
Promise的三种状态
- 对象创建后
- pending进行中
- 异步操作完成后
- fufilled已完成
- rejected已拒绝
Promise的基本用法(2种)
最基本的用法是通过构造函数实例化一个Promise对象,同时传入一个带有两个参数的函数,通常称为executor函数。executor函数接收两个参数:resolve和reject,分别表示异步操作成功和失败时的回调函数。例如,以下代码创建了一个Promise对象并模拟了一个异步操作:
上述代码中,setTimeout函数模拟了一个异步操作,并在1秒钟后随机生成一个数字。如果随机数大于0.5,则执行resolve回调函数并将随机数作为参数传递;否则执行reject回调函数并传递一个错误对象作为参数。
Promise对象创建后,可以使用then方法和catch方法指定fulfilled状态和rejected状态的回调函数。then方法可接受两个参数,一个处理fulfilled状态的函数,另一个处理rejected状态的函数。只传一个参数则表示当Promise对象状态变为fulfilled时,then方法会自动调用这个回调函数,并将Promise对象的结果作为参数传递给它。使用catch方法注册一个回调函数,用于处理“失败”的结果,即捕获Promise的状态改变为rejected状态或操作失败抛出的异常。例如:
上述代码中,then方法的回调函数接收Promise对象的成功结果作为参数,并将其输出到控制台上。如果Promise对象进入rejected状态,则catch方法的回调函数接收错误对象作为参数,并将其输出到控制台上。
async/await
async/await是一种用于处理异步操作的Promise语法糖???,使得编写异步代码变得更加简单和易读。通过使用async关键字声明一个函数为异步函数,并使用await关键字等待Promise的解析(完成或拒绝),以同步的方式编写异步操作的代码。
async函数是一个返回Promise对象的函数,用于表示一个异步操作。在async函数内部,可以使用await关键字等待一个Promise对象的解析,并返回其解析值。如果一个async函数抛出异常,那么该函数返回的Promise对象将被拒绝,并且异常信息会被传递给Promise对象的onRejected()方法。
下面是一个使用async/await的例子,其中模拟了一个异步操作,该操作会在3秒钟后返回一个字符串。
在上述示例代码中,使用了await关键字来等待Promise对象的解析,并将其解析值存储在result变量中。
需要注意的是,由于要等待异步操作完成,因此需要将整个操作包在async函数中。除了在async函数中使用await外,还可以使用try/catch块来捕获异步操作中的异常。
单次I/O任务开发指导
Promise和async/await提供异步并发能力,适用于单次I/O任务的场景开发,本文以使用异步进行单次文件写入为例来提供指导。
1.实现单次I/O任务逻辑
2.采用异步能力调用单次I/O任务。
使用多线程并发能力进行开发
多线程并发概述
(1)常见的并发模型
- 基于内存共享的并发模型
- 基于消息通信的并发模型:Actor并发模型
Actor并发模型作为基于消息通信并发模型的典型代表,不需要开发者去面对锁带来的一系列复杂偶发的问题,同时并发度也相对较高,因此得到了广泛的支持和使用,也是当前ArkTS语言选择的并发模型。由于Actor模型的内存隔离特性,所以需要进行跨线程的数据序列化传输。
(2)数据传输对象
- 普通对象
普通对象传输采用标准的结构化克隆算法(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绑定对象(Native Binding Object)是系统所提供的对象,该对象与底层系统功能进行绑定,提供直接访问底层系统功能的能力。
当前支持序列化传输的Native绑定对象主要包含:Context和RemoteObject。
Context对象包含应用程序组件的上下文信息,它提供了一种访问系统服务和资源的方式,使得应用程序组件可以与系统进行交互。获取Context信息的方法可以参考获取上下文信息。
RemoteObject对象的主要作用是实现远程通信的功能,它允许在不同的进程间传递对象的引用,使得不同进程之间可以共享对象的状态和方法,服务提供者必须继承此类,RemoteObject对象的创建可以参考RemoteObject的实现。
TaskPool(任务池)和Worker的作用是为应用程序提供一个多线程的运行环境,用于处理耗时的计算任务或其他密集型任务。可以有效地避免这些任务阻塞主线程,从而最大化系统的利用率,降低整体资源消耗,并提高系统的整体性能。
TaskPool和Worker的对比
(1)实现特点对比
实现 | TaskPool | Worker |
内存模型 | 线程间隔离,内存不共享。 | 线程间隔离,内存不共享。 |
参数传递机制 | 采用标准的结构化克隆算法(Structured Clone)进行序列化、反序列化,完成参数传递。支持ArrayBuffer转移和SharedArrayBuffer共享。 | 采用标准的结构化克隆算法(Structured Clone)进行序列化、反序列化,完成参数传递。支持ArrayBuffer转移和SharedArrayBuffer共享。 |
参数传递 | 直接传递,无需封装,默认进行transfer。 | 消息对象唯一参数,需要自己封装。 |
方法调用 | 直接将方法传入调用。 | 在Worker线程中进行消息解析并调用对应方法。 |
返回值 | 异步调用后默认返回。 | 主动发送消息,需在onmessage解析赋值。 |
生命周期 | TaskPool自行管理生命周期,无需关心任务负载高低。 | 开发者自行管理Worker的数量及生命周期。 |
任务池个数上限 | 自动管理,无需配置。 | 同个进程下,最多支持同时开启8个Worker线程。 |
任务执行时长上限 | 无限制。 | 无限制。 |
设置任务的优先级 | 不支持。 | 不支持。 |
执行任务的取消 | 支持取消任务队列中等待的任务。 | 不支持。 |
(2)适用场景对比
- TaskPool偏向独立任务维度,该任务在线程中执行,无需关注线程的生命周期
- 需要频繁取消的任务。例如图库大图浏览场景,为提升体验,会同时缓存当前图片左右侧各2张图片,往一侧滑动跳到下一张图片时,要取消另一侧的一个缓存任务,需要使用TaskPool。
- 而Worker偏向线程的维度,支持长时间占据线程执行,需要主动管理线程生命周期。
- 有关联的一系列同步任务。例如在一些需要创建、使用句柄的场景中,句柄创建每次都是不同的,该句柄需永久保存,保证使用该句柄进行操作,需要使用Worker。
- 大量或者调度点较分散的任务。例如大型应用的多个模块包含多个耗时任务,不方便使用8个Worker去做负载管理,推荐采用TaskPool。
(3)运作机制
- TaskPool运作机制
TaskPool支持开发者在主线程封装任务抛给任务队列,系统选择合适的工作线程,进行任务的分发及执行,再将结果返回给主线程。接口直观易用,支持任务的执行、取消。工作线程数量上限为4。
- Worker运作机制
创建Worker的线程称为宿主线程(不一定是主线程,工作线程也支持创建Worker子线程),Worker自身的线程称为Worker子线程(或Actor线程、工作线程)。每个Worker子线程与宿主线程拥有独立的实例,包含基础设施、对象、代码段等。Worker子线程和宿主线程之间的通信是基于消息传递的,Worker通过序列化机制与宿主线程之间相互通信,完成命令及数据交互。
(4)注意事项
TaskPool注意事项
- 实现任务的函数需要使用装饰器@Concurrent标注,且仅支持在.ets文件中使用。
- 实现任务的函数入参需满足序列化支持的类型,详情请参见数据传输对象。
- 由于不同线程中上下文对象是不同的,因此TaskPool工作线程只能使用线程安全的库,例如UI相关的非线程安全库不能使用。
- 序列化传输的数据量大小限制为16MB。
Worker注意事项
- 创建Worker时,传入的Worker.ts路径在不同版本有不同的规则,详情请参见文件路径注意事项。
- Worker创建后需要手动管理生命周期,且最多同时运行的Worker子线程数量为8个,详情请参见生命周期注意事项。
- 创建Worker不支持使用其他Module的Worker.ts文件,即不支持跨模块调用Worker。
- 由于不同线程中上下文对象是不同的,因此Worker线程只能使用线程安全的库,例如UI相关的非线程安全库不能使用。
- 序列化传输的数据量大小限制为16MB。
- Worker的创建和销毁耗费性能,建议开发者合理管理已创建的Worker并重复使用。Worker空闲时也会一直运行,因此当不需要Worker时,可以调用terminate()接口或parentPort.close()方法主动销毁Worker。若Worker处于已销毁或正在销毁等非运行状态时,调用其功能接口,会抛出相应的错误。
(5)@Concurrent装饰器:校验并发函数——TaskPool
在使用TaskPool时,执行的并发函数需要使用该装饰器修饰,否则无法通过相关校验。
装饰器说明
@Concurrent并发装饰器 | 说明 |
装饰器参数 | 无。 |
使用场景 | 仅支持在Stage模型的工程中使用。 |
装饰的函数类型 | 允许标注async函数或普通函数。禁止标注generator、箭头函数、method。不支持类成员函数或者匿名函数。 |
装饰的函数内的变量类型 | 允许使用local变量、入参和通过import引入的变量。禁止使用闭包变量。 |
装饰器使用示例
CPU密集型任务开发指导
CPU密集型任务特点
CPU密集型任务是指需要占用系统资源处理大量计算能力的任务,需要长时间运行,这段时间会阻塞线程其它事件的处理,不适宜放在主线程进行。例如图像处理、视频编码、数据分析等。
基于多线程并发机制处理CPU密集型任务可以提高CPU利用率,提升应用程序响应速度。
CPU任务并发选择
- 一系列同步任务:Worker
- 大量或调度点分散的独立任务:TaskPool
接下来将以图像直方图处理以及后台长时间的模型预测任务分别进行举例。
使用TaskPool进行图像直方图处理
- 实现图像处理的业务逻辑。
- 数据分段,将各段数据通过不同任务的执行完成图像处理。
- 结果数组汇总处理。
使用Worker进行长时间数据分析
本文通过某地区提供的房价数据训练一个简易的房价预测模型,该模型支持通过输入房屋面积和房间数量去预测该区域的房价,模型需要长时间运行,房价预测需要使用前面的模型运行结果,因此需要使用Worker。
- DevEco Studio提供了Worker创建的模板,新建一个Worker线程,例如命名为“MyWorker”。
- 在主线程中通过调用ThreadWorker的constructor()方法创建Worker对象,当前线程为宿主线程。
- 在宿主线程中通过调用onmessage()方法接收Worker线程发送过来的消息,并通过调用postMessage()方法向Worker线程发送消息。
例如向Worker线程发送训练和预测的消息,同时接收Worker线程发送回来的消息。
- 在MyWorker.ts文件中绑定Worker对象,当前线程为Worker线程。
- 在Worker线程中通过调用onmessage()方法接收宿主线程发送的消息内容,并通过调用postMessage()方法向宿主线程发送消息。
例如在Worker线程中定义预测模型及其训练过程,同时与主线程进行信息交互。
- 在Worker线程中完成任务之后,执行Worker线程销毁操作。销毁线程的方式主要有两种:根据需要可以在宿主线程中对Worker线程进行销毁;也可以在Worker线程中主动销毁Worker线程。
在宿主线程中通过调用onexit()方法定义Worker线程销毁后的处理逻辑。
方式一:在宿主线程中通过调用terminate()方法销毁Worker线程,并终止Worker接收消息。
方式二:在Worker线程中通过调用close()方法主动销毁Worker线程,并终止Worker接收消息。
I/O密集型任务开发指导
I/O密集型任务特点
I/O密集型任务的性能重点通常不在于CPU的处理能力,而在于I/O操作的速度和效率。这种任务通常需要频繁地进行磁盘读写、网络通信等操作。此处以频繁读写系统文件来模拟I/O密集型并发任务的处理。
I/O任务并发选择
- 单次I/O任务:异步并发
- I/O密集型任务:多线程并发——TaskPool
使用示例
(1)定义并发函数,内部密集调用I/O能力。
(2)使用TaskPool执行包含密集I/O的并发函数:通过调用execute()方法执行任务,并在回调中进行调度结果处理。示例中的filePath1和filePath2的获取方式请参见获取应用文件路径。
同步任务开发指导
同步任务特点
同步任务是指在多个线程之间协调执行的任务,其目的是确保多个任务按照一定的顺序和规则执行,例如使用锁来防止数据竞争。
同步任务的实现需要考虑多个线程之间的协作和同步,以确保数据的正确性和程序的正确执行。
同步并发选择
- 相对独立的同步任务:TaskPool
- 有关联性的同步任务:Worker
使用TaskPool处理同步任务
当调度独立的同步任务,或者一系列同步任务为静态方法实现,或者可以通过单例构造唯一的句柄或类对象,可在不同任务池之间使用时,推荐使用TaskPool。
- 定义并发函数,内部调用同步方法。
模拟一个包含同步调用的单实例类。
业务使用TaskPool调用相关同步方法的代码。
使用Worker处理关联的同步任务
当一系列同步任务需要使用同一个句柄调度,或者需要依赖某个类对象调度,无法在不同任务池之间共享时,需要使用Worker。
1.在主线程中创建Worker对象,同时接收Worker线程发送回来的消息。
2.在Worker线程中绑定Worker对象,同时处理同步任务逻辑。
容器类库
容器类库概述
容器类库,用于存储各种数据类型的元素,并具备一系列处理数据元素的方法,作为纯数据结构容器来使用具有一定的优势。
容器类采用了类似静态语言的方式来实现,并通过对存储位置以及属性的限制,让每种类型的数据都能在完成自身功能的基础上去除冗余逻辑,保证了数据的高效访问,提升了应用的性能。
当前提供了线性和非线性两类容器,共14种。每种容器都有自身的特性及使用场景。
线性容器
线性容器实现能按顺序访问的数据结构,其底层主要通过数组实现,包括ArrayList、Vector、List、LinkedList、Deque、Queue、Stack七种。
线性容器,充分考虑了数据访问的速度,运行时(Runtime)通过一条字节码指令就可以完成增、删、改、查等操作。
(1)ArrayList
ArrayList即动态数组,可用来构造全局的数组对象。 当需要频繁读取集合中的元素时,推荐使用ArrayList。
ArrayList依据泛型定义,要求存储位置是一片连续的内存空间,初始容量大小为10,并支持动态扩容,每次扩容大小为原始容量的1.5倍。
ArrayList进行增、删、改、查操作的常用API如下:
操作 | 描述 |
增加元素 | 通过add(element: T)函数每次在数组尾部增加一个元素。 |
ㅤ | 通过insert(element: T, index: number)在指定位置插入一个元素。 |
访问元素 | 通过arr[index]获取指定index对应的value值,通过指令获取保证访问速度。 |
ㅤ | 通过forEach(callbackFn: (value: T, index?: number, arrlist?: ArrayList<T>) => void, thisArg?: Object): void访问整个ArrayList容器的元素。 |
ㅤ | 通过[Symbol.iterator]():IterableIterator<T>迭代器进行数据访问。 |
修改元素 | 通过arr[index] = xxx修改指定index位置对应的value值。 |
删除元素 | 通过remove(element: T)删除第一个匹配到的元素。 |
ㅤ | 通过removeByRange(fromIndex: number, toIndex:number)删除指定范围内的元素。 |
(2)Vector
API version 9开始,该接口不再维护,推荐使用ArrayList。
Vector是指连续存储结构,可用来构造全局的数组对象。Vector依据泛型定义,要求存储位置是一片连续的内存空间,初始容量大小为10,并支持动态扩容,每次扩容大小为原始容量的2倍。
Vector和ArrayList相似,都是基于数组实现,但Vector提供了更多操作数组的接口。Vector在支持操作符访问的基础上,还增加了get/set接口,提供更为完善的校验及容错机制,满足用户不同场景下的需求。
Vector进行增、删、改、查操作的常用API如下:
操作 | 描述 |
增加元素 | 通过add(element: T)函数每次在数组尾部增加一个元素。 |
ㅤ | 通过insert(element: T, index: number)在指定位置插入一个元素。 |
访问元素 | 通过vec[index]获取指定index对应的value值,通过指令获取保证访问速度。 |
ㅤ | 通过get(index: number)获取指定index位置对应的元素。 |
ㅤ | 通过getLastElement()获取最后一个元素。 |
ㅤ | 通过getIndexOf(element:T)获取第一个匹配到元素的位置。 |
ㅤ | 通过getLastIndexOf(element:T)获取最后一个匹配到元素的位置。 |
ㅤ | 通过forEach(callbackFn: (value: T, index?: number, Vector?: Vector<T>) => void, thisArg?: Object)访问整个Vector的元素。 |
ㅤ | 通过[Symbol.iterator]():IterableIterator<T>迭代器进行数据访问。 |
修改元素 | 通过vec[index]=xxx修改指定index位置对应的value值。 |
ㅤ | 通过set(index:number,element:T)修改指定index位置的元素值为element。 |
ㅤ | 通过setLength(newSize:number)设置Vector的长度大小。 |
删除元素 | 通过removeByIndex(index:number)删除index位置对应的value值。 |
ㅤ | 通过remove(element:T)删除第一个匹配到的元素。 |
ㅤ | 通过removeByRange(fromIndex:number,toIndex:number)删除指定范围内的元素。 |
(3)List
List可用来构造一个单向链表对象,即只能通过头节点开始访问到尾节点。List依据泛型定义,在内存中的存储位置可以是不连续的。
- List和LinkedList相比,LinkedList是双向链表,可以快速地在头尾进行增删,而List是单向链表,无法双向操作。
- 当需要频繁的插入删除时,推荐使用List高效操作。
可以通过get/set等接口对存储的元素进行修改,List进行增、删、改、查操作的常用API如下:
操作 | 描述 |
增加元素 | 通过add(element: T)函数每次在数组尾部增加一个元素。 |
ㅤ | 通过insert(element: T, index: number)在指定位置插入一个元素。 |
访问元素 | 通过list[index]获取指定index对应的value值,通过指令获取保证访问速度。 |
ㅤ | 通过get(index: number)获取指定index位置对应的元素。 |
ㅤ | 通过getFirst()获取第一个元素。 |
ㅤ | 通过getLast()获取最后一个元素。 |
ㅤ | 通过getIndexOf(element: T)获取第一个匹配到元素的位置。 |
ㅤ | 通过getLastIndexOf(element: T)获取最后一个匹配到元素的位置。 |
ㅤ | 通过forEach(callbackfn: (value:T, index?: number, list?: List<T>)=> void,thisArg?: Object)访问整个List的元素。 |
ㅤ | 通过[Symbol.iterator]():IterableIterator<T>迭代器进行数据访问。 |
修改元素 | 通过list[index] = xxx修改指定index位置对应的value值。 |
ㅤ | 通过set(index:number, element: T)修改指定index位置的元素值为element。 |
ㅤ | 通过replaceAllElements(callbackFn:(value: T,index?: number,list?: List<T>)=>T,thisArg?: Object)对List内元素进行替换操作。 |
删除元素 | 通过removeByIndex(index:number)删除index位置对应的value值。 |
ㅤ | 通过remove(element:T)删除第一个匹配到的元素。 |
(4)LinkedList
LinkedList可用来构造一个双向链表对象,可以在某一节点向前或者向后遍历List。LinkedList依据泛型定义,在内存中的存储位置可以是不连续的。
- LinkedList和List相比,LinkedList是双向链表,可以快速地在头尾进行增删,而List是单向链表,无法双向操作。
- LinkedList和ArrayList相比,插入数据效率LinkedList优于ArrayList,而查询效率ArrayList优于LinkedList。
- 当需要频繁的插入删除时,推荐使用LinkedList高效操作。
可以通过get/set等接口对存储的元素进行修改,LinkedList进行增、删、改、查操作的常用API如下:
操作 | 描述 |
增加元素 | 通过add(element: T)函数每次在数组尾部增加一个元素。 |
ㅤ | 通过insert(index: number, element: T)在指定位置插入一个元素。 |
访问元素 | 通过list[index]获取指定index对应的value值,通过指令获取保证访问速度。 |
ㅤ | 通过get(index: number)获取指定index位置对应的元素。 |
ㅤ | 通过getFirst()获取第一个元素。 |
ㅤ | 通过getLast()获取最后一个元素。 |
ㅤ | 通过getIndexOf(element: T)获取第一个匹配到元素的位置。 |
ㅤ | 通过getLastIndexOf(element: T)获取最后一个匹配到元素的位置。 |
ㅤ | 通过forEach(callbackFn: (value: T, index?: number, list?: LinkedList<T>) => void, thisArg?: Object)访问整个LinkedList的元素。 |
ㅤ | 通过[Symbol.iterator]():IterableIterator<T>迭代器进行数据访问。 |
修改元素 | 通过list[index]=xxx修改指定index位置对应的value值。 |
ㅤ | 通过set(index: number,element: T)修改指定index位置的元素值为element。 |
删除元素 | 通过removeByIndex(index: number)删除index位置对应的value值。 |
ㅤ | 通过remove(element: T)删除第一个匹配到的元素。 |
(5)Deque
Deque可用来构造双端队列对象,存储元素遵循先进先出以及先进后出的规则,双端队列可以分别从队头或者队尾进行访问。
Deque依据泛型定义,要求存储位置是一片连续的内存空间,其初始容量大小为8,并支持动态扩容,每次扩容大小为原始容量的2倍。Deque底层采用循环队列实现,入队及出队操作效率都比较高。
- Deque和Queue相比,Queue的特点是先进先出,只能在头部删除元素,尾部增加元素。
- Deque和Vector相比,它们都支持在两端增删元素,但Deque不能进行中间插入的操作。对头部元素的插入删除效率高于Vector,而Vector访问元素的效率高于Deque。
- 需要频繁在集合两端进行增删元素的操作时,推荐使用Deque。
Deque进行增、删、改、查操作的常用API如下:
操作 | 描述 |
增加元素 | 通过insertFront(element: T)函数每次在队头增加一个元素。 |
增加元素 | 通过insertEnd(element: T)函数每次在队尾增加一个元素。 |
访问元素 | 通过getFirst()获取队首元素的value值,但是不进行出队操作。 |
ㅤ | 通过getLast()获取队尾元素的value值,但是不进行出队操作。 |
ㅤ | 通过popFirst()获取队首元素的value值,并进行出队操作。 |
ㅤ | 通过popLast()获取队尾元素的value值,并进行出队操作。 |
ㅤ | 通过forEach(callbackFn:(value: T, index?: number, deque?: Deque<T>) => void, thisArg?: Object)访问整个Deque的元素。 |
ㅤ | 通过[Symbol.iterator]():IterableIterator<T>迭代器进行数据访问。 |
修改元素 | 通过forEach(callbackFn:(value: T, index?: number, deque?: Deque<T>)=> void, thisArg?: Object)对队列进行修改操作。 |
删除元素 | 通过popFirst()对队首元素进行出队操作并删除。 |
ㅤ | 通过popLast()对队尾元素进行出队操作并删除。 |
(6)Queue
Queue可用来构造队列对象,存储元素遵循先进先出的规则。
Queue依据泛型定义,要求存储位置是一片连续的内存空间,初始容量大小为8,并支持动态扩容,每次扩容大小为原始容量的2倍。
Queue底层采用循环队列实现,入队及出队操作效率都比较高。
- Queue和Deque相比,Queue只能在一端删除一端增加,Deque可以两端增删。
一般符合先进先出的场景可以使用Queue。
Queue进行增、删、改、查操作的常用API如下:
操作 | 描述 |
增加元素 | 通过add(element: T)函数每次在队尾增加一个元素。 |
访问元素 | 通过getFirst()获取队首元素的value值,但是不进行出队操作。 |
ㅤ | 通过pop()获取队首元素的value值,并进行出队操作。 |
ㅤ | 通过forEach(callbackFn: (value: T, index?: number, queue?: Queue<T>) => void,thisArg?: Object)访问整个Queue的元素。 |
ㅤ | 通过[Symbol.iterator]():IterableIterator<T>迭代器进行数据访问。 |
修改元素 | 通过forEach(callbackFn:(value: T, index?: number, queue?: Queue<T>) => void,thisArg?: Object)对队列进行修改操作。 |
删除元素 | 通过pop()对队首进行出队操作并删除。 |
(7)Stack
Stack可用来构造栈对象,存储元素遵循先进后出的规则。
Stack依据泛型定义,要求存储位置是一片连续的内存空间,初始容量大小为8,并支持动态扩容,每次扩容大小为原始容量的1.5倍。Stack底层基于数组实现,入栈出栈均从数组的一端操作。
- Stack和Queue相比,Queue基于循环队列实现,只能在一端删除,另一端插入,而Stack都在一端操作。
- 一般符合先进后出的场景可以使用Stack。
Stack进行增、删、改、查操作的常用API如下:
操作 | 描述 |
增加元素 | 通过push(item: T)函数每次在栈顶增加一个元素。 |
访问元素 | 通过peek()获取栈顶元素的value值,但是不进行出栈操作。 |
ㅤ | 通过pop()获取栈顶的value值,并进行出栈操作。 |
ㅤ | 通过forEach(callbackFn: (value: T, index?: number, stack?: Stack<T>) => void, thisArg?: Object)访问整个Stack的元素。 |
ㅤ | 通过[Symbol.iterator]():IterableIterator<T>迭代器进行数据访问。 |
ㅤ | 通过locate(element: T)获取元素对应的位置。 |
修改元素 | 通过forEach(callbackFn:(value: T, index?: number, stack?: Stack<T>) => void, thisArg?: Object)对栈内元素进行修改操作。 |
删除元素 | 通过pop()对栈顶进行出栈操作并删除。 |
线性容器的使用
此处列举常用的线性容器ArrayList、Vector、Deque、Stack、List的使用示例,包括导入模块、增加元素、访问元素及修改等操作。示例代码如下所示:
非线性容器
非线性容器实现能快速查找的数据结构,其底层通过hash或者红黑树实现,包括HashMap、HashSet、TreeMap、TreeSet、LightWeightMap、LightWeightSet、PlainArray七种。非线性容器中的key及value的类型均满足ECMA标准。
(1)HashMap
HashMap可用来存储具有关联关系的key-value键值对集合,存储元素中key是唯一的,每个key会对应一个value值。
HashMap依据泛型定义,集合中通过key的hash值确定其存储位置,从而快速找到键值对。HashMap的初始容量大小为16,并支持动态扩容,每次扩容大小为原始容量的2倍。HashMap底层基于HashTable实现,冲突策略采用链地址法。
- HashMap和TreeMap相比,HashMap依据键的hashCode存取数据,访问速度较快。而TreeMap是有序存取,效率较低。
- HashSet基于HashMap实现。HashMap的输入参数由key、value两个值组成。在HashSet中,只对value对象进行处理。
- 需要快速存取、删除以及插入键值对数据时,推荐使用HashMap。
HashMap进行增、删、改、查操作的常用API如下:
操作 | 描述 |
增加元素 | 通过set(key: K, value: V)函数每次在HashMap增加一个键值对。 |
访问元素 | 通过get(key: K)获取key对应的value值。 |
ㅤ | 通过keys()返回一个迭代器对象,包含map中的所有key值。 |
ㅤ | 通过values()返回一个迭代器对象,包含map中的所有value值。 |
ㅤ | 通过entries()返回一个迭代器对象,包含map中的所有键值对。 |
ㅤ | forEach(callbackFn: (value?: V, key?: K, map?: HashMap<K, V>) => void, thisArg?: Object)访问整个map的元素。 |
ㅤ | 通过[Symbol.iterator]():IterableIterator<[K,V]>迭代器进行数据访问。 |
修改元素 | 通过replace(key: K, newValue: V)对指定key对应的value值进行修改操作。 |
ㅤ | 通过forEach(callbackFn: (value?: V, key?: K, map?: HashMap<K, V>) => void, thisArg?: Object)对map中元素进行修改操作。 |
删除元素 | 通过remove(key: K)对map中匹配到的键值对进行删除操作。 |
ㅤ | 通过clear()清空整个map集合。 |
(2)HashSet
HashSet可用来存储一系列值的集合,存储元素中value是唯一的。
HashSet依据泛型定义,集合中通过value的hash值确定其存储位置,从而快速找到该值。HashSet初始容量大小为16,支持动态扩容,每次扩容大小为原始容量的2倍。value的类型满足ECMA标准中要求的类型。HashSet底层数据结构基于HashTable实现,冲突策略采用链地址法。
- HashSet基于HashMap实现。在HashSet中,只对value对象进行处理。
- HashSet和TreeSet相比,HashSet中的数据无序存放,即存放元素的顺序和取出的顺序不一致,而TreeSet是有序存放。它们集合中的元素都不允许重复,但HashSet允许放入null值,TreeSet不建议插入空值,可能会影响排序结果。
- 可以利用HashSet不重复的特性,当需要不重复的集合或需要去重某个集合的时候使用。
HashSet进行增、删、改、查操作的常用API如下:
操作 | 描述 |
增加元素 | 通过add(value: T)函数每次在HashSet增加一个值。 |
访问元素 | 通过values()返回一个迭代器对象,包含set中的所有value值。 |
ㅤ | 通过entries()返回一个迭代器对象,包含类似键值对的数组,键值都是value。 |
ㅤ | 通过forEach(callbackFn: (value?: T, key?: T, set?: HashSet<T>) => void, thisArg?: Object)访问整个set的元素。 |
ㅤ | 通过[Symbol.iterator]():IterableIterator<T>迭代器进行数据访问。 |
修改元素 | 通过forEach(callbackFn: (value?: T, key?: T, set?: HashSet<T>) => void, thisArg?: Object)对set中value进行修改操作。 |
删除元素 | 通过remove(value: T)对set中匹配到的值进行删除操作。 |
ㅤ | 通过clear()清空整个set集合。 |
(3)TreeMap
TreeMap可用来存储具有关联关系的key-value键值对集合,存储元素中key是唯一的,每个key会对应一个value值。
- TreeMap依据泛型定义,集合中的key值是有序的,TreeMap的底层是一棵二叉树,可以通过树的二叉查找快速的找到键值对。key的类型满足ECMA标准中要求的类型。TreeMap中的键值是有序存储的。TreeMap底层基于红黑树实现,可以进行快速的插入和删除。
- TreeMap和HashMap相比,HashMap依据键的hashCode存取数据,访问速度较快。而TreeMap是有序存取,效率较低。
- 一般需要存储有序键值对的场景,可以使用TreeMap。
TreeMap进行增、删、改、查操作的常用API如下:
操作 | 描述 |
增加元素 | 通过set(key: K,value: V)函数每次在TreeMap增加一个键值对。 |
访问元素 | 通过get(key: K)获取key对应的value值。 |
ㅤ | 通过getFirstKey()获取map中排在首位的key值。 |
ㅤ | 通过getLastKey()获取map中排在未位的key值。 |
ㅤ | 通过keys()返回一个迭代器对象,包含map中的所有key值。 |
ㅤ | 通过values()返回一个迭代器对象,包含map中的所有value值。 |
ㅤ | 通过entries()返回一个迭代器对象,包含map中的所有键值对。 |
ㅤ | 通过forEach(callbackFn: (value?: V, key?: K, map?: TreeMap<K, V>) => void, thisArg?: Object)访问整个map的元素。 |
ㅤ | 通过[Symbol.iterator]():IterableIterator<[K,V]>迭代器进行数据访问。 |
修改元素 | 通过replace(key: K,newValue: V)对指定key对应的value值进行修改操作。 |
ㅤ | 通过forEach(callbackFn: (value?: V, key?: K, map?: TreeMap<K, V>) => void, thisArg?: Object)对map中元素进行修改操作。 |
删除元素 | 通过remove(key: K)对map中匹配到的键值对进行删除操作。 |
ㅤ | 通过clear()清空整个map集合。 |
(4)TreeSet
TreeSet可用来存储一系列值的集合,存储元素中value是唯一的。
TreeSet依据泛型定义,集合中的value值是有序的,TreeSet的底层是一棵二叉树,可以通过树的二叉查找快速的找到该value值,value的类型满足ECMA标准中要求的类型。TreeSet中的值是有序存储的。TreeSet底层基于红黑树实现,可以进行快速的插入和删除。
- TreeSet基于TreeMap实现,在TreeSet中,只对value对象进行处理。TreeSet可用于存储一系列值的集合,元素中value唯一且有序。
- TreeSet和HashSet相比,HashSet中的数据无序存放,而TreeSet是有序存放。它们集合中的元素都不允许重复,但HashSet允许放入null值,TreeSet不建议插入空值,可能会影响排序结果。
- 一般需要存储有序集合的场景,可以使用TreeSet。
TreeSet进行增、删、改、查操作的常用API如下:
操作 | 描述 |
增加元素 | 通过add(value: T)函数每次在TreeSet增加一个值。 |
访问元素 | 通过values()返回一个迭代器对象,包含set中的所有value值。 |
ㅤ | 通过entries()返回一个迭代器对象,包含类似键值对的数组,键值都是value。 |
ㅤ | 通过getFirstValue()获取set中排在首位的value值。 |
ㅤ | 通过getLastValue()获取set中排在未位的value值。 |
ㅤ | 通过forEach(callbackFn: (value?: T, key?: T, set?: TreeSet<T>) => void, thisArg?: Object)访问整个set的元素。 |
ㅤ | 通过[Symbol.iterator]():IterableIterator<T>迭代器进行数据访问。 |
修改元素 | 通过forEach(callbackFn: (value?: T, key?: T, set?: TreeSet<T>) => void, thisArg?: Object)对set中value进行修改操作。 |
删除元素 | 通过remove(value: T)对set中匹配到的值进行删除操作。 |
ㅤ | 通过clear()清空整个set集合。 |
(5)LightWeightMap
LightWeightMap可用来存储具有关联关系的key-value键值对集合,存储元素中key是唯一的,每个key会对应一个value值。LightWeightMap依据泛型定义,采用更加轻量级的结构,底层标识唯一key通过hash实现,其冲突策略为线性探测法。集合中的key值的查找依赖于hash值以及二分查找算法,通过一个数组存储hash值,然后映射到其他数组中的key值以及value值,key的类型满足ECMA标准中要求的类型。
初始默认容量大小为8,每次扩容大小为原始容量的2倍。
- LightWeightMap和HashMap都是用来存储键值对的集合,LightWeightMap占用内存更小。
- 当需要存取key-value键值对时,推荐使用占用内存更小的LightWeightMap。
LightWeightMap进行增、删、改、查操作的常用API如下:
操作 | 描述 |
增加元素 | 通过set(key: K,value: V)函数每次在LightWeightMap增加一个键值对。 |
访问元素 | 通过get(key: K)获取key对应的value值。 |
ㅤ | 通过getIndexOfKey(key: K)获取map中指定key的index。 |
ㅤ | 通过getIndexOfValue(value: V)获取map中指定value出现的第一个的index。 |
ㅤ | 通过keys()返回一个迭代器对象,包含map中的所有key值。 |
ㅤ | 通过values()返回一个迭代器对象,包含map中的所有value值。 |
ㅤ | 通过entries()返回一个迭代器对象,包含map中的所有键值对。 |
ㅤ | 通过getKeyAt(index: number)获取指定index对应的key值。 |
ㅤ | 通过getValueAt(index: number)获取指定index对应的value值。 |
ㅤ | 通过forEach(callbackFn: (value?: V, key?: K, map?: LightWeightMap<K, V>) => void, thisArg?: Object)访问整个map的元素。 |
ㅤ | 通过[Symbol.iterator]():IterableIterator<[K,V]>迭代器进行数据访问。 |
修改元素 | 通过setValueAt(index: number, newValue: V)对指定index对应的value值进行修改操作。 |
ㅤ | 通过forEach(callbackFn: (value?: V, key?: K, map?: LightWeightMap<K, V>) => void, thisArg?: Object)对map中元素进行修改操作。 |
删除元素 | 通过remove(key: K)对map中匹配到的键值对进行删除操作。 |
ㅤ | 通过removeAt(index: number)对map中指定index的位置进行删除操作。 |
ㅤ | 通过clear()清空整个map集合。 |
(6)LightWeightSet
LightWeightSet可用来存储一系列值的集合,存储元素中value是唯一的。
LightWeightSet依据泛型定义,采用更加轻量级的结构,初始默认容量大小为8,每次扩容大小为原始容量的2倍。集合中的value值的查找依赖于hash以及二分查找算法,通过一个数组存储hash值,然后映射到其他数组中的value值,value的类型满足ECMA标准中要求的类型。
- LightWeightSet底层标识唯一value基于hash实现,其冲突策略为线性探测法,查找策略基于二分查找法。
- LightWeightSet和HashSet都是用来存储键值的集合,LightWeightSet的占用内存更小。
- 当需要存取某个集合或是对某个集合去重时,推荐使用占用内存更小的LightWeightSet。
LightWeightSet进行增、删、改、查操作的常用API如下:
操作 | 描述 |
增加元素 | 通过add(obj: T)函数每次在LightWeightSet增加一个值。 |
访问元素 | 通过getIndexOf(key: T)获取对应的index值。 |
ㅤ | 通过values()返回一个迭代器对象,包含map中的所有value值。 |
ㅤ | 通过entries()返回一个迭代器对象,包含map中的所有键值对。 |
ㅤ | 通过getValueAt(index: number)获取指定index对应的value值。 |
ㅤ | 通过forEach(callbackFn: (value?: T, key?: T, set?: LightWeightSet<T>) => void, thisArg?: Object)访问整个set的元素。 |
ㅤ | 通过[Symbol.iterator]():IterableIterator<T>迭代器进行数据访问。 |
修改元素 | 通过forEach(callbackFn: (value?: T, key?: T, set?: LightWeightSet<T>) => void, thisArg?: Object)对set中元素进行修改操作。 |
删除元素 | 通过remove(key: K)对set中匹配到的键值对进行删除操作。 |
ㅤ | 通过removeAt(index: number)对set中指定index的位置进行删除操作。 |
ㅤ | 通过clear()清空整个set集合。 |
(7)PlainArray
PlainArray可用来存储具有关联关系的键值对集合,存储元素中key是唯一的,并且对于PlainArray来说,其key的类型为number类型。每个key会对应一个value值,类型依据泛型的定义,PlainArray采用更加轻量级的结构,集合中的key值的查找依赖于二分查找算法,然后映射到其他数组中的value值。
初始默认容量大小为16,每次扩容大小为原始容量的2倍。
- PlainArray和LightWeightMap都是用来存储键值对,且均采用轻量级结构,但PlainArray的key值类型只能为number类型。
- 当需要存储key值为number类型的键值对时,可以使用PlainArray。
PlainArray进行增、删、改、查操作的常用API如下:
操作 | 描述 |
增加元素 | 通过add(key: number,value: T)函数每次在PlainArray增加一个键值对。 |
访问元素 | 通过get(key: number)获取key对应的value值。 |
ㅤ | 通过getIndexOfKey(key: number)获取PlainArray中指定key的index。 |
ㅤ | 通过getIndexOfValue(value: T)获取PlainArray中指定value的index。 |
ㅤ | 通过getKeyAt(index: number)获取指定index对应的key值。 |
ㅤ | 通过getValueAt(index: number)获取指定index对应的value值。 |
ㅤ | 通过forEach(callbackFn: (value: T, index?: number, PlainArray?: PlainArray<T>) => void, thisArg?: Object)访问整个plainarray的元素。 |
ㅤ | 通过[Symbol.iterator]():IterableIterator<[number, T]>迭代器进行数据访问。 |
修改元素 | 通过setValueAt(index:number, value: T)对指定index对应的value值进行修改操作。 |
ㅤ | 通过forEach(callbackFn: (value: T, index?: number, PlainArray?: PlainArray<T>) => void, thisArg?: Object)对plainarray中元素进行修改操作。 |
删除元素 | 通过remove(key: number)对plainarray中匹配到的键值对进行删除操作。 |
ㅤ | 通过removeAt(index: number)对plainarray中指定index的位置进行删除操作。 |
ㅤ | 通过removeRangeFrom(index: number, size: number)对plainarray中指定范围内的元素进行删除操作。 |
ㅤ | 通过clear()清空整个PlainArray集合。 |
非线性容器的使用
此处列举常用的非线性容器HashMap、TreeMap、LightWeightMap、PlainArray的使用示例,包括导入模块、增加元素、访问元素及修改等操作,示例代码如下所示:
XML生成、解析与转换
XML概述
XML(可扩展标记语言)是一种用于描述数据的标记语言,旨在提供一种通用的方式来传输和存储数据,特别是Web应用程序中经常使用的数据。XML并不预定义标记。因此,XML更加灵活,并且可以适用于广泛的应用领域。
XML文档由元素(element)、属性(attribute)和内容(content)组成。
- 元素指的是标记对,包含文本、属性或其他元素。
- 属性提供了有关元素的其他信息。
- 内容则是元素包含的数据或子元素。
XML还可以通过使用XML Schema或DTD(文档类型定义)来定义文档结构。这些机制允许开发人员创建自定义规则以验证XML文档是否符合其预期的格式。
XML还支持命名空间、实体引用、注释、处理指令等特性,使其能够灵活地适应各种数据需求。
XML生成
- XML可以作为数据交换格式,被各种系统和应用程序所支持。例如Web服务,可以将结构化数据以XML格式进行传递。
- XML还可以作为消息传递格式,在分布式系统中用于不同节点之间的通信与交互。
注意事项
- XML标签必须成对出现,生成开始标签就要生成结束标签。
- XML标签对大小写敏感,开始标签与结束标签大小写要一致。
开发步骤
XML模块提供XmlSerializer类来生成XML文件,输入为固定长度的Arraybuffer或DataView对象,该对象用于存放输出的XML数据。
通过调用不同的方法来写入不同的内容,如startElement(name: string)写入元素开始标记,setText(text: string)写入标签值。
XML模块的API接口可以参考@ohos.xml的详细描述,按需求调用对应函数可以生成一份完整的XML文件。
(1)引入模块
(2)创建缓冲区,构造XmlSerializer对象(可以基于Arraybuffer构造XmlSerializer对象, 也可以基于DataView构造XmlSerializer对象)。
(3)调用XML元素生成函数
(4)使用Uint8Array操作Arraybuffer,调用TextDecoder对Uint8Array解码后输出。
输出结果如下:
XML解析
XML模块提供XmlPullParser类对XML文件解析,输入为含有XML文本的ArrayBuffer或DataView,输出为解析得到的信息。
表1 XML解析选项
名称 | 类型 | 必填 | 说明 |
supportDoctype | boolean | 否 | 是否忽略文档类型。默认为false,表示对文档类型进行解析。 |
ignoreNameSpace | boolean | 否 | 是否忽略命名空间。默认为false,表示对命名空间进行解析。 |
tagValueCallbackFunction | (name: string, value: string) => boolean | 否 | 获取tagValue回调函数,打印标签及标签值。默认为null,表示不进行XML标签和标签值的解析。 |
attributeValueCallbackFunction | (name: string, value: string) => boolean | 否 | 获取attributeValue回调函数, 打印属性及属性值。默认为null,表示不进行XML属性和属性值的解析。 |
tokenValueCallbackFunction | (eventType: EventType, value: ParseInfo) => boolean | 否 | 获取tokenValue回调函数,打印标签事件类型及parseInfo对应属性。默认为null,表示不进行XML事件类型解析。 |
注意事项
- XML解析及转换需要确保传入的XML数据符合标准格式。
- XML解析目前不支持按指定节点解析对应的节点值。
解析XML标签和标签值
(1)引入模块。
(2)对XML文件编码后调用XmlPullParser。可以基于ArrayBuffer构造XmlPullParser对象, 也可以基于DataView构造XmlPullParser对象。
(3)自定义回调函数,本例直接打印出标签及标签值。
(4)设置解析选项,调用parse函数。
输出结果如下所示:
解析XML属性和属性值
(1)引入模块。
(2)对XML文件编码后调用XmlPullParser。
(3)自定义回调函数,本例直接打印出属性及属性值。
(4)设置解析选项,调用parse函数。
(5)输出结果如下所示:
解析XML事件类型和元素深度
(1)引入模块。
(2)对XML文件编码后调用XmlPullParser。
(3)自定义回调函数,本例直接打印元素事件类型及元素深度。
(4)设置解析选项,调用parse函数。
(5)输出结果如下所示:
场景示例
此处以调用所有解析选项为例,提供解析XML标签、属性和事件类型的开发示例。
输出结果如下所示:
XML转换
将XML文本转换为JavaScript对象可以更轻松地处理和操作数据,并且更适合在JavaScript应用程序中使用。
语言基础类库提供ConvertXML类将XML文本转换为JavaScript对象,输入为待转换的XML字符串及转换选项,输出为转换后的JavaScript对象。具体转换选项可见@ohos.convertxml。
注意事项
XML解析及转换需要确保传入的XML数据符合标准格式。
开发步骤
此处以XML转为JavaScript对象后获取其标签值为例,说明转换效果。
(1)引入模块。
(2)输入待转换的XML,设置转换选项。
(3)调用转换函数,打印结果。
输出结果如下所示:
- Author:Koreyoshi
- URL:https://Koreyoshi1216.com/article/150c7b13-c6a7-8044-ba88-f0f3578f2d12
- Copyright:All articles in this blog, except for special statements, adopt BY-NC-SA agreement. Please indicate the source!