📖 鸿蒙应用研发
注:以下内容是对鸿蒙开发文档的开发章节和工具章节的学习整理,仅供参考。
一、Ability Kit(程序框架服务)- 应用框架
简要介绍
-
解释:Ability Kit(程序框架服务)提供了应用程序开发和运行的应用模型,是系统为开发者提供的应用程序所需能力的抽象提炼,它提供了应用程序必备的组件和运行机制。
-
使用场景:应用的多 Module 开发(HAP for 应用的功能和特性;HAR & HSP for 代码和资源的共享)、应用内的交互、应用间的交互、应用的跨设备流转。
-
能力范围:应用进程创建和销毁、应用生命周期调度;应用组件运行入口、应用组件生命周期调度、组件间交互;应用上下文环境、系统环境变化监听;应用流转能力;多包机制、共享包、应用信息配置;程序访问控制能力;安全密码自动填充能力。
-
-
应用模型的构成要素
-
应用组件:是应用的基本组成单位、应用的运行入口。应用组件的不同状态是应用组件的生命周期。
流程:操作系统在运行期间通过配置文件创建应用组件的实例,并调度它的生命周期回调函数,从而执行开发者的代码。
-
应用进程模型:定义应用进程的创建和销毁方式、进程间的通信方式。
-
应用线程模型:定义应用进程内线程的创建和销毁方式、主线程和 UI 线程的创建方式、线程间的通信方式。
-
应用任务管理模型(仅对系统应用开放):定义任务(Mission)的创建和销毁方式、任务与组件间的关系。
任务:用户使用一个应用组件实例的记录(“最近任务” 界面)。
-
应用配置文件:应用配置信息、应用组件信息、权限信息、开发者自定义信息等。
使用时机:编译构建 --> 编译工具;分发阶段 --> 应用市场;运行阶段 --> 操作系统。
-
-
应用模型的分类
FA(Feature Ability)模型:每个应用组件独享一个 ArkTS 引擎实例,不再主推。- Stage 模型:多个应用组件共享同一个 ArkTS 引擎实例,主推。
Stage 模型
Stage 模型概念图
注:这里的 0:N 表示一个 Ability 可以持有 0~N 个 ExtensionAbility。
- UIAbility 组件:包含 UI 的应用组件,主要用于和用户交互。生命周期包含创建/销毁/前台/后台等状态。
- ExtensionAbility 组件:面向特定场景的应用组件。开发者需要使用 ExtensionAbility 组件的派生类。
- WindowStage 类:起到了应用进程内窗口管理器的作用,该类包含一个主窗口,该主窗口为 ArkUI 提供了绘制区域。
- Context 及其派生类:向开发者提供在运行期可以调用的各种资源和能力。
应用组件开发
应用/组件级配置
-
应用包名配置
1
2
3
4
5
6{
"app": {
// ...
"bundleName": "com.application.myapplication", //【必须】标识应用的 Bundle 名称,用于标识应用的唯一性。
},
}注:推荐采用反域名形式命名(如 com.example.demo,建议第一级为域名后缀 com,第二级为厂商/个人名,第三级为应用名,也可以多级)。
-
图标和标签配置
1
2
3
4
5
6
7{
"app": {
// ...
"icon": "$media:app_icon", //【必须】应用图标
"label": "$string:app_name" //【必须】应用标签
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20{
"module": {
// ...
"abilities": [
{
"icon": "$media:icon", // 应用图标
"label": "$string:EntryAbility_label", // 应用标签
"skills": [
"entities": [
"entity.system.home" // 必须是 "entity.system.home"
],
"actions": [
"ohos.want.action.home" // 必须是 "ohos.want.action.home" 或 "action.system.home"
]
}
],
}
]
}
}注:系统对无图标应用实施严格管控,如果预置应用确需隐藏桌面应用图标,需要配置 AllowAppDesktopIconHide 应用特权,此时应用不会在桌面上显示。
鸿蒙允许同时配置 AppScope/app.json5 和 HAP 包下的 src/main/module.json5 中的
icon
和label
字段。其中 module.json5 中的配置是非强制的,但此时必须确保 app.json5 中进行了配置。如果 app.json5 和 module.json5 中同时配置了icon
和label
字段,则最终的图标和标签的生成规则为,- HAP 中包含 UIAbility 时,
- 如果在 module.json5 的 abilities 字段中配置了
icon
和label
,且该对应的 ability 中skills
字段下面的entities
中包含 “entity.system.home”、actions
中包含 " ohos.want.action.home" 或者 “action.system.home”,则系统将优先返回 module.json5 中的icon
与label
。如果存在多个满足条件的 ability,优先返回 module.json5 中mainElement
对应的 ability 配置的icon
和label
。 - 如果在 module.json5 配置文件的 abilities 中未设置
icon
和label
,系统将返回 app.json5 中的icon
和label
。
- 如果在 module.json5 的 abilities 字段中配置了
- HAP 中不包含 UIAbility 时,系统以 app.json5 中的配置为准。
- HAP 中包含 UIAbility 时,
-
应用版本声明配置
1
2
3
4
5
6{
"app": {
// ...
"versionCode": 1000000, //【必须】标识应用的版本号,取值为小于 2^31 次方的正整数。此数字仅用于确定某个版本是否比另一个版本更新,数值越大表示版本越高。
"versionName": "1.0.0", //【必须】标识向用户展示的应用版本号。取值为长度不超过 127 字节的字符串,仅由数字和点构成,推荐采用 “A.B.C.D” 四段式的形式。
} -
Module 支持的设备类型配置
1
2
3
4
5
6
7
8{
"module": {
// ...
"deviceTypes": [ //【必须】标识当前 Module 可以运行在哪类设备上。
"tv",
"tablet"
],
} -
Module 权限配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16{
"module": {
"requestPermissions": [ // 标识当前应用运行时需向系统申请的权限集合。【默认值为空,对象数组】
{
"name" : "ohos.permission.PERMISSION1", // 【必须】需要使用的权限名称。
"reason": "$string:reason", // 申请权限的原因。user_grant 权限时必填,并且需要进行多语种适配。
"usedScene": { // 【必须】权限使用的场景,该字段用于应用上架校验。
"abilities": [ // 使用权限的 UIAbility 或者 ExtensionAbility 组件的名称。【默认值为空,字符串数组】
"FormAbility"
],
"when": "inuse" // 调用时机。可选 "inuse" 或 "always"。【默认值为空,字符串】
}
}
]
},
}
UIAbility 组件
解释:UIAbility 组件是一种包含 UI 的应用组件,主要用于和用户交互;是系统调度的基本单元,为应用提供绘制界面的窗口。
-
能力支持:跨端迁移和多端协同;多设备和多窗口形态。
-
划分原则:每一个 UIAbility 组件实例都会在最近任务列表中显示一个对应的任务,可以基此选择使用单个还是多个 UIAbility 进行应用开发。
-
使用配置:为了正常使用 UIAbility 组件,需要在对应的 HAP 包下的 src/main/module.json5 中的
abilities
字段中进行配置。1
2
3
4
5
6
7
8
9
10
11
12
13
14{
"module": {
// ...
"abilities": [{ // 标识当前 Module 中 UIAbility 的配置信息,只对当前 UIAbility 生效。【默认值为空,对象数组】
"name": "EntryAbility", //【必须】标识当前 UIAbility 组件的名称,确保该名称在整个应用中唯一。取值为长度不超过 127 字节的字符串,不支持中文。
"srcEntry": "./ets/entryability/EntryAbility.ets", // 【必须】标识入口 UIAbility 的代码路径,取值为长度不超过 127 字节的字符串。
"launchType":"singleton", // 标识当前 UIAbility 组件的启动模式,支持的取值如下:1️⃣ multiton:多实例模式,每次启动创建一个新实例。2️⃣ singleton:单实例模式,仅第一次启动创建新实例【默认值】。3️⃣ specified:指定实例模式,运行时由开发者决定是否创建新实例。4️⃣ standard:multiton 的曾用名,效果与多实例模式一致。
"description": "$string:description_main_ability", // 标识当前 UIAbility 组件的描述信息,取值为长度不超过 255 字节的字符串。要求采用描述信息的资源索引,以支持多语言。【默认值为空,字符串】
"icon": "$media:layered_image", // 标识当前 UIAbility 组件的图标,取值为图标资源文件的索引。【默认值为空,字符串】
"label": "$string:EntryAbility_label", // 标识当前 UIAbility 组件对用户显示的名称,要求采用该名称的资源索引,以支持多语言。取值为长度不超过 255 字节的字符串。【默认值为空,字符串】
"startWindowIcon": "$media:icon", // 【必须,字符串】标识当前 UIAbility 组件启动页面图标资源文件的索引,取值为长度不超过 255 字节的字符串。
"startWindowBackground": "$color:red", // 【必须,字符串】标识当前 UIAbility 组件启动页面背景颜色资源文件的索引,取值为长度不超过 255 字节的字符串。
}]
}
生命周期
Stage 模型生命周期图
1 | import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit'; |
-
UIAbility 生命周期:
onCreate
、onForeground
、onBackground
、onDestory
状态 生命周期回调 触发时机 作用 Create onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void
应用加载过程中,UIAbility 实例创建完成时 页面初始化操作,例如变量定义资源加载等,用于后续的 UI 展示 Foreground onForeground(): void
在 UIAbility 的 UI 可见之前,如 UIAbility 切换至前台时 申请系统需要的资源,或者重新申请在 onBackground()
中释放的资源Background onBackground(): void
在 UIAbility 的 UI 完全不可见之后,如 UIAbility 切换至后台时 释放 UI 不可见时无用的资源,或者在此回调中执行较为耗时的操作,例如状态保存等 Destroy `onDestroy(): void Promise ` 在 UIAbility 实例销毁,如调用 terminateSelf() 方法停止当前 UIAbility 实例,执行 onDestroy() 回调,并完成 UIAbility 实例的销毁 注:Want 是对象间信息传递的载体,可以用于应用组件间的信息传递。
注:API 13+,通过最近任务列表一键清理来关闭该 UIAbility 实例,将不会执行 onDestroy() 回调,而是会直接终止进程。
-
WindowStage 生命周期:
WindowStageCreate
、WindowStageDestroy
、WindowStageWillDestroy
状态 生命周期回调 触发时机 作用 WindowStageCreate onWindowStageCreate(windowStage: window.WindowStage): void
UIAbility 实例创建完成之后,在进入 Foreground 之前,系统会创建一个 WindowStage,WindowStage 创建完成后 设置 UI 加载( windowStage.loadContent
设置应用要加载的页面)、设置 WindowStage 的事件订阅(windowStage.on('windowStageEvent')
订阅 WindowStage 事件)。WindowStageDestroy onWindowStageDestroy(): void
在 UIAbility 实例销毁之前 释放 UI 资源 WindowStageWillDestroy onWindowStageWillDestroy(windowStage: window.WindowStage): void
在 WindowStage 销毁前执行,此时 WindowStage 可以使用 释放通过 windowStage 对象获取的资源、注销 WindowStage 事件订阅**( windowStage.off('windowStageEvent')
)** -
UIAbility 事件回调:
onNewWant
事件回调 触发时机 作用 onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void
当应用的 UIAbility 实例已创建,且 UIAbility 配置为 singleton 启动模式时,再次调用 startAbility()方法启动该 UIAbility 实例时,此时不会触发 onCreate() 和 onWindowStageCreate() 回调 更新要加载的资源和数据等,用于后续的 UI 展示
启动模式
1 | { |
-
singleton(单实例模式):每次调用
startAbility()
方法时,如果应用进程中该类型的 UIAbility 实例已经存在,则复用系统中的 UIAbility 实例,并执行该 UIAbility 的onNewWant()
回调(不会执行该 UIAbility 的onCreate()
和onWindowStageCreate()
生命周期回调)。系统中只存在唯一一个该 UIAbility 实例,即在最近任务列表中只存在一个该类型的 UIAbility 实例。单实例模式也是默认情况下的启动模式。如果已经创建的实例仍在启动过程中,调用startAbility()
接口启动该实例,将收到错误码 16000082。 -
multiton(多实例模式):每次调用
startAbility()
方法时,都会在应用进程中创建一个新的该类型 UIAbility 实例。即在最近任务列表中可以看到有多个该类型的 UIAbility 实例。 -
specified(指定实例模式):每次调用
startAbility()
方法时,可以在**Want**
参数的 parameters 字段中设置唯一 Key 值(键名可自定义,如 instanceKey),用于标识 SpecifiedAbility(即启动模式设置为 sepcified 的 UIAbility)。系统在拉起对应的 SpecifiedAbility 前,会指定对应的 AbilityStage 的生命周期函数onAcceptWant()
,该函数接收通过startAbility()
传递的Want
参数,计算后返回一个字符串,作为目标 UIAbility 的 Key 值。系统根据该计算的 Key 值匹配 UIAbility:如果匹配到,则启动对应的 UIAbility 实例,并执行对应的onNewWant()
生命周期回调;否则,创建一个新的 UIAbility 实例,并执行该实例对应的onCreate()
和onWindowStageCreate()
生命周期回调。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44// 在启动指定实例模式(specified)的 UIAbility 时,给每一个 UIAbility 实例配置一个独立的 Key 标识
// 例如在文档使用场景中,可以用文档路径作为 Key 标识
import { common, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { BusinessError } from '@kit.BasicServicesKit';
const DOMAIN_NUMBER: number = 0xFF00;
const TAG: string = '[Page_Test_StartAbility]';
@Entry
@Component
struct Page_StartModel {
private KEY_NEW = 'KEY';
build() {
Row() {
Column() {
// ...
Button()
.onClick(() => {
let context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext;
// context 为调用方 UIAbility 的 UIAbilityContext;
let want: Want = {
deviceId: '', // deviceId 为空表示本设备
bundleName: 'com.samples.stagemodelabilitydevelop',
abilityName: 'SpecifiedAbility',
moduleName: 'entry', // moduleName 非必选
parameters: {
// 自定义信息
instanceKey: this.KEY_NEW // 唯一标识符,以区分不同的 UIAbility 实例(系统最终还是按照被启动的 UIAbility 所在的 AbilityStage 的 onAcceptWant 计算的 Key 值为准)
}
};
context.startAbility(want).then(() => {
hilog.info(DOMAIN_NUMBER, TAG, 'Succeeded in starting SpecifiedAbility.');
}).catch((err: BusinessError) => {
hilog.error(DOMAIN_NUMBER, TAG, `Failed to start SpecifiedAbility. Code is ${err.code}, message is ${err.message}`);
})
this.KEY_NEW = this.KEY_NEW + 'a';
})
// ...
}
}
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15import { AbilityStage, Want } from '@kit.AbilityKit';
export default class TestAbilityStage extends AbilityStage {
onAcceptWant(want: Want): string {
// 在被调用方的 AbilityStage 中,针对启动模式为 specified 的 UIAbility 返回一个 UIAbility 实例对应的一个 Key 值
if (want.abilityName === 'SpecifiedAbility') { // 只有 UIAbility 的 name 为 SpecifiedAbility 时,才返回自定义的 Key 标识
// 返回的字符串 KEY 标识为自定义拼接的字符串内容
if (want.parameters) {
return `SpecifiedAbilityInstance_${want.parameters.instanceKey}`; //
}
}
// ...
return 'MyAbilityStage'; // 其他未指定的 UIAbility,返回默认的 MyAbilityStage 作为 Key 标识
}
}注:
startAbility()
通过Want
参数传递的 Key 值是开发者自定义的;onAcceptWant()
计算的 Key 值是根据指定的逻辑解析Want
参数后得到的,系统根据该 Key 值来匹配 UIAbility。注:AbilityStage 文件需要自行创建,并在 HAP 包下的 src/main/module.json5 中的
srcEntry
字段中指定该文件路径,以作为 HAP 加载的入口。
基本用法
-
指定启动页面:在 UIAbility 的
onWindowStageCreate()
生命周期回调中,通过 WindowStage 对象的loadContent()
方法设置启动页面,否则会导致应用启动后白屏。1
2
3
4
5
6
7
8
9
10
11
12import { UIAbility } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
export default class EntryAbility extends UIAbility {
onWindowStageCreate(windowStage: window.WindowStage): void {
// Main window is created, set main page for this ability
windowStage.loadContent('pages/Index', (err, data) => {
// ...
});
}
// ...
} -
获取上下文信息:UIAbility 的上下文信息是 UIAbilityContext 类的实例,通过该实例,获取 UIAbility 的相关配置信息,如包代码路径、Bundle 名称、Ability 名称和应用程序需要的环境状态等属性信息;获取操作 UIAbility 实例的方法(如
startAbility()
、connectServiceExtensionAbility()
、terminateSelf()
等)。- UIAbility 中获取上下文信息:
this.context
- UI 中获取对应 UIAbility 的上下文信息:
private context = getContext(this) as common.UIAbilityContext
(需要预先从@kit.AbilityKit
中加载common
)
- UIAbility 中获取上下文信息:
数据同步(UIAbility 与 UI 组件)
-
基类 Context 中提供的
**EventHub**
对象(发布订阅机制)注:
EventHub
对象需要通过 UIAbility 的上下文获取。-
UIAbility 中注册事件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24import { hilog } from '@kit.PerformanceAnalysisKit';
import { UIAbility, Context, Want, AbilityConstant } from '@kit.AbilityKit';
const DOMAIN_NUMBER: number = 0xFF00;
const TAG: string = '[EventAbility]';
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
// 获取 eventHub
let eventhub = this.context.eventHub;
// 执行订阅操作
eventhub.on('event1', this.eventFunc); // 订阅方式 1
eventhub.on('event1', (data: string) => { // 订阅方式 2
// 触发事件,完成相应的业务操作
});
hilog.info(DOMAIN_NUMBER, TAG, '%{public}s', 'Ability onCreate');
}
// ...
eventFunc(argOne: Context, argTwo: Context): void {
hilog.info(DOMAIN_NUMBER, TAG, '1. ' + `${argOne}, ${argTwo}`);
return;
}
} -
UIAbility 中取消事件订阅
1
2
3
4
5
6
7
8
9
10
11
12
13import { UIAbility } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
const DOMAIN_NUMBER: number = 0xFF00;
const TAG: string = '[EventAbility]';
export default class EntryAbility extends UIAbility {
// ...
onDestroy(): void {
this.context.eventHub.off('event1');
hilog.info(DOMAIN_NUMBER, TAG, '%{public}s', 'Ability onDestroy');
}
} -
UI 中触发事件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27import { common } from '@kit.AbilityKit';
import { promptAction } from '@kit.ArkUI';
@Entry
@Component
struct Page_EventHub {
private context = getContext(this) as common.UIAbilityContext;
build() {
Column() {
Button('Emit')
.onClick(()=>{
this.context.eventHub.emit('event1'); // 触发事件
promptAction.showToast({
message: 'EventHubFuncA'
});
})
Button('Off')
.onClick(()=>{
this.context.eventHub.off('event1'); // 取消事件订阅
promptAction.showToast({
message: 'EventHubFuncB'
});
})
}
}
}
-
-
AppStorage(多 UIAbility 数据共享)/LocalStorage(单 UIAbility 数据共享)
组件启动
-
启动应用内的 UIAbility 组件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16let wantInfo: Want = {
deviceId: '', // 为空表示本设备
bundleName: 'com.samples.stagemodelabilitydevelop', // 启动应用的 Bundle 名称
moduleName: 'entry', // 非必选,在待启动的 UIAbility 与当前 UIAbility 属于不同 Module 时添加
abilityName: 'FuncAbilityA', // 待启动的 UIAbility 名称
parameters: {
// 自定义信息
info: '来自 EntryAbility Page_UIAbilityComponentsInteractive 页面'
},
};
// context 为调用方 UIAbility 的 UIAbilityContext
this.context.startAbility(wantInfo).then(() => {
hilog.info(DOMAIN_NUMBER, TAG, 'startAbility success.');
}).catch((error: BusinessError) => {
hilog.error(DOMAIN_NUMBER, TAG, 'startAbility failed.');
});1
2
3
4
5
6
7
8
9
10import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit';
export default class FuncAbilityA extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
// 接收调用方 UIAbility 传过来的参数
let funcAbilityWant = want;
let info = funcAbilityWant?.parameters?.info;
}
//...
}1
2
3
4
5
6
7
8let context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext; // UIAbilityContext
// context 为需要停止的 UIAbility 实例的 AbilityContext
context.terminateSelf((err) => {
if (err.code) {
hilog.error(DOMAIN_NUMBER, TAG, `Failed to terminate self. Code is ${err.code}, message is ${err.message}`);
return;
}
});注:调用
terminateSelf()
方法停止当前 UIAbility 实例时,默认会保留该实例的快照(Snapshot),即在最近任务列表中仍然能查看到该实例对应的任务。可以通过 HAP 包下的 src/main/module.json5 中的abilities
的removeMissionAfterTerminate
为 true 以取消保留快照。注:可以调用上下文信息的
getApplicationContext().killAllProcesses()
方法关闭应用所有的 UIAbility 实例。 -
启动应用内的 UIAbility 组件并获取返回结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28let context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext; // UIAbilityContext
const RESULT_CODE: number = 1001;
let want: Want = {
deviceId: '', // deviceId 为空表示本设备
bundleName: 'com.samples.stagemodelabilitydevelop',
moduleName: 'entry', // moduleName 非必选
abilityName: 'FuncAbilityA',
parameters: {
// 自定义信息
info: '来自EntryAbility UIAbilityComponentsInteractive页面'
}
};
context.startAbilityForResult(want).then((data) => {
if (data?.resultCode === RESULT_CODE) {
// 解析被调用方 UIAbility 返回的信息
let info = data.want?.parameters?.info;
hilog.info(DOMAIN_NUMBER, TAG, JSON.stringify(info) ?? '');
if (info !== null) {
promptAction.showToast({
message: JSON.stringify(info)
});
}
}
hilog.info(DOMAIN_NUMBER, TAG, JSON.stringify(data.resultCode) ?? '');
}).catch((err: BusinessError) => {
hilog.error(DOMAIN_NUMBER, TAG, `Failed to start ability for result. Code is ${err.code}, message is ${err.message}`);
});
})1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19let context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext; // UIAbilityContext
const RESULT_CODE: number = 1001;
let abilityResult: common.AbilityResult = {
resultCode: RESULT_CODE,
want: {
bundleName: 'com.samples.stagemodelabilitydevelop',
moduleName: 'entry', // moduleName 非必选
abilityName: 'FuncAbilityB',
parameters: {
info: '来自FuncAbility Index页面'
},
},
};
context.terminateSelfWithResult(abilityResult, (err) => {
if (err.code) {
hilog.error(DOMAIN_NUMBER, TAG, `Failed to terminate self with result. Code is ${err.code}, message is ${err.message}`);
return;
}
}); -
启动应用内的 UIAbility 组件的指定页面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16let context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext; // UIAbilityContext
let want: Want = {
deviceId: '', // deviceId为空表示本设备
bundleName: 'com.samples.stagemodelabilityinteraction',
moduleName: 'entry', // moduleName 非必选
abilityName: 'FuncAbility',
parameters: { // 自定义参数传递页面信息
router: 'funcA' // 通过制定自定义参数,表示跳转到 FuncAbility 下的 pages/funcA 页面
}
};
// context为调用方UIAbility的UIAbilityContext
context.startAbility(want).then(() => {
hilog.info(DOMAIN_NUMBER, TAG, 'Succeeded in starting ability.');
}).catch((err: BusinessError) => {
hilog.error(DOMAIN_NUMBER, TAG, `Failed to start ability. Code is ${err.code}, message is ${err.message}`);
});1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28import { AbilityConstant, Want, UIAbility } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';
const DOMAIN_NUMBER: number = 0xFF00;
const TAG: string = '[EntryAbility]';
export default class EntryAbility extends UIAbility {
funcAbilityWant: Want | undefined = undefined;
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
// 接收调用方 UIAbility 传过来的参数,将其保存到 this 上
this.funcAbilityWant = want;
}
onWindowStageCreate(windowStage: window.WindowStage): void {
// Main window is created, set main page for this ability
hilog.info(DOMAIN_NUMBER, TAG, '%{public}s', 'Ability onWindowStageCreate');
// Main window is created, set main page for this ability
let url = 'pages/Index'; // 默认跳转页面
if (this.funcAbilityWant?.parameters?.router && this.funcAbilityWant.parameters.router === 'funcA') { // 解析指定要跳转的页面
url = 'pages/Page_ColdStartUp'; // 自定义跳转页面
}
windowStage.loadContent(url, (err, data) => {
// ...
});
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53import { AbilityConstant, Want, UIAbility } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import type { Router, UIContext, window } from '@kit.ArkUI';
import type { BusinessError } from '@kit.BasicServicesKit';
const DOMAIN_NUMBER: number = 0xFF00;
const TAG: string = '[EntryAbility]';
export default class EntryAbility extends UIAbility {
funcAbilityWant: Want | undefined = undefined;
uiContext: UIContext | undefined = undefined;
// ...
onWindowStageCreate(windowStage: window.WindowStage): void {
// Main window is created, set main page for this ability
hilog.info(DOMAIN_NUMBER, TAG, '%{public}s', 'Ability onWindowStageCreate');
let url = 'pages/Index';
if (this.funcAbilityWant?.parameters?.router && this.funcAbilityWant.parameters.router === 'funcA') {
url = 'pages/Page_ColdStartUp';
}
windowStage.loadContent(url, (err, data) => {
if (err.code) {
return;
}
let windowClass: window.Window;
windowStage.getMainWindow((err, data) => {
if (err.code) {
hilog.error(DOMAIN_NUMBER, TAG, `Failed to obtain the main window. Code is ${err.code}, message is ${err.message}`);
return;
}
windowClass = data;
this.uiContext = windowClass.getUIContext(); // 需要在冷启动时,将 windowStage 对应的主窗口的 context 绑定在 this 上,便于后续冷启动时,通过 UIContext 去调用 getRouter
});
hilog.info(DOMAIN_NUMBER, TAG, 'Succeeded in loading the content. Data: %{public}s', JSON.stringify(data) ?? '');
});
}
onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
if (want?.parameters?.router && want.parameters.router === 'funcA') {
let funcAUrl = 'pages/Page_HotStartUp';
if (this.uiContext) {
let router: Router = this.uiContext.getRouter();
router.pushUrl({
url: funcAUrl
}).catch((err: BusinessError) => {
hilog.error(DOMAIN_NUMBER, TAG, `Failed to push url. Code is ${err.code}, message is ${err.message}`);
});
}
}
}
// ...
}UIAbility 的冷启动与热启动
- 冷启动:UIAbility 实例处于完全关闭状态下被启动,这需要完整地加载和初始化 UIAbility 实例的代码、资源等。
- 热启动:UIAbility 实例已经启动并在前台运行过,由于某些原因切换到后台,再次启动该 UIAbility 实例,这种情况下可以快速恢复 UIAbility 实例的状态。
备份恢复
-
解释:当应用后台运行时,可能由于系统资源管控等原因导致应用关闭、进程退出,应用直接退出可能会导致用户数据丢失。此时应用可以在 UIAbilityContext 中启用 UIAbility 的备份恢复功能,并对临时数据进行保存,则可以在应用退出后的下一次启动时恢复先前的状态和数据(包括应用的页面栈以及 onSaveState 接口中保存的数据),从而保证用户体验的连贯性。
注:备份恢复机制不适用于:应用的正常启动、正常关闭;设备重启;UIExtensionAbility。
注:备份恢复支持多实例;数据最多保留 7 天,以文件的形式存储在应用的沙箱路径中;由于序列化大小限制,支持的最大数据量为 200 KB。
-
UIAbility 数据备份恢复功能的启用
1
2
3
4
5
6
7
8import { UIAbility } from '@kit.AbilityKit';
export default class EntryAbility extends UIAbility {
onCreate() {
console.info("[Demo] EntryAbility onCreate");
this.context.setRestoreEnabled(true); // 该接口需要在初始化阶段(onForeground 前)调用,设置当前 UIAbility 从后台切换时是否启用数据恢复
}
} -
UIAbility 数据备份恢复
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21import { AbilityConstant,UIAbility,Want } from '@kit.AbilityKit';
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) {
console.info("[Demo] EntryAbility onCreate");
this.context.setRestoreEnabled(true);
if (want && want.parameters) {
let recoveryMyData = want.parameters["myData"]; // 数据恢复(在 onCreate 生命周期中恢复 Want 数据;在 onWindowStateCreate 生命周期中恢复页面栈数据)
}
}
onSaveState(state:AbilityConstant.StateType, wantParams: Record<string, Object>): AbilityConstant.OnSaveResult { // 数据备份(onBackground 生命周期后,系统自动调用 onSaveState,以 WantParams 形式存储备份数据)
// state 是回调保存数据的原因的枚举。AbilityConstant.StateType.CONTINUATION 表示迁移保存状态;AbilityConstant.StateType.APP_RECOVERY 表示应用恢复保存状态。
// wantParams 对应 Want 参数的 parameters 参数对象。
// 返回值 表示是否同意保存当前 UIAbility 的状态的枚举。
// Ability has called to save app data
console.log("[Demo] EntryAbility onSaveState");
wantParams["myData"] = "my1234567";
return AbilityConstant.OnSaveResult.ALL_AGREE; // AbilityConstant.OnSaveResult.ALL_AGREE 表示总是同意保存状态。
}
}- 数据恢复:在
onCreate
生命周期中恢复Want
数据;在onWindowStateCreate
生命周期中恢复页面栈数据。 - 数据备份:在
onBackground
生命周期后,系统自动调用onSaveState
,以WantParams
形式(即Want
参数的parameters
字段)存储备份数据。
- 数据恢复:在
ExtensionAbility 组件(略看)
解释:ExtensionAbility 组件是基于特定场景(例如服务卡片、输入法等)提供的应用组件,以便满足更多的使用场景。每一个具体场景对应一个 ExtensionAbilityType,开发者只能使用(包括实现和访问)系统已定义的类型。
AbilityStage 组件容器
-
解释:AbilityStage 是一个 Module 级别的组件容器,应用的 HAP 包在首次加载时会创建一个 AbilityStage 实例,可以对该 Module 进行初始化等操作。AbilityStage 与 Module 一一对应,即一个 Module 拥有一个 AbilityStage。
-
使用 AbilityStage 的能力
-
创建 AbilityStage 文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23import { AbilityConstant, AbilityStage, Configuration, Want } from '@kit.AbilityKit';
export default class MyAbilityStage extends AbilityStage {
onCreate(): void {
// 应用 HAP 首次加载时触发(AbilityStage 创建完成之后),可以在此执行该 Module 的初始化操作(例如资源预加载、线程创建等)。
}
onAcceptWant(want: Want): string {
// 仅 specified 模式下触发
return 'MyAbilityStage';
}
onConfigurationUpdate(newConfig: Configuration): void {
// 当系统全局配置发生变更时触发的事件,系统语言、深浅色等,配置项目前均定义在 Configuration 类中。
}
onMemoryLevel(level: AbilityConstant.MemoryLevel): void {
// 当系统调整内存时触发的事件。
// 应用被切换到后台时,系统会将在后台的应用保留在缓存中。即使应用处于缓存中,也会影响系统整体性能。当系统资源不足时,系统会通过多种方式从应用
// 中回收内存,必要时会完全停止应用,从而释放内存用于执行关键任务。为了进一步保持系统内存的平衡,避免系统停止用户的应用进程,可以在 AbilityStage
// 中的 onMemoryLevel() 生命周期回调中订阅系统内存的变化情况,释放不必要的资源。
}
} -
HAP 包下的 src/main/module.json5 中的 srcEntry 字段配置 AbilityStage 文件的路径,作为 HAP 加载的入口
1
2
3
4
5
6
7{
"module": {
// ...
"srcEntry": "./ets/myabilitystage/MyAbilityStage.ets",
// ...
}
}
-
应用上下文 Context
各类 Context 的继承关系
各类 Context 的持有关系
-
解释:
Context
是应用中对象的上下文,提供应用的一些基础信息,如 resourceManager(资源管理)、applicationInfo(当前应用信息)、dir(应用文件路径)、area(文件分区)等;提供应用的一些基本方法,例如 createBundleContext()、getApplicationContext()等。- 基类 Context:获取应用文件路径(应用沙箱路径)的能力,其他子类均继承该能力。
- ApplicationContext:相较于基类 Context,额外提供了订阅应用内应用组件的生命周期的变化、订阅系统内存变化、订阅应用内系统环境变化、设置应用语言、设置应用颜色模式、清除应用自身数据的同时撤销应用向用户申请的权限等能力。在 中均可以获取。
- AbilityStageContext:相较于基类 Context,额外提供 HapModuleInfo、Configuration 等信息。
- UIAbilityContext:操作应用组件,获取应用组件的配置信息等能力。
- ExtensionContext:基于特定场景,提供不同能力。
-
Context 的获取方式
- ApplicationContext:可以通过 UIAbility、ExtensionAbility、AbilityStage 的
context
获取,this.context.getApplicationContext()
- AbilityStageContext:在 AbilityStage 文件中,
this.context
- UIAbilityContext
- 在 UIAbility 文件中
this.context
- 在 UI 页面中
private context = getContext(this) as common.UIAbilityContext()
(这里的 common 加载自@kit.AbilityKit
)
- 在 UIAbility 文件中
- ExtensionContext:在特定的文件中,
this.context
- ApplicationContext:可以通过 UIAbility、ExtensionAbility、AbilityStage 的
-
Context 的使用场景
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27import { buffer } from '@kit.ArkTS';
import { fileIo, ReadOptions } from '@kit.CoreFileKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
let applicationContext = this.context.getApplicationContext();
// 获取应用文件路径
let filesDir = applicationContext.filesDir;
hilog.info(DOMAIN_NUMBER, TAG, `filePath: ${filesDir}`);
// 文件不存在时创建并打开文件,文件存在时打开文件
let file = fileIo.openSync(filesDir + '/test.txt', fileIo.OpenMode.READ_WRITE | fileIo.OpenMode.CREATE);
// 写入一段内容至文件
let writeLen = fileIo.writeSync(file.fd, "Try to write str.");
hilog.info(DOMAIN_NUMBER, TAG, `The length of str is: ${writeLen}`);
// 创建一个大小为 1024 字节的 ArrayBuffer 对象,用于存储从文件中读取的数据
let arrayBuffer = new ArrayBuffer(1024);
// 设置读取的偏移量和长度
let readOptions: ReadOptions = {
offset: 0,
length: arrayBuffer.byteLength
};
// 读取文件内容到 ArrayBuffer 对象中,并返回实际读取的字节数
let readLen = fileIo.readSync(file.fd, arrayBuffer, readOptions);
// 将 ArrayBuffer 对象转换为 Buffer 对象,并转换为字符串输出
let buf = buffer.from(arrayBuffer, 0, readLen);
hilog.info(DOMAIN_NUMBER, TAG, `the content of file: ${buf.toString()}`);
// 关闭文件
fileIo.closeSync(file);ApplicationContext:应用级的文件路径。该路径用于存放应用全局信息,路径下的文件会跟随应用的卸载而删除。
AbilityStageContext、UIAbilityContext、ExtensionContext:Module 级的文件路径。该路径用于存放对应 Context 所在 Module 相关信息,路径下的文件会跟随 HAP/HSP 的卸载而删除。
不同级别 Context 获取的应用文件路径说明
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26// EntryAbility.ets
import { UIAbility, contextConstant, AbilityConstant, Want } from '@kit.AbilityKit';
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) {
// 存储普通信息前,切换到EL1设备级加密
this.context.area = contextConstant.AreaMode.EL1; // 切换area
// 存储普通信息
// 存储敏感信息前,切换到EL2用户级加密
this.context.area = contextConstant.AreaMode.EL2; // 切换area
// 存储敏感信息
// 存储敏感信息前,切换到EL3用户级加密
this.context.area = contextConstant.AreaMode.EL3; // 切换area
// 存储敏感信息
// 存储敏感信息前,切换到EL4用户级加密
this.context.area = contextConstant.AreaMode.EL4; // 切换area
// 存储敏感信息
// 存储敏感信息前,切换到EL5应用级加密
this.context.area = contextConstant.AreaMode.EL5; // 切换area
// 存储敏感信息
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21// Index.ets
import { contextConstant, common } from '@kit.AbilityKit';
import { promptAction } from '@kit.ArkUI';
// 存储普通信息前,切换到 EL1 设备级加密
if (this.context.area === contextConstant.AreaMode.EL2) { // 获取area
this.context.area = contextConstant.AreaMode.EL1; // 修改area
promptAction.showToast({
message: 'SwitchToEL1'
});
}
// 存储普通信息
// 存储敏感信息前,切换到 EL2 用户级加密
if (this.context.area === contextConstant.AreaMode.EL1) { // 获取area
this.context.area = contextConstant.AreaMode.EL2; // 修改area
promptAction.showToast({
message: 'SwitchToEL2'
});
}
// 存储敏感信息注:应用文件加密是一种保护数据安全的方法,可以使得文件在未经授权访问的情况下得到保护。鸿蒙开发中,通过 contextConstant.AreaMode 实现分区权限控制,从 EL1~EL5 密级依次增强。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17import { common, application } from '@kit.AbilityKit';
import { promptAction } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';
let moduleName2: string = 'entry';
application.createModuleContext(this.context, moduleName2)
.then((data: common.Context) => {
console.info(`CreateModuleContext success, data: ${JSON.stringify(data)}`);
if (data !== null) {
promptAction.showToast({
message: ('成功获取Context')
});
}
})
.catch((err: BusinessError) => {
console.error(`CreateModuleContext failed, err code:${err.code}, err msg: ${err.message}`);
});1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83import { AbilityConstant, AbilityLifecycleCallback, UIAbility, Want } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit';
const TAG: string = '[LifecycleAbility]';
const DOMAIN_NUMBER: number = 0xFF00;
export default class LifecycleAbility extends UIAbility {
// 定义生命周期ID
lifecycleId: number = -1;
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
// 定义生命周期回调对象
let abilityLifecycleCallback: AbilityLifecycleCallback = {
// 当 UIAbility 创建时被调用
onAbilityCreate(uiAbility) {
hilog.info(DOMAIN_NUMBER, TAG, `onAbilityCreate uiAbility.launchWant: ${JSON.stringify(uiAbility.launchWant)}`);
},
// 当窗口创建时被调用
onWindowStageCreate(uiAbility, windowStage: window.WindowStage) {
hilog.info(DOMAIN_NUMBER, TAG, `onWindowStageCreate uiAbility.launchWant: ${JSON.stringify(uiAbility.launchWant)}`);
hilog.info(DOMAIN_NUMBER, TAG, `onWindowStageCreate windowStage: ${JSON.stringify(windowStage)}`);
},
// 当窗口处于活动状态时被调用
onWindowStageActive(uiAbility, windowStage: window.WindowStage) {
hilog.info(DOMAIN_NUMBER, TAG, `onWindowStageActive uiAbility.launchWant: ${JSON.stringify(uiAbility.launchWant)}`);
hilog.info(DOMAIN_NUMBER, TAG, `onWindowStageActive windowStage: ${JSON.stringify(windowStage)}`);
},
// 当窗口处于非活动状态时被调用
onWindowStageInactive(uiAbility, windowStage: window.WindowStage) {
hilog.info(DOMAIN_NUMBER, TAG, `onWindowStageInactive uiAbility.launchWant: ${JSON.stringify(uiAbility.launchWant)}`);
hilog.info(DOMAIN_NUMBER, TAG, `onWindowStageInactive windowStage: ${JSON.stringify(windowStage)}`);
},
// 当窗口被销毁时被调用
onWindowStageDestroy(uiAbility, windowStage: window.WindowStage) {
hilog.info(DOMAIN_NUMBER, TAG, `onWindowStageDestroy uiAbility.launchWant: ${JSON.stringify(uiAbility.launchWant)}`);
hilog.info(DOMAIN_NUMBER, TAG, `onWindowStageDestroy windowStage: ${JSON.stringify(windowStage)}`);
},
// 当 UIAbility 被销毁时被调用
onAbilityDestroy(uiAbility) {
hilog.info(DOMAIN_NUMBER, TAG, `onAbilityDestroy uiAbility.launchWant: ${JSON.stringify(uiAbility.launchWant)}`);
},
// 当 UIAbility 从后台转到前台时触发回调
onAbilityForeground(uiAbility) {
hilog.info(DOMAIN_NUMBER, TAG, `onAbilityForeground uiAbility.launchWant: ${JSON.stringify(uiAbility.launchWant)}`);
},
// 当 UIAbility 从前台转到后台时触发回调
onAbilityBackground(uiAbility) {
hilog.info(DOMAIN_NUMBER, TAG, `onAbilityBackground uiAbility.launchWant: ${JSON.stringify(uiAbility.launchWant)}`);
},
// 当 UIAbility 迁移时被调用
onAbilityContinue(uiAbility) {
hilog.info(DOMAIN_NUMBER, TAG, `onAbilityContinue uiAbility.launchWant: ${JSON.stringify(uiAbility.launchWant)}`);
}
};
// 获取应用上下文
let applicationContext = this.context.getApplicationContext();
try {
// 注册应用内生命周期回调
this.lifecycleId = applicationContext.on('abilityLifecycle', abilityLifecycleCallback); // 返回一个监听生命周期的 ID,会自增 1。当超过监听上限 2^63-1 时返回 -1。
} catch (err) {
let code = (err as BusinessError).code;
let message = (err as BusinessError).message;
hilog.error(DOMAIN_NUMBER, TAG, `Failed to register applicationContext. Code is ${code}, message is ${message}`);
}
hilog.info(DOMAIN_NUMBER, TAG, `register callback number: ${this.lifecycleId}`);
}
//...
onDestroy(): void {
// 获取应用上下文
let applicationContext = this.context.getApplicationContext();
try {
// 取消应用内生命周期回调
applicationContext.off('abilityLifecycle', this.lifecycleId);
} catch (err) {
let code = (err as BusinessError).code;
let message = (err as BusinessError).message;
hilog.error(DOMAIN_NUMBER, TAG, `Failed to unregister applicationContext. Code is ${code}, message is ${message}`);
}
}
}注:该示例适用于应用内的 DFX 统计场景,如统计对应页面停留时间和访问频率等信息。
Want 参数(略看)
-
解释:Want 是一种对象,用于在应用组件之间传递信息。Want 分为显式 Want 和隐式 Want,
- 显式 Want:在启动目标应用组件时,调用方传入的 want 参数中指定了 abilityName 和 bundleName,适用于有明确处理请求的对象时。API 12+,不推荐使用显式 Want 拉起其他应用。
- 隐式 Want:在启动目标应用组件时,调用方传入的 want 参数中未指定 abilityName,适用于处理请求的对象不明确时,只关注能提供指定能力的应用组件。对于隐式 Want,根据系统中待匹配应用组件的匹配情况不同,会出现以下三种情况:启动失败(未匹配到);直接启动该应用组件(匹配到一个);弹出选择框让用户选择(匹配到多个)。
-
显式 Want 匹配规则(左) 隐式 Want 匹配规则(右)
-
action/entities
注:由于 action/entity 被泛化使用,现已不推荐使用。
- action:表示调用方要执行的通用操作(如查看、分享、应用详情)。
- entities:表示目标应用组件的类别信息(如浏览器、视频播放器)。
组件启动规则
同设备组件启动规则(左) 分布式跨设备组件启动规则(右)
- 跨应用启动组件,需校验目标组件是否可以被其他应用调用(目标组件 module.json5 配置文件中对应
abilities
下的exported
字段)。 - 位于后台的 UIAbility 应用,启动组件需校验 BACKGROUND 权限(ohos.permission.START_ABILITIES_FROM_BACKGROUND(该权限仅系统应用可申请))。
- 跨设备使用 startAbilityByCall 接口,需校验分布式权限(ohos.permission.DISTRIBUTED_DATASYNC)。
应用启动框架
启动框架执行时机
-
解释:AppStartup 应用启动框架提供了一种简单高效的应用启动方式,可以支持任务的异步启动,加快应用启动速度。同时,通过在一个配置文件中统一设置多个启动任务的执行顺序以及依赖关系,让执行启动任务的代码变得更加简洁清晰、容易维护。启动框架支持以自动模式或手动模式执行启动任务,默认采用自动模式。该启动框架只支持在 entry 类型的 Module 下的 UIAbility 中使用。
- 自动模式:当 AbilityStage 组件容器完成创建后,自动执行启动任务。
- 手动模式:在 UIAbility 完成创建后手动调用,来执行启动任务。手动模式适用于某些使用频率不高的模块,不需要应用最开始启动时就进行初始化。
-
开发流程
-
定义启动框架配置文件(应用主模块(即 entry 类型的 Module)下)
-
ets/startup 路径下,依次创建若干个启动任务文件、以及一个公共的启动参数配置文件。文件名称必须确保唯一性。
-
resources/base/profile 路径下,新建启动框架配置文件,添加所有启动任务以及启动参数配置文件的相关信息,如 startup_config.json。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23{
"startupTasks": [ //【必须,对象数组】启动任务配置信息,标签说明详见下表。
{
"name": "StartupTask_001", //【必须,字符串】启动任务对应的类名。
"srcEntry": "./ets/startup/StartupTask_001.ets", //【必须,字符串】启动任务对应的文件路径。
"dependencies": [ // 启动任务依赖的其他启动任务的类名数组。【默认值为空,字符串数组】
"StartupTask_002",
"StartupTask_003"
],
"runOnThread": "taskPool", // 执行初始化所在的线程。mainThread:在主线程中执行【默认值】;taskPool:在异步线程中执行。
"waitOnMainThread": false // 主线程是否需要等待启动框架执行。当 runOnThread 取值为 taskPool 时,该字段生效。true:主线程等待启动框架执行完之后,才会加载应用首页【默认值】;false:主线程不等待启动任务执行。
},
// ...
{
"name": "StartupTask_006",
"srcEntry": "./ets/startup/StartupTask_006.ets",
"runOnThread": "mainThread",
"waitOnMainThread": false,
"excludeFromAutoStart": true // 是否排除自动模式。true:手动模式;false:自动模式【默认值】。
}
],
"configEntry": "./ets/startup/StartupConfig.ets" //【必须,字符串】启动参数配置文件所在路径。
} -
module.json5 的
appStartup
字段中,添加启动框架配置文件的索引。1
2
3
4
5
6
7
8
9{
"module": {
"name": "entry",
"type": "entry",
// ...
"appStartup": "$profile:startup_config", // 启动框架的配置文件
// ...
}
}
-
-
设置启动参数(启动参数配置文件中),如超时时间和启动任务的监听器等参数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27import { StartupConfig, StartupConfigEntry, StartupListener } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { BusinessError } from '@kit.BasicServicesKit';
// 使用 StartupConfigEntry 接口实现启动框架公共参数的配置,包括超时时间和启动任务的监听器等参数,其中需要用到如下接口:
// StartupConfig:用于设置任务超时时间和启动框架的监听器。
// StartupListener:用于监听启动任务是否执行成功。
export default class MyStartupConfigEntry extends StartupConfigEntry {
onConfig() {
hilog.info(0x0000, 'testTag', `onConfig`);
let onCompletedCallback = (error: BusinessError<void>) => {
hilog.info(0x0000, 'testTag', `onCompletedCallback`);
if (error) {
hilog.info(0x0000, 'testTag', 'onCompletedCallback: %{public}d, message: %{public}s', error.code, error.message);
} else {
hilog.info(0x0000, 'testTag', `onCompletedCallback: success.`);
}
};
let startupListener: StartupListener = {
'onCompleted': onCompletedCallback
};
let config: StartupConfig = {
'timeoutMs': 10000, //
'startupListener': startupListener
};
return config;
}
} -
为每个待初始化组件添加启动任务(启动任务文件中)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23import { StartupTask, common } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
// 通过实现 StartupTask 来添加启动任务。
// init:启动任务初始化。当该任务依赖的启动任务全部执行完毕,即 onDependencyCompleted 完成调用后,才会执行 init 方法对该任务进行初始化。
// onDependencyCompleted:当前任务依赖的启动任务执行完成时,调用该方法。
// 由于 StartupTask 采用了 Sendable 协议,在继承该接口时,必须添加 Sendable 注解。
@Sendable
export default class StartupTask_001 extends StartupTask {
constructor() {
super();
}
async init(context: common.AbilityStageContext) {
hilog.info(0x0000, 'testTag', 'StartupTask_001 init.');
return 'StartupTask_001';
}
onDependencyCompleted(dependence: string, result: Object): void {
hilog.info(0x0000, 'testTag', 'StartupTask_001 onDependencyCompleted, dependence: %{public}s, result: %{public}s',
dependence, JSON.stringify(result));
}
}
-
-
执行手动任务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26import { AbilityConstant, UIAbility, Want, startupManager } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { BusinessError } from '@kit.BasicServicesKit';
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate');
let startParams = ["StartupTask_005", "StartupTask_006"];
try {
startupManager.run(startParams).then(() => { // startupManager.run 的参数是表明准备执行的启动任务所实现的 StartupTask 接口的类名称数组。
console.log('StartupTest startupManager run then, startParams = ');
}).catch((error: BusinessError) => {
console.info('StartupTest promise catch error, error = ' + JSON.stringify(error));
console.info('StartupTest promise catch error, startParams = '
+ JSON.stringify(startParams));
})
} catch (error) {
let errMsg = JSON.stringify(error);
let errCode: number = error.code;
console.log('Startup catch error , errCode= ' + errCode);
console.log('Startup catch error ,error= ' + errMsg);
}
}
// ...
}1
2
3
4
5import { startupManager } from '@kit.AbilityKit';
if (!startupManager.isStartupTaskInitialized("StartupTask_006")) { // 判断是否已经完成初始化
startupManager.run(this.startParams) // startupManager.run 的参数是表明准备执行的启动任务所实现的 StartupTask 接口的类名称数组。
}
订阅系统环境变量的变化
环境变量:在应用程序运行期间,终端设备的系统设置(例如系统的语言环境、屏幕方向等),具体有哪些环境变量见 Configuration。
1 | import { common, EnvironmentCallback, Configuration } from '@kit.AbilityKit'; |
注:DevEco Studio 默认工程中未自动生成 AbilityStage,需自行创建。
1 | import { AbilityConstant, Configuration, UIAbility, Want } from '@kit.AbilityKit'; |
应用间跳转(略看)
应用跳转是指从一个应用跳转至另外一个应用,传递相应的数据、执行特定的功能,分为以下两种类型,
- 拉起指定应用:拉起方应用明确指定跳转的目标应用,来实现应用跳转。这种跳转又分为指定应用链接(推荐)、指定 Ability(不推荐,即显示 Want)。指定应用链接是指通过 openLink 或 startAbility 接口来指定应用链接,拉起目标应用页面。通过指定应用链接拉起指定应用(
scheme://host[:port]/path
),按照应用链接的scheme
以及校验机制的不同,可以分为 Deep Linking 与 App Linking 两种方式。- Deep Linking(推荐):支持开发者定义任意形式的 scheme;于缺乏域名校验机制。
- App Linking:限定了 scheme 必须为 https;增加域名校验机制,可以从已匹配到的应用中筛选过滤出目标应用。
- 拉起指定类型的应用:拉起方应用通过指定应用类型,拉起垂类应用面板,由用户选择指定应用。
注意:系统应用的拉起方式包括使用系统 Picker 组件和使用特定接口。
了解进程模型
进程模型示意图
-
进程是系统进行资源分配的基本单位,进程模型可描述为,
- 应用中(同一 Bundle 名称)的所有 UIAbility 均是运行在同一个独立进程(主进程)中;
- 应用中(同一 Bundle 名称)的所有同一类型 ExtensionAbility 均是运行在一个独立进程中;
- WebView 拥有独立的渲染进程。
-
仅 2in1 设备支持将 HAP 或 UIAbility 设置为独立进程,
-
某个 HAP 运行在独立进程中,
1
2
3
4{
// ...
"isolationMode": "nonisolationFirst" // 标识当前 Module 的多进程配置项。isolationOnly(只在独立进程中运行)或者 isolationFirst(优先在独立进程中运行)。
} -
某个 UIAbility 运行在独立进程中,
1
2
3
4
5
6
7
8
9
10{
"module": {
"abilities": [
{
// ...
"isolationProcess": true // 标识组件能否运行在独立的进程中。1️⃣ true:表示能运行在独立的进程中。2️⃣ false:表示不能运行在独立的进程中【默认值】。
},
],
}
}注:除了上述配置外,还需要在 AbilityStage 文件中的
onNewProcessRequest(want: Want): string
回调中返回一个唯一的进程标识符。
-
-
注意事项
- 仅系统应用支持构建 ServiceExtensionAbility 和 DataShareExtensionAbility。
- 查看所有正在运行的进程信息:
hdc shell & ps -ef
。 - 应用间和应用内存在的多个进程的通信机制:公共事件机制。
了解线程模型
线程模型示意图
-
线程是操作系统进行运算调度的基本单位,共享进程的资源。一个进程可以包含多个线程,主要分为以下三类,
线程 功能 主线程 执行 UI 绘制 管理主线程、其他线程的 ArkTS 引擎实例 分发交互事件 处理应用代码的回调,包括事件处理和生命周期管理 接收 TaskPool 以及 Worker 线程发送的消息。 TaskPool Worker 线程 用于执行耗时操作(推荐) 支持设置调度优先级、负载均衡等功能 自行管理线程数量,其生命周期由 TaskPool 统一管理 Worker 线程 用于执行耗时操作支持线程间通信 生命周期由开发者自行维护 -
注意事项
- 查看所有正在运行的进程信息:
hdc shell & ps -p <pid> -T
(为需要指定的应用进程的进程 ID。)。 - 线程内通信:EventHub。
- 查看所有正在运行的进程信息:
应用配置文件
二、ArkTS(方舟编程语言)- 应用框架
基础类库(略看)
- XML 生成、解析、转换:@ohos.xml
- Buffer:@ohos.buffer
- URL 字符串解析:@ohos.url
- 高精度浮点计算:@arkts.math.Decimal
- 容器类库:作为纯数据存储容器,保证了数据的高效访问,提升了应用的性能。容器类库提供的两种容器都是非多线程安全的。
- 线性容器:实现能按顺序访问的数据结构,其底层主要通过数组实现。
- ArrayList:动态数组,占用一片连续的内存空间。需要频繁读取元素时推荐使用。
Vector:动态数组,占用一片连续的内存空间。该类型已不再维护,推荐使用 ArrayList。- List:单向链表,占用的空间可以不连续。需要频繁的插入删除元素且需要使用单向链表时推荐使用。
- LinkedList:双向链表,占用的空间可以不连续。需要频繁的插入删除元素且需要使用双向链表时推荐使用。
- Deque:双端队列,可以从容器头尾进行进出元素操作,占用一片连续的内存空间。需要频繁访问、操作头尾元素时推荐使用。
- Queue:队列,从容器尾部插入元素,从容器头部弹出元素,占用一片连续的内存空间。一般符合先进先出的场景可以使用。
- Stack:栈,只能从容器的一端进行插入删除操作,占用一片连续的内存空间。一般符合先进后出的场景可以使用。
- 非线性容器:实现快速查找的数据结构,其底层通过 hash 或者红黑树实现。
- HashMap:存储具有关联关系的键值对集合,存储元素中键唯一,依据键的 hash 值确定存储位置。访问速度较快,但不能自定义排序。需要快速存取、插入删除键值对数据时推荐使用。
- HashSet:存储一系列值的集合,存储元素中值唯一,依据值的 hash 确定存储位置。允许放入 null 值,但不能自定义排序。需要不重复的集合或需要去重某个集合时可以使用。
- TreeMap:存储具有关联关系的键值对集合,存储元素中键唯一,允许用户自定义排序方法。一般需要按序存储键值对的场景可以使用。
- TreeSet:存储一系列值的集合,存储元素中值唯一,允许用户自定义排序方法,但不建议放入 null 值。一般需要按序存储集合的场景可以使用。
- LightWeightMap:存储具有关联关系的键值对集合,存储元素中键唯一,底层采用更加轻量级的结构,空间占用小。需要存取键值对数据且内存不充足时推荐使用。
- LightWeightSet:存储一系列值的集合,存储元素中值唯一,底层采用更加轻量级的结构,空间占用小。需要不重复的集合或需要去重某个集合时推荐使用。
- PlainArray:存储具有关联关系的键值对集合,存储元素中键唯一,底层与 LightWeightMap 一样采用更加轻量级的结构,且键固定为 number 类型。需要存储键为 number 类型的键值对时可以使用。
- 线性容器:实现能按顺序访问的数据结构,其底层主要通过数组实现。
并发编程
并发是指在同一时间内,存在多个任务同时执行的情况。为了提升应用的响应速度与帧率,避免耗时任务对 UI 主线程的影响,ArkTS 提供了异步并发和多线程并发两种处理策略。
异步并发
ArkTS 通过 Promise 和 async/await 提供异步并发能力,可使用errorManager.on(‘error’)接口监听未被处理的 Promise reject(触发 unhandledrejection 事件)。
多线程并发
并发模型是用来实现不同应用场景中并发任务的编程模型,常见的并发模型分为基于内存共享的并发模型和基于消息通信的并发模型。其中 Actor 并发模型是基于消息通信的并发模型的典型代表。当前ArkTS 提供了 TaskPool 和 Worker 两种并发能力,二者都基于 Actor 并发模型实现。
- 内存共享并发模型:多线程同时执行任务,这些线程依赖同一内存并且都有权限访问,线程访问内存前需要抢占并锁定内存的使用权,没有抢占到内存的线程需要等待其他线程释放使用权再执行。
- 消息通信并发模型(Actor):每一个线程都是一个独立 Actor,每个 Actor 有自己独立的内存,Actor 之间通过消息传递机制触发对方 Actor 的行为,不同 Actor 之间不能直接访问对方的内存空间。
TaskPool
-
解释:任务池(TaskPool)为应用程序提供一个多线程的运行环境,降低整体资源的消耗、提高系统的整体性能,且无需关心线程实例的生命周期。
-
执行流程:在宿主线程封装任务 --> 封装的任务交给任务队列 --> 系统选择合适的工作线程(默认启动一个,会根据实际情况扩容和减少),进行任务的分发与执行,再将结果返回给宿主线程。
1
2
3
4
5
6
7
8import { taskpool } from "@kit.ArkTS"
@Concurrent
function concurrentlyExecutedFunctionName(arg1: Type1, arg2: Type2, /* ... */, argN: TypeN): TypeR {
// 并行执行的逻辑
}
const task: taskpool.Task = new taskpool.Task(concurrentlyExecutedFunctionName, arg1, arg2, /* ... */, argN); // 封装任务1
taskpool.execute(task); // 将封装的任务交给任务队列
1
2
3
4taskpool.execute(task)
.then((data: Object) => { // data 的类型就是并发函数的返回值
// 成功获取并发线程中任务 task 的 concurrentlyExecutedFunctionName 函数的计算结果
}) -
@Concurrent
:该装饰器装饰的函数称之为并发函数,允许被封装为任务(taskpool.Task)。该装饰器仅支持 Stage 模型,仅支持 .ets 文件。- 可装饰函数:仅支持装饰
**async**
函数和普通函数。 - 函数参数:参数需满足序列化支持的类型。
- 函数体约束
- 函数中禁止使用闭包变量。
- 函数中不能使用定义在同一文件的自定义类或函数,否则会被认为是闭包而报错。
- 函数返回值
- 返回值类型为线程间通信对象类型。
- 返回值是带方法的实例对象,对应的类必须使用装饰器
@Sendable
装饰器装饰。
- 可装饰函数:仅支持装饰
-
使用说明
- 任务在 TaskPool 工作线程的执行耗时不能超过 3 分钟(不包含 Promise 和 async/await 异步调用的耗时),否则会被强制退出。
- 由于不同线程中上下文对象是不同的,因此 TaskPool 工作线程只能使用线程安全的库。
- 序列化传输的数据量大小限制为 16MB。
- 不支持在 TaskPool 工作线程中使用 AppStorage。
- Promise 不支持跨线程传递,如果 TaskPool 返回 pending 或 rejected 状态的 Promise,会返回失败;对于 fulfilled 状态的 Promise,TaskPool 会解析返回的结果,如果结果可以跨线程传递,则返回成功。
- 并发异步函数中如果使用 Promise,建议搭配 await 使用。这样 TaskPool 会捕获 Promise 中可能发生的异常。
Worker
-
解释:Worker 主要作用是为应用程序提供一个多线程的运行环境,可满足应用程序在执行过程中与宿主线程分离,在后台线程中运行一个脚本进行耗时操作,极大避免类似于计算密集型或高延迟的任务阻塞宿主线程的运行。Worker 创建后需要手动管理生命周期。每个 Worker 子线程与宿主线程拥有独立的实例,包含基础设施、对象、代码段等,因此每个 Worker 启动存在一定的内存开销,需要限制 Worker 的子线程数量
-
执行流程:宿主线程(主线程 or Work 线程)通过 Worker 文件创建 Worker 子线程 --> 宿主线程通过
**.postMessage()**
方法向 Worker 子线程发送消息 --> Work 子线程触发**onmessage**
回调,子线程中也可以通过**.postMessage()**
方法向宿主线程发送消息 --> 宿主线程触发**onmessage()**
回调1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53// src/main/ets/pages/Index.ets
import { ErrorEvent, MessageEvents, worker } from '@kit.ArkTS'
@Entry
@Component
struct Index {
@State message: string = 'Hello World';
build() {
RelativeContainer() {
Text(this.message)
.onClick(() => {
// 创建 Worker 对象
// constructor(scriptURL: string, options?: WorkerOptions)
let workerInstance = new worker.ThreadWorker('entry/ets/workers/WorkerTest');
// 注册 onmessage 回调,当宿主线程接收到来自其创建的 Worker 通过 workerPort.postMessage 接口发送的消息时被调用,在宿主线程执行
// onmessage?: (event: MessageEvents) => void
// interface MessageEvents{ data: any /* 线程间传递的数据。 */ }
workerInstance.onmessage = (e: MessageEvents) => {
let data: string = e.data;
console.info("workerInstance onmessage is: ", data);
}
// 注册 onerror 回调,当 Worker 在执行过程中发生异常时被调用,在宿主线程执行
// onerror?: (err: ErrorEvent) => void
// interface ErrorEvent { message: string; filename: string; lineno: number; colno: number; error: Object;}
workerInstance.onerror = (err: ErrorEvent) => {
console.info("workerInstance onerror message is: " + err.message);
}
// 注册 onmessageerror 回调,当 Worker 对象接收到一条无法被序列化的消息时被调用,在宿主线程执行
// onmessageerror?: (event: MessageEvents) => void
workerInstance.onmessageerror = () => {
console.info('workerInstance onmessageerror');
}
// 注册 onexit 回调,当 Worker 销毁时被调用,在宿主线程执行
// onexit?: (code: number) => void
workerInstance.onexit = (e: number) => {
// 当 Worker 正常退出时 code 为 0,异常退出时 code 为 1
console.info("workerInstance onexit code is: ", e);
}
// 向 Worker 线程发送消息
// postMessage(message: Object, options?: PostMessageOptions): void
// interface PostMessageOptions { transfer: Object[] /* ArrayBuffer 数组,用于传递所有权。该数组中不可传入 null。 */ }
// 明确数据传递过程中需要转移所有权对象的类,传递所有权的对象必须是 ArrayBuffer,发送它的上下文中将会变为不可用,仅在接收方可用。
workerInstance.postMessage('1');
})
}
}
}注:构造 Worker 实例对象时传入的 Worker 线程文件的路径(scriptURL)规则为,
- 加载 HAP/HSP 中 Worker:
{moduleName}/ets/{relativePath}
,其中relativePath
是 Worker 线程文件相对于{moduleName}/src/main/ets/
目录的相对路径。 - 加载 HAR 中 Worker:
@{moduleName}/ets/{relativePath}
,其中relativePath
是 Worker 线程文件相对于{moduleName}/src/main/ets/
目录的相对路径。- 当本地 HAR 加载该包内的 Worker 线程文件时,允许使用相对路径加载(创建 Worker 对象所在文件与 Worker 线程文件的相对路径)。
- 当开启 useNormalizedOHMUrl 或 HAR 包会被打包成三方包使用时,HAR 包中使用 Worker 仅支持通过相对路径的加载形式创建。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37// src/main/ets/workers/WorkerTest.ets
import { ErrorEvent, MessageEvents, ThreadWorkerGlobalScope, worker } from '@kit.ArkTS';
const workerPort: ThreadWorkerGlobalScope = worker.workerPort;
/**
* 注册 onmessage 回调,当 Worker 线程收到来自其宿主线程通过 postMessage 接口发送的消息时被调用,在 Worker 线程执行
* onmessage?: (this: ThreadWorkerGlobalScope, ev: MessageEvents) => void
*/
workerPort.onmessage = (event: MessageEvents) => {
let data: string = event.data;
console.info('workerPort onmessage is: ', data);
if(data === "销毁 Worker"){
workerPort.close()
}
// 向主线程发送消息
// postMessage(messageObject: Object, options?: PostMessageOptions): void
workerPort.postMessage('2');
};
/**
* 注册 onmessageerror 回调,当 Worker 对象接收到一条无法被序列化的消息时被调用,在 Worker 线程执行
* onmessageerror?: (this: ThreadWorkerGlobalScope, ev: MessageEvents) => void
*/
workerPort.onmessageerror = (event: MessageEvents) => {
console.info('workerPort onmessageerror');
};
/**
* 注册 onerror 回调,当 Worker 在执行过程中发生异常被调用,在 Worker 线程执行
* onerror?: (ev: ErrorEvent) => void
*/
workerPort.onerror = (event: ErrorEvent) => {
console.info('workerPort onerror err is: ', event.message);
};注:Worker 线程文件需要位于
{moduleName}/src/main/ets/
下,手动创建(可右键自动创建)时,需要配置模块的 build-profile.json5 文件中buildOption.sourceOption.workers
字段为 Worker 线程文件的地址。 - 加载 HAP/HSP 中 Worker:
-
跨 har 包加载 Worker
1
2
3
4
5
6
7
8
9
10
11
12// 在entry模块配置har包的依赖
{
"name": "entry",
"version": "1.0.0",
"description": "Please describe the basic information.",
"main": "",
"author": "",
"license": "",
"dependencies": {
"har": "file:../har"
}
}1
2
3
4
5
6
7
8
9// Index.ets
import { worker } from '@kit.ArkTS';
// 通过 @ 标识路径加载形式,加载 har 中 Worker 线程文件
let workerInstance = new worker.ThreadWorker('@har/ets/workers/worker.ets');
workerInstance.onmessage = () => {
console.info('main thread onmessage');
};
workerInstance.postMessage('hello world'); -
使用说明
- Worker 创建后需要手动管理生命周期,且最多同时运行的 Worker 子线程数量为 64 个。
- 由于不同线程中上下文对象是不同的,因此 TaskPool 工作线程只能使用线程安全的库。
- 序列化传输的数据量大小限制为 16MB。
- 不支持在 TaskPool 工作线程中使用 AppStorage。
- 使用 Worker 模块时,需要在宿主线程中注册 onerror 接口,否则当 Worker 线程出现异常时会发生 jscrash 问题。
- 不支持跨 HAP 使用 Worker 线程文件。引用 HAR/HSP 前,需要先配置对 HAR/HSP 的依赖。
- Worker 空闲时也会一直运行,因此当不需要 Worker 时,可以调用
terminate()
(宿主线程) 接口或close()
(Worker 子线程) 方法主动销毁 Worker。 - Worker 的数量由内存管理策略决定,设定的内存阈值为 1.5GB 和设备物理内存的 60% 中的较小者。
- 对于多级 Worker(即通过父 Worker 创建子 Worker 的机制形成层级线程关系),确保子 Worker 的生命周期始终在父 Worker 生命周期范围内,并在销毁父 Worker 前先销毁所有子 Worker。
TaskPool Vs. Worker(略看)
TaskPool 偏向独立任务维度,Worker 偏向线程的维度,性能方面使用 TaskPool 会优于 Worker,因此大多数场景推荐使用 TaskPool。除非运行时间超过 3 分钟的任务、有关联的一系列同步任务需要使用 Worker。
并发线程间通信
-
解释:线程间通信指的是并发多线程间存在的数据交换行为。
-
线程间通信对象
-
普通对象:深拷贝形式传递,如 Object、Array、Map 等对象。如果普通类实例对象上有方法,则该类必须是被
@Sendable
装饰器所装饰的类,否则类实例对象跨线程传递时,会丢失类实例对象上的方法。补充:普通对象被分配在各自线程的虚拟机本地堆(LocalHeap)。
-
ArrayBuffer:深拷贝(深拷贝 ArrayBuffer 壳 + 新建并复制 ArrayBuffer 对应的 Native 内存)、转移(深拷贝 ArrayBuffer 壳 + 转移 ArrayBuffer 对应的 Native 内存的操作权给接收方)。对于深拷贝的传递方式,对象转移前后 ArrayBuffer 对应 Native 的内存地址改变;对于转移的拷贝方式,对象转移前后 ArrayBuffer 对应 Native 的内存地址不变。转移方式传递 ArrayBuffer 的性能更高,适用于原线程在线程通信后不需要再访问 ArrayBuffer 的场景。
1
2
3
4
5
6
7
8
9
10@Concurrent
function concurrentlyExecutedFuncName(arrayBuffer1: ArrayBuffer, arrayBuffer2: ArrayBuffer) {
}
const arrayBuffer1 = new ArrayBuffer(1024);
const arrayBuffer2 = new ArrayBuffer(1024);
let task: taskpool.Task = new taskpool.Task(concurrentlyExecutedFuncName, arrayBuffer1, arrayBuffer2);
task.setTransferList([]); // arrayBuffer1, arrayBuffer2 都采用深拷贝传递
task.setTransferList([arrayBuffer1]); // arrayBuffer1 采用转移传递,arrayBuffer2 采用深拷贝传递1
2
3
4
5
6
7
8
9
10@Concurrent
function concurrentlyExecutedFuncName(arrayBuffer1: ArrayBuffer, arrayBuffer2: ArrayBuffer) {
}
const arrayBuffer1 = new ArrayBuffer(1024);
const arrayBuffer2 = new ArrayBuffer(1024);
let task: taskpool.Task = new taskpool.Task(concurrentlyExecutedFuncName, arrayBuffer1, arrayBuffer2);
task.setTransferList(); // arrayBuffer1, arrayBuffer2 都采用转移传递
task.setTransferList([arrayBuffer1]); // arrayBuffer1 采用转移传递,arrayBuffer2 采用深拷贝传递补充:ArrayBuffer 是 JavaScript 中用于处理二进制数据的核心对象,其底层会关联一块由 JavaScript 引擎在操作系统的内存空间中分配的 Native 内存 —— 用于存储二进制数据。在 JavaScript 层面,可以认为 ArrayBuffer 是一个被分配在虚拟机本地堆(LocalHeadp)中的 JavaScript 对象壳,而这个壳包含了只想底层 Native 内存的指针、长度等信息。
-
SharedArrayBuffer:跨并发实例间共享,需要使用 Atomics 访问及修改。
补充:SharedArrayBuffer 与 ArrayBuffer 类似,但是其支持跨并发实例间共享。
-
Transferable 对象:共享(C++ 实现能够保证线程安全性,此时共享传输 NativeBinding 对象的 C++ 部分,同时深拷贝 ArrayBuffer 壳)、转移(C++ 实现包含了数据,且无法保证线程安全性,此时转移传输 NativeBinding 对象的 C++ 部分,原对象需要移除对此对象的绑定关系,同时深拷贝 ArrayBuffer 壳)。
补充:Transferable 对象(又称 NativeBinding 对象)指的是一个绑定了 C++ 对象,且主体功能由 C++ 提供的 JavaScript 对象,其 JS 对象壳被分配在虚拟机本地堆(LocalHeap)。可共享传输的 Transferable 对象包括 Context 等,可转移传输的 Transferable 对象包括 PixelMap 等。
-
[Sendable 对象]
-
-
并发线程间通信场景
- 使用 TaskPool 执行独立的耗时任务,即在任务执行完毕后将结果返回给宿主线程,没有上下文依赖,如图片加载。
new taskpool.Task
- 使用 TaskPool 执行多个耗时任务,如将原始数据拆分成多个列表,并将每个子列表分配给一个独立的 Task 进行执行,并且等待全部执行完毕后拼成完整的数据,从而节省处理时间,提升用户体验。
new taskpool.TaskGroup()
- TaskPool 任务与宿主线程通信,如一个 Task 不仅需要返回最后的执行结果,而且需要定时通知宿主线程状态、数据的变化,或者需要分段返回数量级较大的数据(比如从数据库中读取大量数据)。
new taskpool.Task(func, ...args).onReceiveData(cb)
&taskpool.Task.sendData(data)
- Worker 和宿主线程的即时消息通信,一个 Worker 中可能会执行多个不同的任务,每个任务执行的时长或者返回的结果可能都不相同,宿主线程需要根据情况调用 Worker 中的不同方法,Worker 则需要及时地将结果返回给宿主线程。
- Worker 同步调用宿主线程的接口,宿主线程中通过
new worker.ThreadWork(scriptURL).registerGlobalCallObject("instanceName", instance)
在 Worker 上注册接口,Worker 线程中通过worker.workerPort.callGlobalCallObjectMethod("instanceName", "methodName", timeout, ...args)
访问接口上的方法。
- 使用 TaskPool 执行独立的耗时任务,即在任务执行完毕后将结果返回给宿主线程,没有上下文依赖,如图片加载。
Sendable
-
解释:Sendable 对象是跨线程过程中可共享的对象,在运行时类型固定。为了避免多个并发实例同时尝试更新 Sendable 对象导致的数据竞争,可以通过异步锁机制或对象冻结的方式解决。Sendable 对象在跨线程传输(TaskPool 或 Worker)的过程中可以有效提升传输效率。
-
Sendable 实现原理 - 共享堆
- Sendable 对象存储于共享堆(SharedHeap)- 进程级别。
- 并发实例的数据存储于本地堆(LocalHeap)- 线程级别。
- 共享堆(SharedHeap)是进程级别的堆空间,可以被所有并发实例访问。
- 判断 Sendable 对象是否存活,取决于所有并发实例的对象是否存在对此 Sendable 对象的引用。
-
Sendable 相关概念
-
Sendable 协议:符合 Sendable 协议的数据(简称 Sendable 对象)可以在 ArkTS 并发实例间(包括 UI 主线程、TaskPool 线程、Worker 线程)传递。Sendable 对象默认引用传递,也可以拷贝传递。
-
ISendable:ISendable 是所有 Sendable 类型(除了 null 和 undefined)的父类型。@Sendable 是 implement ISendable 的语法糖。
注:Sendable 对象不支持增加、删除属性,允许修改属性;不支持修改方法。
- @Sendable: 可以装饰 class、function(要求项目的 build-profile.json5 中配置 compatibleSdkVersionStage": “beta3”)、type。
- ISendable 可以被 interface 继承。
-
Sendable 支持的数据类型
注:JS 内置对象、对象字面量、数组字面量在并发实例间的传递遵循结构化克隆算法,跨线程是拷贝传递,不是 Sendable 类型。
注:Sendable 对象与 TS/JS 互操作时,不支持对 Sendable 对象的布局进行修改(增删属性,修改属性类型),不支持许多 NAPI 接口。
注:为了让 Sendable 对象可被观察,可以结合 makeObserved 接口使用。
- 所有的 ArkTS 基本数据类型:boolean, number, string, bigint, null, undefined。
- 容器类型(@arkts.collections:collections.Array、collections.Map、collections.Set 等):又称共享容器,是一种在并发任务间共享传输的容器类,可以用于并发场景下的高性能数据传递。ArkTS 共享容器并不是线程安全的,内部使用了 fail-fast(快速失败)机制,即当检测多个并发实例同时对容器进行结构性改变时,会触发异常。因此建议使用异步锁。
- 异步锁对象(@arkts.utils:ArkTSUtils.locks.AsyncLock)
- @Sendable class、@Sendable function、ISendable Interface、接入 Sendable 的系统对象、元素为 Sendable 类型的联合类型数据。
-
-
Sendable 使用规则与约束
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57import { collections, lang } from '@kit.ArkTS';
// 1. Sendable class 只能继承 Sendable class,且只能被 Sendable class 继承
@Sendable
class Base {
// arr1:number[] = []; // 2. Sendable class/interface 的成员必须是 Sendable 支持的数据类型
// count!:number; // 3. Sendable class/interface 的成员变量不支持 ! 断言
// arr1: collections.Array<Array<number>> = new collections.Array<Array<number>>(); // 4. Sendable 数据结构不能持有非 Sendable 数据
arr2: collections.Array<number> = new collections.Array<number>();
}
@Sendable
class Derived extends Base {
}
interface I extends lang.ISendable {}
{
@Sendable
class A {
}
@Sendable
class B {
u: Derived = new Derived();
// v: A = new A(); // 5. Sendable class/interface 的成员的值不能是非定义在顶层(top level)的类实例或方法
foo() {
return new Derived();
}
bar() {
// return new A(); // 6. Sendable class/interface 的成员方法的返回值不能是非定义在顶层(top level)的类实例或方法
}
}
}
// @ObservedV2 // 7. Sendable class/function 不能使用其他装饰器
@Sendable
class C {
}
// const arr: collections.Array<number> = [1, 2, 3]; // 8. 不能使用对象字面量/数组字面量初始化 Sendable 数据
@Sendable
function testA(): void {
}
@Sendable
type SendableFunc = () => void; // 定义 Sendable 函数类型
const testB: SendableFunc = testA;
// const testC: SendableFunc = () => { } // 9. 箭头函数不是 Sendable 类型,不能用于共享
// @Sendable
// type SendableType = number; // 10. Sendable 只能装饰表示函数类型的类型 -
异步锁:ArkTS 提供 AsyncLock 对象,用于解决多线程并发任务的数据竞争问题,同时可以用于保证单线程内的异步任务时序一致性。
- 为了保证时序正确,使用异步锁的方法需要标记为 async,调用方需要使用 await。
- AsyncLock 对象支持跨线程引用传递。
- AsyncLock 对象常被类对象持有。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30import { ArkTSUtils, taskpool } from "@kit.ArkTS"
@Sendable
export class Counter {
private _count: number = 0; // 需要保护的数据
private _lock: ArkTSUtils.locks.AsyncLock = new ArkTSUtils.locks.AsyncLock(); // 类持有的异步锁对象
public async getCount(): Promise<number> {
return this._lock.lockAsync(() => {
return this._count
})
}
public async setCount(val: number) {
this._lock.lockAsync(() => {
this._count = val;
})
}
}
@Concurrent
export async function getCounterValue(counter: Counter) {
return counter.getCount();
}
function createTaskAndExecute() {
const counter = new Counter();
const task: taskpool.Task = new taskpool.Task(getCounterValue, counter);
taskpool.execute(task);
} -
ASON:ASON.stringify 和 ASON.parse 方法用于 Sendable 对象的序列化和反序列化。
1
2
3
4import { ArkTSUtils, collections } from '@kit.ArkTS'
ArkTSUtils.ASON.stringify(new collections.Array<number>(1, 2, 3));
ArkTSUtils.ASON.parse("{}"); -
共享模块(略看):在同一进程内只加载一次,可在不同线程间共享的模块。在模块顶层使用 “use shared” 标记。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23import { ArkTSUtils } from "@kit.ArkTS"
"use shared"
@Sendable
class Counter {
private _count: number = 0; // 需要保护的数据
private _lock: ArkTSUtils.locks.AsyncLock = new ArkTSUtils.locks.AsyncLock(); // 类持有的异步锁对象
public async getCount(): Promise<number> {
return this._lock.lockAsync(() => {
return this._count
})
}
public async setCount(val: number) {
this._lock.lockAsync(() => {
this._count = val;
})
}
}
export const counter = new Counter(); // 由于是共享模块,当前文件导出后的 counter 在全局唯一1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16import {counter} from "./demo1"
import { taskpool } from "@kit.ArkTS";
@Concurrent
export async function getCounterValue() {
return counter.getCount();
}
function createTaskAndExecute() {
const task: taskpool.Task = new taskpool.Task(getCounterValue, counter);
taskpool.execute(task);
}
createTaskAndExecute();
createTaskAndExecute();
createTaskAndExecute(); // 此时多个线程访问的都是同一个 Counter 类的实例 counter,因为 ./demo1 是共享模块 -
对象冻结:冻结后的 Sendable 对象变成只读对象,不能增删改属性,因此在多个并发实例间访问均不需要加锁。使用
Object.freeze(obj)
冻结对象。1
2
3
4
5
6
7
8
9
10
11import { freezeObj } from "./demo4"
@Sendable
export class GlobalConfig {
bundleName: string = '';
abilityName: string = '';
init() {
freezeObj(this);
}
}1
2
3export function freezeObj(obj:any){
Object.freeze(obj);
}
应用多线程开发
运行时
ArkTS 运行时是 HarmonyOS 上应用默认语言运行时,运行着 ArkTS、TS、JS 语言的字节码和相关标准库,支持解释器、AOT 和 JIT 高效执行方式,并提供完善的跨语言调用接口实现 Node-API,支持多语言混合开发。
GC(略看)
ArkTS 模块化
-
可加载模块类型
-
ets/ts/js:ECMAScript 模块规范、CommonJS 模块规范。
-
json:仅支持 default 导入。
1
import data from './example.json'
-
Native:与加载 ets/ts/js 语法规格一致,但不支持在 CommonJS 模块中导入,也不支持 native 模块导出和导入同时使用命名空间(即
export * from 'xxx'
&import * as add from 'yyy'
)。同时,不建议通过import * as xxx from 'xxx'
方式进行导入。1
2import { add } from 'libentry.so'
add(2, 3)
-
-
模块加载规范
- ECMAScript 模块规范:export、import。
- CommonJS 模块规范:require、module.exports、exports。
- 互相引用支持:ES Module 导入支持 CommonJS 导出;CommonJS 导入不支持 ES Module 导出。
-
模块加载方式(扩展)
-
动态加载:支持条件导入、按需导入;分为动态加载常量表达式、动态加载变量表达式。
注 1:动态加载使用模块名时,依赖方需要在 oh-package.json5 中的
dependencies
配置项进行别名配置,此时导入的是该模块的入口文件,一般是index.ets/index.ts
。注 2:动态加载变量表达式时,依赖方需要在 build-profile.json5 中的
buildOption.arkOptions.runtimeOnly.packages
和buildOption.arkOptions.runtimeOnly.sources
配置项进行变量是模块名和路径时的配置。注 3:HAR 包之间如果仅存在变量动态加载关系,可以将 HAR 包之间的依赖关系转移到 HAP/HSP 中,从而对 HAR 包之间的依赖关系解耦合,避免循环依赖的形成。
1
2
3import('moduleName').then((ns:ESObject) => {
// ns 即动态导入的模块
}1
2
3
4// HAP's oh-package.json5
"dependencies": {
"moduleName": "file:../moduleName"
}1
2
3
4
5
6
7
8
9// HAP's build-profile.json5
"buildOption": {
"arkOptions": {
"runtimeOnly": {
"packages": [ "moduleName" ] // 配置本模块变量动态 import 其他模块名,要求与 dependencies 中配置的名字一致。
"sources": [ "./src/main/ets/utils/customFile.ets" ] // 配置本模块变量动态 import 自己的文件路径,路径相对于当前 build-profile.json5文件。
}
}
} -
延迟加载:应用冷启动阶段不加载模块,直到应用程序运行过程中使用到对应组件时才同步加载(可能会阻塞任务,请注意)。
1
import lazy { x } from "mod";
注 1:为了使用延迟加载特性,需要在工程 build-profile.json5 配置文件中添加
"compatibleSdkVersionStage": "beta3"
。注 2:不支持延迟加载的变量重导出(re-export);不支持延迟加载 kit。
-
同步动态加载 Native 模块:在应用程序运行过程中使用到某个 Native 模块时进行同步加载(会产生 so 耗时,请注意)。
注:loadNativeModule 只局限于 UI 主线程中模块加载,且需要配置依赖方 oh-package.json5 文件中的
dependencies
和 build-profile.json5 文件中的buildOption.arkOptions.runtimeOnly.packages
。1
loadNativeModule(moduleName: string): Object;
1
2
3
4
5{
"dependencies": {
"libentry.so": "file:../src/main/cpp/types/libentry"
}
}1
2
3
4
5
6
7
8
9
10
11{
"buildOption" : {
"arkOptions" : {
"runtimeOnly" : {
"packages": [
"libentry.so"
]
}
}
}
} -
Node-API 接口加载文件:基于 napi_load_module 的在宿主线程中实现当前 hap/hsp 包工程内模块的加载;基于 napi_load_module_with_info 的在宿主线程或子线程中实现加载 har 包、hsp 包、native 模块等多种文件类型。
-
编译工具链(略看)
-
语法检查:检查 ArkTS/TS 语法正确性。
-
UI 转换:将 ArkTS 的 UI 范式语法转换为标准 TS 语法。
-
源码混淆:使用 ArkGuard 源码混淆工具对源码进行混淆,开发者可根据业务需要选择开启。
注:要开启代码混淆,需要将模块的 build-profile.json5 文件中的 ruleOptions.enable 字段的值设置为 true。
-
字节码编译:方舟编译器编译方舟字节码文件(*.abc)。
注:方舟字节码(Ark Bytecode),是由方舟编译器编译 ArkTS/TS/JS 生成的,提供给方舟运行时解释执行的二进制文件,字节码中的主要内容是方舟字节码指令。为了了解方舟字节码,需要了解方舟字节码的文件格式、基本原理、函数命名规则以及编译期自定义修改方舟字节码。
-
自定义修改方舟字节码:提供开发者修改字节码能力的入口,在字节码编译落盘前调用。
-
反汇编:使用 Disassembler 反汇编工具将字节数据反汇编成可阅读的汇编指令。
三、ArkUI(方舟 UI 框架)(略看)- 应用框架
组件导航(Navigation)
-
解释:组件导航(Navigation)主要用于实现页面间以及组件内部的页面跳转,支持在不同组件间传递跳转参数,提供灵活的跳转栈操作。
- Navigation 是路由导航的根视图容器,一般作为页面(@Entry)的根容器,包括单栏(Stack)、分栏(Split)和自适应(Auto)三种显示模式。API10+,推荐使用
NavPathStack
实现页面路由。 - Navigation 组件主要包含导航页和子页。导航页由标题栏(包含菜单栏)、内容区和工具栏组成,可以通过
hideNavBar
属性进行隐藏,导航页不存在页面栈中。
- Navigation 是路由导航的根视图容器,一般作为页面(@Entry)的根容器,包括单栏(Stack)、分栏(Split)和自适应(Auto)三种显示模式。API10+,推荐使用
-
显示模式
1
2
3
4
5
6
7Navigation() {
// ...
}
.mode(NavigationMode.Auto)
// NavigationMode.Auto 自适应模式,默认。当页面宽度大于等于一定阈值 600vp(API 10+)时,Navigation 组件采用分栏模式,反之采用单栏模式。
// NavigationMode.Split 分栏模式
// NavigationMode.Stack 单栏模式 -
标题栏模式:界面顶部的标题栏用于呈现界面名称和操作入口。当 Navigation 或 NavDestination 未设置主副标题并且没有返回键时,不显示标题栏。
1
2
3
4
5
6Navigation() {
// ...
}
.titleMode(NavigationTitleMode.Mini)
// NavigationTitleMode.Mini 普通型标题栏,用于一级页面不需要突出标题的场景。
// NavigationTitleMode.Full 强调型标题栏,用于一级页面需要突出标题的场景。 -
设置菜单栏
1
2
3
4
5const TooTmp: NavigationMenuItem = { 'value': "", 'icon': "./image/ic_public_highlights.svg", 'action': ()=> {} }
Navigation() {
// ...
}
.menus([TooTmp, TooTmp, TooTmp, TooTmp]) // 竖屏最多支持显示 3 个图标,多余的图标会被放入自动生成的更多图标。注:menus 支持
Array<NavigationMenuItem>
和CustomBuilder
两种参数类型。使用Array<NavigationMenuItem>
类型时,竖屏最多支持显示 3 个图标,横屏最多支持显示 5 个图标,多余的图标会被放入自动生成的更多图标。1
2
3
4
5
6
7interface NavigationMenuItem {
value: string | Resource;
icon?: string | Resource; // 菜单栏单个选项的图标资源路径。
isEnabled?: boolean; // 使能状态,默认使能(true)。
action?: () => void; // 当前选项被选中的事件回调。
symbolIcon?: SymbolGlyphModifier; // 菜单栏单个选项的 symbol 资源(优先级高于 icon)。
} -
设置工具栏
1
2
3
4
5
6let TooTmp: ToolbarItem = { 'value': "func", 'icon': "./image/ic_public_highlights.svg", 'action': ()=> {} }
let TooBar: ToolbarItem[] = [TooTmp, TooTmp, TooTmp]
Navigation() {
// ...
}
.toolbarConfiguration(TooBar)注:语法为
.toolbarConfiguration(value: Array<ToolbarItem> | CustomBuilder, options?: NavigationToolbarOptions)
,使用Array<ToolbarItem>
类型时,工具栏所有选项均分底部工具栏,在每个均分内容区布局文本和图标;文本超长时,若工具栏选项个数小于 5 个,优先拓展选项的宽度,最大宽度与屏幕等宽,其次逐级缩小,缩小之后换行,最后截断;竖屏最多支持显示 5 个图标,多余的图标会被放入自动生成的更多图标;横屏时,如果为 Split 模式,仍按照竖屏规则显示,如果为 Stack 模式需配合 menus 属性的 Array使用,底部工具栏会自动隐藏,同时底部工具栏所有选项移动至页面右上角菜单。 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21interface ToolbarItem {
value: ResourceStr; // 工具栏单个选项的显示文本。
icon?: ResourceStr; // 工具栏单个选项的图标资源路径。
action?: () => void; // 当前选项被选中的事件回调。
status?: ToolbarItemStatus; // 工具栏单个选项的状态。默认值:ToolbarItemStatus.NORMAL
activeIcon?: ResourceStr; // 工具栏单个选项处于 ACTIVE 态时的图标资源路径。
symbolIcon?: SymbolGlyphModifier; // 工具栏单个选项的 symbol 资源(优先级高于 icon)。
activeSymbolIcon?: SymbolGlyphModifier; // 工具栏单个选项处于 ACTIVE 态时的 symbol 资源(优先级高于 activeIcon)。
}
interface NavigationToolbarOptions {
backgroundColor?: ResourceColor; // 工具栏背景颜色,不设置时为系统默认颜色。
backgroundBlurStyle?: BlurStyle; // 工具栏背景模糊样式,不设置时关闭背景模糊效果。
barStyle?: BarStyle; // 设置工具栏布局方式。
}
enum ToolbarItemStatus {
NORMAL; // 该选项显示默认样式,可以触发 Hover,Press,Focus 事件并显示对应的多态样式。
DISABLED; // 该选项显示 DISABLED 态样式,并且不可交互。
ACTIVE; // 该选项通过点击事件可以将 icon 图标更新为 activeIcon 对应的图片资源。
} -
路由导航:每个 Navigation 都需要创建并传入一个页面栈 NavPathStack 对象,用于管理页面。主要涉及页面跳转、页面返回、页面替换、页面删除、参数获取、路由拦截等功能。API 12+ 页面栈允许被继承,开发者可以在派生类中自定义属性和方法,也可以重写父类的方法,派生类对象可以替代基类 NavPathStack 对象使用。
1 | @Entry |
1 | // 1. 普通跳转,通过页面的 name 去跳转,并可以携带 param。 |
1 | // 1. 返回到上一页 |
1 | // 将栈顶页面替换为 PageOne |
1 | // 删除栈中 name 为 PageOne 的所有页面 |
1 | // 移动栈中 name 为 PageOne 的页面到栈顶 |
1 | // 获取栈中所有页面 name 集合 |
为了获取页面跳转时传递的参数,可以指定 @Builder 函数的类型为
(name: string, param: Type)
,此时可以在 @Builder 函数中使用页面跳转传递的参数 param。
1 | this.pageStack.setInterception({ |
注:setInterception 方法需要传入一个 NavigationInterception 对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14 interface NavigationInterception {
willShow?: interceptionShowCallback; // 页面跳转前回调,允许操作栈,在当前跳转生效。
didShow?: interceptionShowCallback; // 页面跳转后回调,在该回调中操作栈会在下一次跳转生效。
modeChange?: InterceptionModeCallback; // Navigation 单双栏显示状态发生变更时触发该回调。
} // 注:无论是哪个回调,在进入回调时页面栈都已经发生了变化。
type InterceptionShowCallback = (from: NavDestinationContext|NavBar, to: NavDestinationContext|NavBar, operation: NavigationOperation, isAnimated: boolean) => void
type InterceptionModeCallback = (mode: NavigationMode) => void
type NavBar = 'navBar' // Navigation 首页名字。
interface NavDestinationContext {
pathInfo: NavPathInfo; // 跳转 NavDestination 时指定的参数,表示路由页面信息。
pathStack: NavPathStack; // 当前 NavDestination 所处的页面栈,表示 Navigation 路由栈,允许被继承。
navDestinationId: string; // 当前 NavDestination 的唯一 ID,由系统自动生成,和组件通用属性 id 无关。
}
-
子页面:NavDestination 是 Navigation 子页面的根容器,用于承载子页面的一些特殊属性以及生命周期等。NavDestination 可以设置独立的标题栏和菜单栏等属性,使用方法与 Navigation 相同。
-
显示模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29@Component
export struct DialogPage {
@Consume('NavPathStack') pageStack: NavPathStack;
build() {
NavDestination() {
Stack({ alignContent: Alignment.Center }) {
Column() {
Text("Dialog NavDestination")
.fontSize(20)
.margin({ bottom: 100 })
Button("Close").onClick(() => {
this.pageStack.pop()
}).width('30%')
}
.justifyContent(FlexAlign.Center)
.backgroundColor(Color.White)
.borderRadius(10)
.height('30%')
.width('80%')
}.height("100%").width('100%')
}
.backgroundColor('rgba(0,0,0,0.5)')
.hideTitleBar(true)
.mode(NavDestinationMode.DIALOG)
// NavDestinationMode.STANDARD 标准类型,默认。标准类型的 NavDestination 的生命周期跟随其在 NavPathStack 页面栈中的位置变化而改变。
// NavDestinationMode.DIALOG 弹窗类型。弹窗类型的 NavDestination 显示和消失时不会影响下层标准类型的 NavDestination 的显示和生命周期,两者可以同时显示,整 个NavDestination 默认透明显示。
}
} -
生命周期:aboutToAppear 和 aboutToDisappear 是自定义组件的生命周期(NavDestination 外层包含的自定义组件);onAppear 和 onDisappear是 组件的通用生命周期;剩下的六个生命周期为 NavDestination 独有。
-
页面中自定义组件的监听和查询
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17import { uiObserver } from '@kit.ArkUI';
// NavDestination 内的自定义组件
@Component
struct MyComponent {
navDesInfo: uiObserver.NavDestinationInfo | undefined // NavDestinationInfo 类型包括 navigationId、name、state、index、param、navDestinationId 等字段。
aboutToAppear(): void {
this.navDesInfo = this.queryNavDestinationInfo();
}
build() {
Column() {
Text("所属页面Name: " + this.navDesInfo?.name)
}.width('100%').height('100%')
}
}1
2
3uiObserver.on('navDestinationUpdate', (info) => {
console.info('NavDestination state update', JSON.stringify(info));
});1
2
3
4
5
6
7
8
9
10// 在UIAbility中使用
import { UIContext, uiObserver } from '@kit.ArkUI';
// callBackFunc 是开发者定义的监听回调函数
function callBackFunc(info: uiObserver.NavDestinationSwitchInfo) {}
uiObserver.on('navDestinationSwitch', this.context, callBackFunc);
// 可以通过窗口的getUIContext()方法获取对应的UIContent
uiContext: UIContext | null = null;
uiObserver.on('navDestinationSwitch', this.uiContext, callBackFunc);
-
-
页面转场:Navigation 默认提供了页面切换的转场动画,即通过页面栈操作时,会触发不同的转场效果(Dialog 类型的页面默认无转场动画),也可以自定义转场、共享元素转场。
1
2
3
4
5pageStack: NavPathStack = new NavPathStack()
aboutToAppear(): void {
this.pageStack.disableAnimation(true)
}1
2
3
4pageStack: NavPathStack = new NavPathStack()
this.pageStack.pushPath({ name: "PageOne" }, false)
this.pageStack.pop(false) -
跨包动态路由:一般来说,为了在组件导航中使用 name 跳转到对应的子页,需要在 @Entry 组件中给 Navigation 组件的
.navDestination
属性传入一个 @Builder 函数,支持根据不同的 name 渲染不同的 UI,这里的 UI 即 name 对应的子页。1
2
3
4
5
6
7
8
9
10@Builder
PageMap(name: string) {
if (name === "NavDestinationTitle1") {
pageOneTmp() // 此时可以 Navigation 组件中可以通过 NavPathStack 的 .pushPathByName 实现页面跳转
} else if (name === "NavDestinationTitle2") {
pageTwoTmp()
} else if (name === "NavDestinationTitle3") {
pageThreeTmp()
}
}而跨包动态路由可以实现在 @Entry 组件中的 Navigation 组件中不传入 @Builder 函数,只需要在模块中添加路由表配置,则可以在模块中任意地方使用 name 进行页面跳转。动态路由分为系统路由表和自定义路由表两种实现方式。
-
系统路由表
1
2
3
4
5{
"module" : {
"routerMap": "$profile:route_map"
}
}1
2
3
4
5
6
7
8
9
10
11
12{
"routerMap": [
{
"name": "PageOne", // 跳转页面名称。
"pageSourceFile": "src/main/ets/pages/PageOne.ets", // 跳转目标页在包内的路径,相对 src 目录的相对路径。
"buildFunction": "PageOneBuilder", // 跳转目标页的入口函数名称,必须以 @Builder 修饰。
"data": {
"description" : "this is PageOne" // 应用自定义字段。可以通过配置项读取接口 getConfigInRouteMap 获取。
}
}
]
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20// 跳转页面入口函数
@Builder
export function PageOneBuilder() {
PageOne()
}
@Component
struct PageOne {
pathStack: NavPathStack = new NavPathStack()
build() {
NavDestination() {
}
.title('PageOne')
.onReady((context: NavDestinationContext) => {
// NavDestinationContext 获取当前所在的页面栈
this.pathStack = context.pathStack
})
}
}1
2
3
4
5
6
7
8
9
10
11
12
13@Entry
@Component
struct Index {
pageStack : NavPathStack = new NavPathStack();
build() {
Navigation(this.pageStack){
}.onAppear(() => {
this.pageStack.pushPathByName("PageOne", null, false);
})
.hideNavBar(true)
}
} -
自定义路由表(略看)
-
List 组件
文件服务
-
应用文件访问
1
2
3
4
5
6
7import { fileIo as fs, ReadOptions } from '@kit.CoreFileKit';
import { common } from '@kit.AbilityKit';
import { buffer } from '@kit.ArkTS';
// 获取应用文件路径
let context = getContext(this) as common.UIAbilityContext;
let filesDir = context.filesDir; // UIAbilityContext 的 filesDir 为 <路径前缀>/<加密等级>/base/haps/<module-name>/files1
2
3
4
5
6// 文件不存在时创建并打开文件,文件存在时打开文件
let file = fs.openSync(filesDir + '/test.txt', fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
// 写入一段内容至文件
let writeLen = fs.writeSync(file.fd, "Try to write str.");
// 关闭文件
fs.closeSync(file);1
2
3
4
5
6
7
8
9
10
11
12// 创建一个大小为 1024 字节的 ArrayBuffer 对象,用于存储从文件中读取的数据
let arrayBuffer = new ArrayBuffer(1024);
// 设置读取的偏移量和长度
let readOptions: ReadOptions = {
offset: 0,
length: arrayBuffer.byteLength
};
// 读取文件内容到 ArrayBuffer 对象中,并返回实际读取的字节数
let readLen = fs.readSync(file.fd, arrayBuffer, readOptions);
// 将 ArrayBuffer 对象转换为 Buffer 对象,并转换为字符串输出
let buf = buffer.from(arrayBuffer, 0, readLen);
console.info("the content of file: " + buf.toString());1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24// 打开文件
let srcFile = fs.openSync(filesDir + '/test.txt', fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
let destFile = fs.openSync(filesDir + '/destFile.txt', fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
// 读取源文件内容并写入至目的文件
let bufSize = 4096;
let readSize = 0;
let buf = new ArrayBuffer(bufSize);
let readOptions: ReadOptions = {
offset: readSize,
length: bufSize
};
let readLen = fs.readSync(srcFile.fd, buf, readOptions);
while (readLen > 0) {
readSize += readLen;
let writeOptions: WriteOptions = {
length: readLen
};
fs.writeSync(destFile.fd, buf, writeOptions);
readOptions.offset = readSize;
readLen = fs.readSync(srcFile.fd, buf, readOptions);
}
// 关闭文件
fs.closeSync(srcFile);
fs.closeSync(destFile);1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25// 创建并打开输入文件流
let inputStream = fs.createStreamSync(filesDir + '/test.txt', 'r+');
// 创建并打开输出文件流
let outputStream = fs.createStreamSync(filesDir + '/destFile.txt', "w+");
let bufSize = 4096;
let readSize = 0;
let buf = new ArrayBuffer(bufSize);
let readOptions: ReadOptions = {
offset: readSize,
length: bufSize
};
// 以流的形式读取源文件内容并写入到目标文件
let readLen = await inputStream.read(buf, readOptions);
readSize += readLen;
while (readLen > 0) {
const writeBuf = readLen < bufSize ? buf.slice(0, readLen) : buf;
await outputStream.write(writeBuf);
readOptions.offset = readSize;
readLen = await inputStream.read(buf, readOptions);
readSize += readLen;
}
// 关闭文件流
inputStream.closeSync();
outputStream.closeSync();1
2
3
4
5
6
7
8
9
10
11
12
13
14let listFileOption: ListFileOptions = {
recursion: false,
listNum: 0,
filter: {
suffix: [".png", ".jpg", ".txt"],
displayName: ["test*"],
fileSizeOver: 0,
lastModifiedAfter: new Date(0).getTime()
}
};
let files = fs.listFileSync(filesDir, listFileOption);
for (let i = 0; i < files.length; i++) {
console.info(`The name of file: ${files[i]}`);
} -
用户文件(图片)访问:PhotoAccessHelper 的 PhotoViewPicker
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23import { BusinessError } from '@kit.BasicServicesKit';
import { photoAccessHelper } from '@kit.MediaLibraryKit';
try {
// Step1.
let PhotoSelectOptions = new photoAccessHelper.PhotoSelectOptions();
PhotoSelectOptions.MIMEType = photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE;
PhotoSelectOptions.maxSelectNumber = 5;
// Step2.
let photoPicker = new photoAccessHelper.PhotoViewPicker();
photoPicker.select(PhotoSelectOptions)
.then((PhotoSelectResult: photoAccessHelper.PhotoSelectResult) => {
// PhotoSelectResult.photoUris: string[]
// photoUris 具有永久授权,可通过调用 photoAccessHelper.getAssets 接口去使用
console.info('PhotoViewPicker.select successfully, PhotoSelectResult uri: ' + JSON.stringify(PhotoSelectResult.photoUris));
})
.catch((err: BusinessError) => {
console.error(`PhotoViewPicker.select failed with err: ${err.code}, ${err.message}`);
});
} catch (error) {
let err: BusinessError = error as BusinessError;
console.error(`PhotoViewPicker failed with err: ${err.code}, ${err.message}`);
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { dataSharePredicates } from '@kit.ArkData';
const context = getContext(this);
let phAccessHelper = photoAccessHelper.getPhotoAccessHelper(context);
class MediaDataHandler implements photoAccessHelper.MediaAssetDataHandler<ArrayBuffer> {
onDataPrepared(data: ArrayBuffer) {
if (data === undefined) {
console.error('Error occurred when preparing data');
return;
}
console.info('on image data prepared');
// 应用自定义对资源数据的处理逻辑
}
}
async function example() {
// Step1. 配置查询条件
let predicates: dataSharePredicates.DataSharePredicates = new dataSharePredicates.DataSharePredicates();
let uri = 'file://media/Photo/1/IMG_datetime_0001/displayName.jpg' // 需保证此uri已存在。
predicates.equalTo(photoAccessHelper.PhotoKeys.URI, uri.toString());
// Step2. 配置查询选项
let fetchOptions: photoAccessHelper.FetchOptions = {
fetchColumns: [photoAccessHelper.PhotoKeys.TITLE],
predicates: predicates
};
try {
// Step3. 获取图片和视频资源
let fetchResult: photoAccessHelper.FetchResult<photoAccessHelper.PhotoAsset> = await phAccessHelper.getAssets(fetchOptions);
// Step4. 获取文件检索结果中的第一个文件资产
let photoAsset: photoAccessHelper.PhotoAsset = await fetchResult.getFirstObject();
// Step5. 读取 PhotoAssets 的属性 uri、photoType、displayName;成员参数 photoAsset.get(photoAccessHelper.PhotoKeys.TITLE)(必须在 fetchOptions.fetchColumns 中配置)
console.info('getAssets photoAsset.uri : ' + photoAsset.uri);
// 获取属性值,以标题为例;对于非默认查询的属性,get前需要在fetchColumns中添加对应列名
console.info('title : ' + photoAsset.get(photoAccessHelper.PhotoKeys.TITLE));
// Step6.
// 请求图片资源数据
let requestOptions: photoAccessHelper.RequestOptions = {
deliveryMode: photoAccessHelper.DeliveryMode.HIGH_QUALITY_MODE,
}
await photoAccessHelper.MediaAssetManager.requestImageData(context, photoAsset, requestOptions, new MediaDataHandler());
console.info('requestImageData successfully');
// Step7.
// 获取缩略图
asset.getThumbnail((err, pixelMap) => {
if (err == undefined) {
console.info('getThumbnail successful ' + JSON.stringify(pixelMap));
} else {
console.error('getThumbnail fail', err);
}
});
fetchResult.close();
} catch (err) {
console.error('getAssets failed with err: ' + err);
}
} -
用户文件(图片)保存:PhotoAccessHelper 的 photoAccessHelper.getPhotoAccessHelper(context)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38import { photoAccessHelper } from '@kit.MediaLibraryKit';
@Entry
@Component
struct Index {
saveButtonOptions: SaveButtonOptions = {
icon: SaveIconStyle.FULL_FILLED,
text: SaveDescription.SAVE_IMAGE,
buttonType: ButtonType.Capsule
} // 设置安全控件按钮属性
build() {
Row() {
Column() {
SaveButton(this.saveButtonOptions) // 创建安全控件按钮
.onClick(async (event, result: SaveButtonOnClickResult) => {
if (result == SaveButtonOnClickResult.SUCCESS) {
try {
let context = getContext();
let phAccessHelper = photoAccessHelper.getPhotoAccessHelper(context);
// 需要确保fileUri对应的资源存在
let fileUri = 'file://com.example.temptest/data/storage/el2/base/haps/entry/files/test.jpg';
let assetChangeRequest: photoAccessHelper.MediaAssetChangeRequest = photoAccessHelper.MediaAssetChangeRequest.createImageAssetRequest(context, fileUri);
await phAccessHelper.applyChanges(assetChangeRequest);
console.info('createAsset successfully, uri: ' + assetChangeRequest.getAsset().uri);
} catch (err) {
console.error(`create asset failed with error: ${err.code}, ${err.message}`);
}
} else {
console.error('SaveButtonOnClickResult create asset failed');
}
})
}
.width('100%')
}
.height('100%')
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { fileIo } from '@kit.CoreFileKit';
let context = getContext(this);
let phAccessHelper = photoAccessHelper.getPhotoAccessHelper(context);
async function example() {
try {
// 指定待保存到媒体库的位于应用沙箱的图片uri
let srcFileUri = 'file://com.example.temptest/data/storage/el2/base/haps/entry/files/test.jpg';
let srcFileUris: Array<string> = [
srcFileUri
];
// 指定待保存照片的创建选项,包括文件后缀和照片类型,标题和照片子类型可选
let photoCreationConfigs: Array<photoAccessHelper.PhotoCreationConfig> = [
{
title: 'test', // 可选
fileNameExtension: 'jpg',
photoType: photoAccessHelper.PhotoType.IMAGE,
subtype: photoAccessHelper.PhotoSubtype.DEFAULT, // 可选
}
];
// 基于弹窗授权的方式获取媒体库的目标uri
let desFileUris: Array<string> = await phAccessHelper.showAssetsCreationDialog(srcFileUris, photoCreationConfigs);
// 将来源于应用沙箱的照片内容写入媒体库的目标uri
let desFile: fileIo.File = await fileIo.open(desFileUris[0], fileIo.OpenMode.WRITE_ONLY);
let srcFile: fileIo.File = await fileIo.open(srcFileUri, fileIo.OpenMode.READ_ONLY);
await fileIo.copyFile(srcFile.fd, desFile.fd);
fileIo.closeSync(srcFile);
fileIo.closeSync(desFile);
console.info('create asset by dialog successfully');
} catch (err) {
console.error(`failed to create asset by dialog successfully errCode is: ${err.code}, ${err.message}`);
}
}
四、媒体能力
Audio Kit(音频播放和录制接口)
Audio Kit 可以和 CameraKit 对比理解,二者在功能上有相似之处
AVCodec Kit(音视频的编解码、媒体文件的解析、封装、媒体数据输入等原子能力)
注:由于这部分仅提供 C 接口,故而暂时略过
AVSession Kit(统一管理系统中所有音视频行为)
- 主要功能:(1)统一控制:当音乐、视频应用接入AVSession后,它们的播放信息(正在播放的歌曲名称、播放状态等)会被发送到系统,用户可以通过系统播控中心、语音助手等统一控制不同应用的播放;(2)后台播放:允许音频应用在后台继续播放(需要同时申请后台任务权限)未接入AVSession的应用退到后台时,系统会强制暂停其音频播放。
- 核心概念
- 媒体会话:音视频应用和控制端的通信管道。
- 媒体会话提供方:音视频应用,告诉控制端正在播放的内容和状态等,并对控制端的命令予以响应。
- 媒体会话控制方:控制端,可以显示当前播放的媒体信息,向音视频应用发送控制命令。
- 媒体会话控制器:控制端操作媒体会话的工具,用于发送播放、暂停等控制命令,并对音视频应用状态变化。(思考,为什么不把控制器的功能留到媒体会话中去)
- 媒体会话管理器:媒体会话的创建和管理,包括对媒体会话状态的监听。
- 主要分类
- 本地媒体会话:QQ音乐(提供方)播放的歌曲可以被系统播控中心或小爱同学(控制方)在同一设备上控制。(提供方和控制方必须在同一设备上)
- 分布式媒体会话:手机上的QQ音乐(提供方)将歌曲投播到小爱音箱上播放,然后可以通过小爱音箱上的控制界面(控制方)控制播放。
Camera Kit(开发相机应用)
注:如果开发者仅是需要拉起系统相机拍摄一张照片、录制一段视频,可直接使用 CameraPicker,无需申请相机权限,直接拉起系统相机完成拍摄。
- CameraKit 使用所需权限:ohos.permission.CAMERA、ohos.permission.MICROPHONE、ohos.permission.MEDIA_LOCATION。
- 相机开发的核心方面:会话管理、输入输出流配置、其他配置与功能。
1 | import { camera } from '@kit.CameraKit'; |
Image Kit(对图片像素数据进行解析、处理、构造)
- 图片实例:image.createImageSource(原始图片) --> imageSource
- 图片解码:imageSource.createPixelMap() --> pixelMap
- 图片处理:图片的 PixelMap 处理,如旋转、缩放、裁剪等。可以通过 Image 组件显示图片的 PixelMap。
- 图片编码:image.createImagePacker() --> imagePacker --> imagePacker.packing(imageSource or pixelMap)
1 | import { image } from '@kit.ImageKit'; |
Meida Kit(开发音视频播放或录制的各类功能)
这里略看,仅阅读简介。
-
AVPlayer:播放音视频,Audio/Video媒体资源(比如mp4/mp3/mkv/mpeg-ts等)转码为可供渲染的图像和可听见的音频模拟信号,并通过输出设备进行播放。
-
SoundPool:播放短音频,音频媒体资源(比如mp3/m4a/wav等)转码为音频模拟信号,并通过输出设备进行播放。
-
AVRecorder:录制音视频,捕获音频信号,接收视频信号,完成音视频编码并保存到文件中。
-
AVScreenCapture:录制屏幕,捕获音频信号、视频信号,并通过音视频编码将屏幕信息保存到文件中。
-
AVMetadataExtractor:获取音视频元数据。从原始媒体资源中获取视频指定时间的视频帧。
-
AVImageGenerator:获取视频缩略图。
-
AVTranscoder:视频转码,将已压缩编码的视频文件按照指定参数转换为另一种格式的视频。
Media Library Kit(管理相册和媒体文件的能力)
除了受限开放的能力外,其他能力均不需要进行权限申请。
- 使用Picker选择媒体库资源:从图库选择图片,获取对应的 URI
- 保存媒体库资源:将指定 uri 对应的图片保存到图库,并获取保存在图库的图片的 uri
- 使用PhotoPicker组件访问图片/视频:直接展示图库,批量选择图片获取对应的 URI,同时允许其他操作
- 使用AlbumPicker组件访问相册列表:直接展示相册视图,用户点击对应相册时,可以通过相册的 URI 跳转至对应的 PhotoPicker 组件显示对应相册的内容
- 使用RecentPhoto组件获取最近一张图片:直接显示一张最近的图片
- 使用PhotoPicker推荐图片:展示图库的同时,可以对图库中的图片进行推荐显示,便于用户选择相关图片
- 对于动态照片,支持通过 Picker 组件从图库中选择动态照片获取 URI,同时支持根据 URI 创建动态照片对象,并提供相应的组件展示动态照片对象
- 对于受限开放的能力,开发者需要申请权限,包括对相册详细信息的读写等。
五、DevEco Studio
简单介绍
-
解释:HUAWEI DevEco Studio 是基于 IntelliJ IDEA Community 开源版本打造,为运行在 HarmonyOS 系统上的应用和元服务(以下简称应用/元服务)提供一站式的开发平台。
-
DevEco Studio 开发能力:HarmonyOS SDK、Node.js、Hvigor、OHPM、模拟器平台等。
注-1:上述工具在命令行中使用时需要[配置环境变量]
注-2:OHPM CLI(OpenHarmony Package Manager Command-line Interface) 作为鸿蒙生态三方库的包管理工具,支持共享包的发布、安装和依赖管理。
-
HarmonyOS SDK 位置:DevEco Studio 安装位置下 DevEco Studio\sdk 目录中。
-
开发环境诊断:在正式开发前,可以在 DevEco Studio 中进行开发环境诊断,方式为 “Help > Diagnostic Tools > Diagnose Development Environment”。
-
无网络配置:hypium 默认依赖、第三方库依赖、流水线搭建。
-
-
应用/元服务的开发流程
- 开发准备:安装 DevEco Studio --> 配置开发环境;
- 开发应用/元服务:创建应用工程 --> 便携应用代码 --> 使用预览器查看应用/元服务效果;
- 运行、调试和测试应用:申请调测证书 --> 运行应用 --> 调试应用(单步调试、跨语言调试等)–> 隐私/漏洞/性能等测试(Instrument Test、Local Test);
- 发布应用/元服务:申请发布证书 --> 发布至华为应用市场。
工程管理
-
应用/元服务逻辑结构
-
-
编译态
注:每个应用/元服务中至少包含一个 .hap 文件,可能包含若干个 .hsp 文件、也可能不含,一个应用中的所有 .hap 与 .hsp 文件合在一起称为 Bundle,其对应的 bundleName 是应用的唯一标识。
-
发布态
注-1:当应用/元服务发布上架到应用市场时,需要将 Bundle 打包为一个 .app 后缀的文件用于上架,这个 .app 文件称为 App Pack(Application Package),与此同时,DevEco Studio 工具自动会生成一个 pack.info 文件。pack.info 文件描述了 App Pack 中每个 HAP 和 HSP 的属性,包含 APP 中的 bundleName 和 versionCode 信息、以及 Module 中的 name、type 和 abilities 等信息。
注-2:App Pack 是发布上架到应用市场的基本单元,但是不能在设备上直接安装和运行。
注-3:在应用签名、云端分发、端侧安装时,都是以 HAP/HSP 为单位进行签名、分发和安装的。
-
-
视图:DevEco Studio 中展示工程目录的方式。
-
Project 视图:展示工程中实际的文件结构。Project 是默认的工程视图。
-
Ohos 视图:隐藏一些编码中不常用的文件,并对常用的文件进行重组展示。
-
-
模块管理
代码编辑
代码阅读
常用功能:代码格式化(快捷键、禁止格式化)、代码结构树、代码引用查找、代码查找、Optimize Imports 等。
功能 | 位置 | 备注 |
---|---|---|
代码高亮 | DevEco Studio > Preferences > Editor > Color Scheme | 自定义各字段的高亮显示颜色(需要取消“Inherit values from”选项) |
代码跳转 | Command + 点击代码 | |
跨语言跳转 | 选中代码 + 右键 Go To > Implementation(s) 或 选中代码 + Command + Option + B | |
代码格式化 | DevEco Studio > Preferences > Editor > Code Style | 自定义代码格式化规范 |
代码格式化 | [选中指定范围代码 + ]Option + Command + L | 快速对选定范围(默认全部代码)的代码进行格式化 |
代码格式化 | DevEco Studio > Preferences > Editor > Code Style 单击“Formatter”,勾选“Turn formatter on/off with markers in code comments” | 此时可以不格式化指定范围的代码,该范围以 // @formatter:off 开始,以 // @formatter:on 结束 |
代码格式化 | code-linter.json5 + 右键 Apply CodeLinter Style Rules | 使得代码格式化规则与已配置的 code-linter.json5 文件中相关规则保持一致 |
代码折叠 | [选中指定范围代码 + ]右键 Folding > Collapse/Collapse Recursively/Collapse All | 快速对选定范围(默认全部代码)的代码进行折叠 |
代码注释 | Command + / | 注释 or 解注释 |
代码结构树 | Command + 7 | 代码结构树包括全局变量和函数,类成员变量和方法等,并可以通过点击跳转到对应代码行 |
代码引用查找 | 选中代码 + 右键 > Find Usages 或选中代码 + Option + F7 | 快速查看某个对象(变量、函数或者类等)被引用的地方,既可以查看变量赋值位置(红色),也可以查看变量引用情况(绿色) |
函数注释 | “/**” + 回车键 | |
代码查找 | 连续点击两次 Shift | |
Optimize Imports | Control + Option + O 或菜单栏 Code > Optimize Imports | 快速清除未使用的 import,并根据设置的规则对 import 进行合并或排序 可以在“自定义代码格式化规范”页面的 Imports 栏中设置 Optimize Imports 规则 |
父/子类快速跳转 | 点击代码编辑区域左侧的 Gutter Icons(装订线图标) | Implemented:支持跳转到对应的实现类或子接口及其对应的属性/方法;Implementing:支持跳转到对应的父接口或父接口的属性/方法;Overridden:支持跳转到对应的子类或子类的属性/方法;Overriding:支持跳转到对应的父类或父类的属性/方法; 可以在 DevEco Studio > Preferences > Editor > General > Gutter Icons,通过勾选或取消勾选Implemented、Implementing、Overridden、Overriding四项可以开启或关闭该功能 |
接口/类的层次结构 | 选中或将光标放置于类/接口名称处 + Ctrl + H 或选中或将光标放置于类/接口名称处 + 菜单栏 Navigate > Type Hierarchy | 查看当前接口/类父类或子类的层次结构 |
代码生成/补全
代码检查 Code Linter
-
Code Linter:针对 ArkTS/TS 代码进行最佳实践/编程规范方面的检查。
- 代码全量检查方式为,右键 + Code Linter > Full Linter。
- 支持通过设置,在 Git 提交时,对 Git 增量代码进行检查。
- 若未配置代码检查规则文件,直接执行 Code Linter,将按照默认的编程规范规则对 .ets 文件进行检查。
- Code Linter 不对如下文件及目录进行检查:src/ohosTest 文件夹、src/test 文件夹、node_modules 文件夹、oh_modules 文件夹、build 文件夹、.preview 文件夹、hvigorfile.ts 文件、hvigorfile.js 文件。
-
代码检查配置文件:code-linter.json5(工程目录配置文件)
注:files(检查范围)、ignore(不检查范围)、ruleSet(规则集)、rules(对规则集的指定规则进行修改)、overrides(对指定目录或文件的规则进行定制化修改)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87{
/*
用于表示配置适用的文件范围的 glob 模式数组。在没有指定的情况下,应用默认配置。
配置待检查的文件名单,如未指定目录,将检查当前被选中的文件或文件夹中所有的 .ets 文件。
*/
"files": [
"**/*.js",
"**/*.ts"
],
/*
一个表示配置对象不应适用的文件的 glob 模式数组。如果没有指定,配置对象将适用于所有由 files 匹配的文件。
配置无需检查的文件目录,其指定的目录或文件需使用相对路径格式,相对于 code-linter.json5 所在工程根目录,例如:build//**//*。
*/
"ignore": [
"build/**/*",
"node_modules/**/*"
],
/*
设置检查待应用的规则集
配置检查使用的规则集,规则集支持一次导入多条规则。
目前支持的规则集包括:
通用规则 @typescript-eslint
一次开发多端部署规则 @cross-device-app-dev
ArkTS 代码风格规则 @hw-stylistic
安全规则 @security
性能规则 @performance
预览规则 @previewer
以上规则集均分为 all 和 recommended 两种规则集。all 规则集是规则全集,包含所有规则;recommended 规则集是推荐使用的规则集合。all 规则集包含 recommended 规则集。
不在工程根目录新建 code-linter.json5 文件的情况下,Code Linter 默认会检查 @performance/recommended 和 @typescript-eslint/recommended 规则集包含的规则。
*/
"ruleSet": [
"plugin:@typescript-eslint/recommended"
//快捷批量引入的规则集, 枚举类型:plugin:@typescript-eslint/all, plugin:@typescript-eslint/recommended, plugin:@cross-device-app-dev/all, plugin:@cross-device-app-dev/recommended等
],
/*
可以基于 ruleSet 配置的规则集,新增额外规则项,或修改 ruleSet 中规则默认配置,例如:将规则集中某条规则告警级别由 warn 改为 error。
可以对 ruleSet 配置的规则集中特定的某些规则进行修改、去使能, 或者新增规则集以外的规则;ruleSet 和 rules 共同确定了代码检查所应用的规则。
*/
"rules": {
// ruleId 后面跟数组时, 第一个元素为告警级别, 后面的对象元素为规则特定开关配置
"@typescript-eslint/no-explicit-any": [
// 告警级别: 枚举类型, 支持配置为 suggestion, error, warn, off
"error",
// 规则特定的开关配置, 为可选项, 不同规则其下层的配置项不同
{
"ignoreRestArgs": true
}
],
// ruleId 后面跟单独一个数字时, 表示仅设置告警级别, 枚举值为: 3(suggestion), 2(error), 1(warn), 0(off)
"@typescript-eslint/explicit-function-return-type": 2,
// ruleId后面跟单独一个字符串时, 表示仅设置告警级别, 枚举值为: suggestion, error, warn, off
"@typescript-eslint/no-unsafe-return": "warn"
},
/*
针对工程根目录下部分特定目录或文件,可配置定制化检查的规则。
针对特定的目录或文件采用定制化的规则配置
*/
"overrides": [
{
// 指定需要定制化配置规则的文件或目录
"files":
[
"entry/**/*.ts"
],
// 指定需要排除的目录或文件, 被排除的目录或文件不会按照定制化的规则配置被检查; 字符串类型
"excluded": [
"entry/**/*.test.js"
],
// 支持对 overrides 外公共配置的规则进行修改、去使能, 或者新增公共配置以外的规则; 该配置将覆盖公共配置
"rules":
{
// ruleId: 枚举类型
"@typescript-eslint/explicit-function-return-type":
[
// 告警级别: 枚举类型, 支持配置为error, warn, off; 覆盖公共配置, explicit-function-return-type告警级别为warn
"warn",
// 规则特定的开关配置, 为可选项, 不同规则其下层的配置项不同
{
allowExpressions: true
}
],
// 覆盖公共配置, 不检查 no-unsafe-return 规则
"@typescript-eslint/no-unsafe-return": "off"
}
}
]
} -
Code Linter 检查屏蔽
- 全局文件
/* eslint-disable */
(在 eslint-disable 后加入一个或多个以逗号分隔的规则 Id,可以屏蔽具体检查规则) - 代码块
/* eslint-disable */
开始,/* eslint-enable */
结束 - 代码行
/* eslint-disable-next-line */
- 全局文件
代码重构
使用方式:选中代码 + 右键 Refactor + 选择对应功能。
-
Refactor-Extract 代码提取:将函数内、类方法内等区域代码块或表达式,提取为新方法/函数(Method)、常量(Constant)、接口(Interface)、变量(Variable)或类型别名(Type Alias)。
注-1:支持将 ArkUI 组件的属性提取为一个 @Extend 方法,便于样式的管理。
注-2:Refactor-Extract 代码提取为类型别名(Type Alias)能力仅 TS 语言支持。
-
Refactor-Convert 代码转换:JS 源码中的 function --> 符合 ES6 标准的类、箭头函数 <–> 匿名函数、箭头函数 --> 普通函数、named export <–> default export、named export <–> namespace export、字符串 --> 模板字面量、判空逻辑 --> 可选链式调用。
-
Refactor-Rename 代码重命名:快速更改变量、方法、对象属性等相关标识符及文件、模块的名称,并同步到整个工程中对其进行引用的位置。
ArkDocs 文档生成
界面预览
PreviewChecker 检测规则
DevEco Studio 启动预览时将执行 PreviewChecker,检测通过后才可进行预览,以确保在使用预览器前识别到已知的不支持预览的场景,若存在不支持预览的场景,将给出优化提示,以便于开发者根据提示的建议进行代码优化。
-
@previewer/mandatory-default-value-for-local-initialization
-
@previewer/no-unallowed-decorator-on-root-component
-
@previewer/paired-use-of-consume-and-provide
注:对于 @Entry 组件,不允许使用 @Consume、@Link、@ObjectLink、@Prop 注解;对于 @Preview 组件,建议使用一个定义了完整的、合法的、不依赖运行时的默认值的父组件作为预览该组件的容器。
-
@previewer/no-page-method-on-preview-component
注:页面方法指的是 onPageShow、onPageHide、onBackPress 等仅在 @Entry 组件上生效的方法。
-
@previewer/no-page-import-unmocked-hsp
注:建议待预览的组件及其依赖的组件避免引用 HSP,或为该 HSP 设置 Mock 实现。
ArkTS/JS 预览效果 - Previewer
关注:预览的限制条件、实时预览和动态预览、实时预览的关闭、打开预览器 Previewer、重新加载预览器
ArkUI 预览效果 - Previewer
关注:页面预览(@Entry 组件)与组件预览(非 @Entry 组件,且需要被 @Preview 装饰)、页面预览与组件预览的切换、依赖参数注入的组件的组件预览
其他关注
- Profile Manager:如何切换预览设备类型、如何新建预览设备类型、如何同时查看多种设备类型的预览效果。
- Inspector:如何开启 Inspector 双向预览,使得代码编辑器、UI、Component Tree 三者之间联动。
- 预览数据 Mock:如何 Mock 方法、属性、模块。
应用/元服务开发 *
- HAR 包的创建、编译构建、发布、引用(包括第三方包、本地源码、本地 HAR 包)
- HSP 包的创建、编译构建、发布(私仓)、引用(本地 HSP 包)
- 共享包安装(install)或卸载(uninstall)的生命周期管理(通过 Hooks)
- 生成应用图标,通过 Image Asset 功能,帮助开发者生成适应不同设备、不同屏幕密度的图标,并展示图标在目录中的具体位置。
- 快速插入场景化代码片段,通过 Kit Assistant能力,支持通过拖拽方式将基础的场景化的控件/代码片段插入 ArkTS 工程中,减少高频场景代码的编写时间。
编译构建 *
应用/元服务签名 *
应用/元服务运行
-
模拟器运行(Emulator)
注:模拟器与真机的差异在于对 Kit 的支持能力不同。
- 创建模拟器:菜单栏 Tools > Device Manager > 右下角的 Edit 设置模拟器实例的存储路径 Local Emulator Location > 右下角的 New Emulator 创建一个模拟器 > … > 启动创建的模拟器 > Run
- 清除用户数据、查看磁盘数据、生成日志信息、关闭模拟器、删除模拟器
- 模拟器的使用
应用/元服务调试 *
- 自定义调试/运行配置
- ArkTS 代码调试