HarmonyOS
NEXT API文档
文档中心-HarmonyOS NEXT开发文档-华为开发者联盟
HarmonyOS Design
HarmonyOS 主题图标库 | icon素材免费下载 | 华为开发者联盟
审核
HarmonyOS不支持热修复,考虑到应用紧急故障快速修复的诉求,HarmonyOS应用市场提供了审核加急方案,通过该方案,可以快速审核,若您的应用符合上架条件,将在2小时之内完成上架。审核加急填写流程:
- 向AGC运营人员发送申请邮件,邮件标题:[申请使用HarmonyOS应用加急审核上架]-[应用名称]-[APPID]-[公司名称]-[Developer ID]。
- 填写是否加急:配置为“不加急”,您的应用将按照正常的流程进行审核上架。配置为“加急”,系统会优先处理该应用的上架审核任务。
- 加急类型:当前加急类型包括临时漏洞、临时活动和其他。若配置为“临时漏洞”,则表示您需要紧急修复缺陷;配置为“临时活动”表示正在进行一项商业类活动。
- 加急说明:您可在此对加急审核上架进行描述,以便运营人员快速处理审核任务。
另外,审核加急配置当前仅在中国大陆地区开放。在一个自然年内,一个应用有3次加急机会。
链接
官方示例项目:
零碎:
Native侧与ArkTS侧的相互调用:CommonAppDevelopment/feature/etswrapper · HarmonyOS-Cases/Cases
弹框封装:CommonAppDevelopment/feature/encapsulationdialog · HarmonyOS-Cases/Cases
使用colorPicker实现背景跟随主题颜色转换:CommonAppDevelopment/feature/effectkit · HarmonyOS-Cases/Cases
Grid和List内拖拽交换子组件位置:CommonAppDevelopment/feature/dragandexchange · HarmonyOS-Cases/Cases
数字滚动动效:CommonAppDevelopment/feature/digitalscrollanimation · HarmonyOS-Cases/Cases
图片选择和下载保存案例:CommonAppDevelopment/feature/photopickandsave · HarmonyOS-Cases/Cases
环境
工具下载
npm 配置
警告
强烈建议直接使用 ohpm 进行第三方库安装及配置。
npm config set @ohos:registry=https://repo.harmonyos.com/npm/
也可手动修改 C:\Users\<用户名>\.npmrc
+ @ohos:registry=https://repo.harmonyos.com/npm/
ohpm 配置
配置文件在 C:\Users\<用户名>\.ohpm\.ohpmrc
# 如果已经安装了 `DevEco Studio` 那这个配置是本身就在的
+ registry=https://ohpm.openharmony.cn/ohpm/
ohpmrc-ohpm-Command Line Tools | 华为开发者联盟
resolve_conflict
ohpm客户端在1.5.0版本开始支持依赖版本冲突自动解决功能。只需要在.ohpmrc文件中,将resolve_conflict配置为true或缺省,即可开启该功能。依赖冲突的处理策略为:当您的项目同时依赖了某个三方库的不同版本时,ohpm将选择其中的最高版本进行安装。
命令行hap签名
搭建流水线-Command Line Tools - 华为HarmonyOS开发者
插件
AI智能辅助编程工具-DevEco Studio | 华为开发者联盟
应用权限管控概述
权限列表
声明权限
{
"module": {
// ···
// 1.ohos.permission.APPROXIMATELY_LOCATION与ohos.permission.LOCATION为user_grant权限,reason和usedScene为必填字段。
// 2.ohos.permission.USE_BLUETOOTH为system_grant权限,reason和usedScene为选填字段。
"requestPermissions": [
{
// 权限名称
"name": "ohos.permission.APPROXIMATELY_LOCATION",
// 申请原因
"reason": "$string:approximately_location_permission_reason",
"usedScene": {
// 使用权限的Ability
"abilities": [
"FormAbility"
],
// 调用时机:使用时弹框
"when": "inuse"
}
},
{
"name": "ohos.permission.LOCATION",
"reason": "$string:location_permission_reason",
"usedScene": {
"abilities": [
"FormAbility"
],
"when": "inuse"
}
},
{
"name": "ohos.permission.USE_BLUETOOTH"
}
]
}
}
向用户申请授权
校验当前是否已经授权
import { abilityAccessCtrl, bundleManager, Permissions } from '@kit.AbilityKit'; import { BusinessError } from '@kit.BasicServicesKit'; async function checkPermissionGrant(permission: Permissions): Promise<abilityAccessCtrl.GrantStatus> { let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager(); let grantStatus: abilityAccessCtrl.GrantStatus = abilityAccessCtrl.GrantStatus.PERMISSION_DENIED; // 获取应用程序的accessTokenID。 let tokenId: number = 0; try { let bundleInfo: bundleManager.BundleInfo = await bundleManager.getBundleInfoForSelf(bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_APPLICATION); let appInfo: bundleManager.ApplicationInfo = bundleInfo.appInfo; tokenId = appInfo.accessTokenId; } catch (error) { const err: BusinessError = error as BusinessError; console.error(`Failed to get bundle info for self, code: ${err.code}, message: ${err.message}`); } // 校验应用是否被授予权限。 try { grantStatus = await atManager.checkAccessToken(tokenId, permission); } catch (error) { const err: BusinessError = error as BusinessError; console.error(`Failed to check access token, code: ${err.code}, message: ${err.message}`); } return grantStatus; } async function checkPermissions(): Promise<void> { let grantStatus1: boolean = await checkPermissionGrant('ohos.permission.LOCATION') === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;// 获取精确定位权限状态。 let grantStatus2: boolean = await checkPermissionGrant('ohos.permission.APPROXIMATELY_LOCATION') === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;// 获取模糊定位权限状态。 // 精确定位权限只能跟模糊定位权限一起申请,或者已经有模糊定位权限才能申请精确定位权限。 if (grantStatus2 && !grantStatus1) { // 申请精确定位权限。 } else if (!grantStatus1 && !grantStatus2) { // 申请模糊定位权限与精确定位权限或单独申请模糊定位权限。 } else { // 已经授权,可以继续访问目标操作。 } }动态向用户申请权限
重要
应用在
UIAbility#onWindowStageCreate()中申请授权时,需要等待异步接口loadContent()/setUIContent()执行结束后或在loadContent()/setUIContent()回调中调用 requestPermissionsFromUser(),否则在Content加载完成前,requestPermissionsFromUser会调用失败。应用在
UIExtensionAbility申请授权时,需要在onWindowStageCreate函数执行结束后或在onWindowStageCreate函数回调中调用 requestPermissionsFromUser(),否则在ability加载完成前,requestPermissionsFromUser会调用失败。
在UIAbility中向用户申请授权
// 使用UIExtensionAbility:将import { UIAbility } from '@kit.AbilityKit' 替换为import { UIExtensionAbility } from '@kit.AbilityKit'; import { abilityAccessCtrl, common, Permissions, UIAbility } from '@kit.AbilityKit'; import { window } from '@kit.ArkUI'; import { BusinessError } from '@kit.BasicServicesKit'; const permissions: Array<Permissions> = ['ohos.permission.LOCATION','ohos.permission.APPROXIMATELY_LOCATION']; // 使用UIExtensionAbility:将common.UIAbilityContext 替换为common.UIExtensionContext。 function reqPermissionsFromUser(permissions: Array<Permissions>, context: common.UIAbilityContext): void { let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager(); // requestPermissionsFromUser会判断权限的授权状态来决定是否唤起弹窗。 atManager.requestPermissionsFromUser(context, permissions).then((data) => { let grantStatus: Array<number> = data.authResults; let length: number = grantStatus.length; for (let i = 0; i < length; i++) { if (grantStatus[i] === 0) { // 用户授权,可以继续访问目标操作。 } else { // 用户拒绝授权,提示用户必须授权才能访问当前页面的功能,并引导用户到系统设置中打开相应的权限。 return; } } // 授权成功。 }).catch((err: BusinessError) => { console.error(`Failed to request permissions from user. Code is ${err.code}, message is ${err.message}`); }) } // 使用UIExtensionAbility:将 UIAbility 替换为UIExtensionAbility export default class EntryAbility extends UIAbility { onWindowStageCreate(windowStage: window.WindowStage): void { // ... windowStage.loadContent('pages/Index', (err, data) => { reqPermissionsFromUser(permissions, this.context); // ... }); } // ... }在UI中向用户申请授权
import { abilityAccessCtrl, common, Permissions } from '@kit.AbilityKit'; import { BusinessError } from '@kit.BasicServicesKit'; const permissions: Array<Permissions> = ['ohos.permission.LOCATION','ohos.permission.APPROXIMATELY_LOCATION']; // 使用UIExtensionAbility:将common.UIAbilityContext 替换为common.UIExtensionContext function reqPermissionsFromUser(permissions: Array<Permissions>, context: common.UIAbilityContext): void { let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager(); // requestPermissionsFromUser会判断权限的授权状态来决定是否唤起弹窗。 atManager.requestPermissionsFromUser(context, permissions).then((data) => { let grantStatus: Array<number> = data.authResults; let length: number = grantStatus.length; for (let i = 0; i < length; i++) { if (grantStatus[i] === 0) { // 用户授权,可以继续访问目标操作。 } else { // 当用户拒绝授权时,系统应提示用户必须授予相应权限才能使用当前页面的功能,并指导用户前往系统设置开启所需权限。 return; } } // 授权成功 }).catch((err: BusinessError) => { console.error(`Failed to request permissions from user. Code is ${err.code}, message is ${err.message}`); }) } @Entry @Component struct Index { aboutToAppear() { // 使用UIExtensionAbility:将common.UIAbilityContext 替换为common.UIExtensionContext const context: common.UIAbilityContext = this.getUIContext().getHostContext() as common.UIAbilityContext; reqPermissionsFromUser(permissions, context); } build() { // ... } }
重要
- 每次执行需要目标权限的操作时,应用都必须检查自己是否已经具有该权限。
- 如果用户拒绝授权,将无法再次弹窗。需要引导用户进行手动授权,也可以使用requestPermissionOnSetting再次向用户申请授权,此时不再弹窗,而是一个拉起半模态窗口引导用户授权。
- 用户手动授权:
- 路径一:设置 -> 隐私与安全 -> 权限类型(如位置信息) -> 具体应用
- 路径二:设置 -> 应用和元服务 -> 某个应用
- 用户手动授权:
再次向用户申请授权
import { abilityAccessCtrl, Context, common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
let atManager: abilityAccessCtrl.AtManager = abilityAccessCtrl.createAtManager();
let context: Context = this.getUIContext().getHostContext() as common.UIAbilityContext;
atManager.requestPermissionOnSetting(context, ['ohos.permission.APPROXIMATELY_LOCATION']).then((data: Array<abilityAccessCtrl.GrantStatus>) => {
console.info(`requestPermissionOnSetting success, result: ${data}`);
}).catch((err: BusinessError) => {
console.error(`requestPermissionOnSetting fail, code: ${err.code}, message: ${err.message}`);
});
json5
oh-package.json5
从OHPM 5.0.0版本开始,支持区分工程级与模块级oh-package.json5配置。其中:
- 工程级oh-package.json5 文件:位于工程根目录下,主要用来描述全局配置,如:依赖覆盖(overrides)、依赖关系重写(overrideDependencyMap)和参数化配置(parameterFile)等。
- 模块级oh-package.json5 文件:位于工程各个模块的根目录下,用来描述包名、版本、入口文件(类型声明文件)和依赖项等信息。
开发者可将标准的DevEco Studio工程下的各个模块打成HAR包后,发布到OpenHarmony三方库中心仓;所有发布到仓库的包必须包含模块级oh-package.json5文件,以描述当前包基本信息。
提示
可以鼠标左键单击某个属性查看解释
app.json5
module.json5
build-profile.json5
开发
无障碍(李跳跳
警告
(更新)HarmonyOS NEXT现在不再开放无障碍API
重要
虚拟机不支持无障碍Kit
Accessibility Kit简介-Accessibility Kit(无障碍开发服务)-应用框架 | 华为开发者联盟
ArkTS API-Accessibility Kit(无障碍开发服务)-应用框架 | 华为开发者联盟
@ohos.accessibility.GesturePoint (手势触摸点)-ArkTS API-Accessibility Kit(无障碍开发服务)-应用框架 | 华为开发者联盟
- 检测当前窗口内的App包名
- 检测指定App窗口内的是否存在某个文字
HSP & HAR
应用程序包概述-应用程序包基础知识-开发基础知识-基础入门 | 华为开发者联盟

Stage 模型
基本概念

每个Entry类型或者Feature类型的HAP在运行期都有一个AbilityStage实例,当HAP中的代码首次被加载到进程中的时候,系统会先创建AbilityStage实例。
UIAbility组件和ExtensionAbility组件
Stage模型提供UIAbility和ExtensionAbility两种类型的组件,这两种组件都有具体的类承载,支持面向对象的开发方式。
- UIAbility组件是一种包含UI的应用组件,主要用于和用户交互。例如,图库类应用可以在UIAbility组件中展示图片瀑布流,在用户选择某个图片后,在新的页面中展示图片的详细内容。同时用户可以通过返回键返回到瀑布流页面。UIAbility组件的生命周期只包含创建、销毁、前台、后台等状态,与显示相关的状态通过WindowStage的事件暴露给开发者。
- ExtensionAbility组件是一种面向特定场景的应用组件。开发者并不直接从ExtensionAbility组件派生,而是需要使用ExtensionAbility组件的派生类。目前ExtensionAbility组件有用于卡片场景的FormExtensionAbility,用于输入法场景的InputMethodExtensionAbility,用于延时任务场景的WorkSchedulerExtensionAbility等多种派生类,这些派生类都是基于特定场景提供的。例如,用户在桌面创建应用的卡片,需要应用开发者从FormExtensionAbility派生,实现其中的回调函数,并在配置文件中配置该能力。ExtensionAbility组件的派生类实例由用户触发创建,并由系统管理生命周期。在Stage模型上,三方应用开发者不能开发自定义服务,而需要根据自身的业务场景通过ExtensionAbility组件的派生类来实现。
一个HAP包中可以包含一个或多个UIAbility/ExtensionAbility组件,这些组件在运行时共用同一个AbilityStage实例。当HAP中的代码(无论是UIAbility组件还是ExtensionAbility组件)首次被加载到进程中的时候,系统会先创建对应的AbilityStage实例。
每个UIAbility实例都会与一个WindowStage类实例绑定,该类起到了应用进程内窗口管理器的作用。它包含一个主窗口。也就是说UIAbility实例通过WindowStage持有了一个主窗口,该主窗口为ArkUI提供了绘制区域,可以加载不同的ArkUI页面。
在Stage模型上,Context及其派生类向开发者提供在运行期可以调用的各种资源和能力。UIAbility组件和各种ExtensionAbility组件的派生类都有各自不同的Context类,他们都继承自基类Context,但是各自又根据所属组件,提供不同的能力。
Context
详情:应用上下文Context-Stage模型应用组件-指南
不同类型Context的说明
| Context类型 | 说明 | 获取方式 | 使用场景 |
|---|---|---|---|
| ApplicationContext | 应用的全局上下文,提供应用级别的信息和能力。 | - 从API version 14开始,可以直接使用getApplicationContext获取。- API version 14以前版本,只能使用其他Context实例的getApplicationContext方法获取。 | - 获取当前应用的基本信息。- 获取应用级别的文件路径。- 获取和修改加密分区。- 注册生命周期监听。 |
| AbilityStageContext | 模块级别的上下文,提供模块级别的信息和能力。 | - 如果需要获取当前AbilityStage的Context,可以直接通过AbilityStage实例获取context属性。- 如果需要获取同一应用中其他Module的Context,可以通过createModuleContext方法。 | - 获取当前模块的基本信息。- 获取模块的文件路径。 |
| UIAbilityContext | UIAbility组件对应的上下文,提供UIAbility对外的信息和能力。 | - 通过UIAbility实例直接获取context属性。- 在UIAbility的窗口中加载的UI组件实例,需要使用UIContext的getHostContext方法。 | - 获取当前UIAbility基本信息。- 启动其他应用或元服务、连接/断连系统应用创建的ServiceExtensionAbility等。- 销毁自身的UIAbility。 |
| ExtensionContext | ExtensionAbility组件对应的上下文,每种类型的ExtensionContext提供不同的信息和能力。 | 通过ExtensionAbility实例直接获取Context属性。 | 不同类型的ExtensionAbility对应的Context提供的能力不同。以输入法上下文InputMethodExtensionContext为例,主要提供如下能力:- 获取InputMethodExtensionAbility的基本信息。- 销毁当前输入法。 |
| UIContext | ArkUI的UI实例上下文,提供UI操作相关的能力。与上述其他类型的Context无直接关系。 | - 在UI组件内获取UIContext,直接使用getHostContext方法。- 在存在Window实例的情况下,使用Window提供的getUIContext方法。 | 主要用于UI实例中UI相关操作,例如:- 获取当前UI实例的字体。- 显示不同类型的弹框。- 设置软键盘弹出时UI避让模式。 |
不同类型Context的继承关系如下:

不同类型Context的持有关系如下:

提示
UIContext是指UI实例上下文,用于关联窗口*(window)与UI页面(@Entry)*。与本文档中的应用上下文Context无直接关联,不存在继承或持有关系。
应用启动
module.json5中mainElement指定了当前Module的入口UIAbility名称或者ExtensionAbility名称"module": { "name": "flexmod", "type": "entry", "description": "$string:module_desc", "mainElement": "FlexmodAbility", ... }module.json5中abilities下面的srcEntry指定了当前Ability的入口为FlexmodAbility.ets"abilities": [ { "name": "FlexmodAbility", "srcEntry": "./ets/flexmodability/FlexmodAbility.ets", "description": "$string:ability_desc", "icon": "$media:layered_image", "label": "$string:ability_label", "startWindowIcon": "$media:startIcon", "startWindowBackground": "$color:start_window_background", "exported": true, "skills": [ { "entities": [ "entity.system.home" ], "actions": [ // "action.system.home" "ohos.want.action.home" ] } ] } ],FlexmodAbility.ets应用入口Ability中的
onWindowStageCreate方法,使用windowStage.loadContent()方法设置应用要加载的页面:export default class FlexmodAbility extends UIAbility { onWindowStageCreate(windowStage: window.WindowStage): void { // Main window is created, set main page for this ability. Logger.info('Ability onWindowStageCreate'); windowStage.loadContent('pages/Index', (err) => { if (err.code) { Logger.error('Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? ''); return; } Logger.info('Succeeded in loading the content.'); }); } }
UIAbility的生命周期
提示
一个hap可以有多个UIAbility

- UIAbility启动到前台,对应流程图参见上图。
- 当用户启动一个UIAbility时,系统会依次触发onCreate()、onWindowStageCreate()、onForeground()生命周期回调。
- 当用户跳转到其他应用(当前UIAbility切换到后台)时,系统会触发onBackground()生命周期回调。
- 当用户再次将UIAbility切换到前台时,系统会依次触发onNewWant()、onForeground()生命周期回调。
UIAbility启动到后台,对应流程图参见下图。
- 当用户通过UIAbilityContext.startAbilityByCall()接口启动一个UIAbility到后台时,系统会依次触发onCreate()、onBackground()(不会执行onWindowStageCreate()生命周期回调)生命周期回调。
- 当用户将UIAbility拉到前台,系统会依次触发onNewWant()、onWindowStageCreate()、onForeground()生命周期回调。

手机配置文件(环境变量)发生改变,会触发
UIAbility的onConfigurationUpdate事件onConfigurationUpdate(newConfig: Configuration): void { AppStorage.setOrCreate('currentColorMode', newConfig.colorMode); hilog.info(0x0000, 'EntryAbility', 'the newConfig.colorMode is %{public}s', JSON.stringify(AppStorage.get('currentColorMode')) ?? ''); }
在 Stage 模型下,应用主窗口由 UIAbility 创建并维护生命周期。
Create:应用加载过程中,UIAbility实例创建完成时系统会调用onCreate()回调。可以在该回调中进行页面初始化操作,例如变量定义资源加载等,用于后续的UI展示。在进入
Foreground之前,系统会创建一个WindowStage。WindowStage创建完成后会进入onWindowStageCreate()回调,可以在该回调中设置UI加载、设置WindowStage的事件订阅。在
onWindowStageCreate()回调中通过loadContent()方法设置应用要加载的页面,并根据需要调用on('windowStageEvent')方法订阅WindowStage的事件(获焦/失焦、可见/不可见)。onWindowStageCreate(windowStage: window.WindowStage): void { // 设置WindowStage的事件订阅(获焦/失焦、可见/不可见) try { windowStage.on('windowStageEvent', (data) => { let stageEventType: window.WindowStageEventType = data; switch (stageEventType) { case window.WindowStageEventType.SHOWN: // 切到前台 console.info('windowStage foreground.'); break; case window.WindowStageEventType.ACTIVE: // 获焦状态 console.info('windowStage active.'); break; case window.WindowStageEventType.INACTIVE: // 失焦状态 console.info('windowStage inactive.'); break; case window.WindowStageEventType.HIDDEN: // 切到后台 console.info('windowStage background.'); break; default: break; } }); } catch (exception) { console.error('Failed to enable the listener for window stage event changes. Cause:' + JSON.stringify(exception)); } // 设置UI加载 windowStage.loadContent('pages/Index', (err) => { // ... if (err.code) { Logger.error('Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? ''); return; } Logger.info('Succeeded in loading the content.'); }); }
页面和自定义组件生命周期
指南:自定义组件的生命周期
API:自定义组件的生命周期
在开始之前,我们先明确自定义组件和页面的关系:
- 自定义组件:@Component或@ComponentV2装饰的UI单元,可以组合多个系统组件实现UI的复用,可以调用组件的生命周期。
- 页面:即应用的UI页面。可以由一个或者多个自定义组件组成,@Entry装饰的自定义组件为页面的入口组件,即页面的根节点,一个页面有且仅能有一个@Entry。只有被@Entry装饰的组件才可以调用页面的生命周期。
页面生命周期,只有 被@Entry装饰的组件(仅router)生命周期,才提供以下生命周期接口:
- onPageShow(仅router):页面每次显示时触发一次,包括路由过程、应用进入前台等场景。
- onPageHide(仅router):页面每次隐藏时触发一次,包括路由过程、应用进入后台等场景。
- onBackPress(仅router):当用户点击返回按钮时触发。
组件生命周期,即用 装饰的自定义组件的生命周期,提供以下生命周期接口:
- aboutToAppear:组件即将出现时回调该接口,具体时机为在创建自定义组件的新实例后,在执行其build()函数之前执行。
- onDidBuild:组件build()函数执行完成之后回调该接口,不建议在onDidBuild函数中更改状态变量、使用animateTo等功能,这可能会导致不稳定的UI表现。
- aboutToDisappear:aboutToDisappear函数在自定义组件析构销毁之前执行。不允许在aboutToDisappear函数中改变状态变量,特别是@Link变量的修改可能会导致应用程序行为不稳定。
- onWillApplyTheme12+:onWillApplyTheme函数用于获取当前组件上下文的Theme对象,在创建自定义组件的新实例后,在
aboutToAppear之前、build()之后执行。允许在onWillApplyTheme函数中改变状态变量,更改将在后续执行build()函数中生效。- *注:每次执行
ThemeControl.setDefaultTheme()改变了主题之后,onWillApplyTheme()回调都会再次执行。处于这个特性,写了这么实现动态切换主题色的代码:动态改变主题色
- *注:每次执行
生命周期流程如下图所示,下图展示的是被 @Entry 装饰的组件(页面)生命周期。

根据上面的流程图,我们从自定义组件的初始创建、重新渲染和删除来详细解释。
自定义组件的创建和渲染流程
- 自定义组件的创建:自定义组件的实例由ArkUI框架创建。
- 初始化自定义组件的成员变量:通过本地默认值或者构造方法传递参数来初始化自定义组件的成员变量,初始化顺序为成员变量的定义顺序。
- 如果开发者定义了aboutToAppear,则执行aboutToAppear方法。
- 在首次渲染的时候,执行build方法渲染系统组件,如果子组件为自定义组件,则创建自定义组件的实例。在首次渲染的过程中,框架会记录状态变量和组件的映射关系,当状态变量改变时,驱动其相关的组件刷新。
- 如果开发者定义了onDidBuild,则执行onDidBuild方法。
自定义组件重新渲染
当事件句柄被触发(比如设置了点击事件,即触发点击事件)改变了状态变量时,或者LocalStorage / AppStorage中的属性更改,并导致绑定的状态变量更改其值时:
- 框架观察到了变化,将启动重新渲染。
- 根据框架持有的两个map(自定义组件的创建和渲染流程中第4步),框架可以知道该状态变量管理了哪些UI组件,以及这些UI组件对应的更新函数。执行这些UI组件的更新函数,实现最小化更新。
自定义组件的删除
如果if组件的分支改变,或者ForEach循环渲染中数组的个数改变,组件将被删除:
- 在删除组件之前,将调用其aboutToDisappear生命周期函数,标记着该节点将要被销毁。ArkUI的节点删除机制是:后端节点直接从组件树上摘下,后端节点被销毁,对前端节点解引用,前端节点已经没有引用时,将被JS虚拟机垃圾回收。
- 自定义组件和它的变量将被删除,如果其有同步的变量,比如@Link、@Prop、@StorageLink,将从同步源上取消注册。
不建议在生命周期aboutToDisappear内使用async await,如果在生命周期的aboutToDisappear使用异步操作(Promise或者回调方法),自定义组件将被保留在Promise的闭包中,直到回调方法被执行完,这个行为阻止了自定义组件的垃圾回收。
以下示例展示了生命周期的调用时机:
// Index.ets
import { router } from '@kit.ArkUI';
@Entry
@Component
struct MyComponent {
@State showChild: boolean = true;
@State btnColor:string = "#FF007DFF"
// 只有被@Entry装饰的组件才可以调用页面的生命周期
onPageShow() {
console.info('Index onPageShow');
}
// 只有被@Entry装饰的组件才可以调用页面的生命周期
onPageHide() {
console.info('Index onPageHide');
}
// 只有被@Entry装饰的组件才可以调用页面的生命周期
onBackPress() {
console.info('Index onBackPress');
this.btnColor ="#FFEE0606"
return true // 返回true表示页面自己处理返回逻辑,不进行页面路由;返回false表示使用默认的路由返回逻辑,不设置返回值按照false处理
}
// 组件生命周期
aboutToAppear() {
console.info('MyComponent aboutToAppear');
}
// 组件生命周期
onDidBuild() {
console.info('MyComponent onDidBuild');
}
// 组件生命周期
aboutToDisappear() {
console.info('MyComponent aboutToDisappear');
}
build() {
Column() {
// this.showChild为true,创建Child子组件,执行Child aboutToAppear
if (this.showChild) {
Child()
}
// this.showChild为false,删除Child子组件,执行Child aboutToDisappear
Button('delete Child')
.margin(20)
.backgroundColor(this.btnColor)
.onClick(() => {
this.showChild = false;
})
// push到page页面,执行onPageHide
Button('push to next page')
.onClick(() => {
router.pushUrl({ url: 'pages/page' });
})
}
}
}
@Component
struct Child {
@State title: string = 'Hello World';
// 组件生命周期
aboutToDisappear() {
console.info('[lifeCycle] Child aboutToDisappear')
}
// 组件生命周期
onDidBuild() {
console.info('[lifeCycle] Child onDidBuild');
}
// 组件生命周期
aboutToAppear() {
console.info('[lifeCycle] Child aboutToAppear')
}
build() {
Text(this.title)
.fontSize(50)
.margin(20)
.onClick(() => {
this.title = 'Hello ArkUI';
})
}
}
// page.ets
@Entry
@Component
struct page {
@State textColor: Color = Color.Black;
@State num: number = 0
onPageShow() {
this.num = 5
}
onPageHide() {
console.log("page onPageHide");
}
onBackPress() { // 不设置返回值按照false处理
this.textColor = Color.Grey
this.num = 0
}
aboutToAppear() {
this.textColor = Color.Blue
}
build() {
Column() {
Text(`num 的值为:${this.num}`)
.fontSize(30)
.fontWeight(FontWeight.Bold)
.fontColor(this.textColor)
.margin(20)
.onClick(() => {
this.num += 5
})
}
.width('100%')
}
}
以上示例中,Index页面包含两个自定义组件,一个是被@Entry装饰的MyComponent,也是页面的入口组件,即页面的根节点;一个是Child,是MyComponent的子组件。只有@Entry装饰的节点才可以使页面级别的生命周期方法生效,因此在MyComponent中声明当前Index页面的页面生命周期函数(onPageShow / onPageHide / onBackPress)。MyComponent和其子组件Child分别声明了各自的组件级别生命周期函数(aboutToAppear / onDidBuild/aboutToDisappear)。
- 应用冷启动的初始化流程为:MyComponent aboutToAppear --> MyComponent build --> MyComponent onDidBuild--> Child aboutToAppear --> Child build --> Child onDidBuild --> Index onPageShow。
- 点击“delete Child”,if绑定的this.showChild变成false,删除Child组件,会执行Child aboutToDisappear方法。
- 点击“push to next page”,调用router.pushUrl接口,跳转到另外一个页面,当前Index页面隐藏,执行页面生命周期Index onPageHide。此处调用的是router.pushUrl接口,Index页面被隐藏,并没有销毁,所以只调用onPageHide。跳转到新页面后,执行初始化新页面的生命周期的流程。
- 如果调用的是router.replaceUrl,则当前Index页面被销毁,执行的生命周期流程将变为:Index onPageHide --> MyComponent aboutToDisappear --> Child aboutToDisappear。上文已经提到,组件的销毁是从组件树上直接摘下子树,所以先调用父组件的aboutToDisappear,再调用子组件的aboutToDisappear,然后执行初始化新页面的生命周期流程。
- 点击返回按钮,触发页面生命周期Index onBackPress,且触发返回一个页面后会导致当前Index页面被销毁。
- 最小化应用或者应用进入后台,触发Index onPageHide。当前Index页面没有被销毁,所以并不会执行组件的aboutToDisappear。应用回到前台,执行Index onPageShow。
- 退出应用,执行Index onPageHide --> MyComponent aboutToDisappear --> Child aboutToDisappear。
自定义组件监听页面生命周期
使用无感监听页面路由的能力,能够实现在自定义组件中监听页面的生命周期。
// Index.ets
import { uiObserver, router, UIObserver } from '@kit.ArkUI';
@Entry
@Component
struct Index {
listener: (info: uiObserver.RouterPageInfo) => void = (info: uiObserver.RouterPageInfo) => {
let routerInfo: uiObserver.RouterPageInfo | undefined = this.queryRouterPageInfo();
if (info.pageId == routerInfo?.pageId) {
if (info.state == uiObserver.RouterPageState.ON_PAGE_SHOW) {
console.log(`Index onPageShow`);
} else if (info.state == uiObserver.RouterPageState.ON_PAGE_HIDE) {
console.log(`Index onPageHide`);
}
}
}
aboutToAppear(): void {
let uiObserver: UIObserver = this.getUIContext().getUIObserver();
uiObserver.on('routerPageUpdate', this.listener);
}
aboutToDisappear(): void {
let uiObserver: UIObserver = this.getUIContext().getUIObserver();
uiObserver.off('routerPageUpdate', this.listener);
}
build() {
Column() {
Text(`this page is ${this.queryRouterPageInfo()?.pageId}`)
.fontSize(25)
Button("push self")
.onClick(() => {
router.pushUrl({
url: 'pages/Index'
})
})
Column() {
SubComponent()
}
}
}
}
@Component
struct SubComponent {
listener: (info: uiObserver.RouterPageInfo) => void = (info: uiObserver.RouterPageInfo) => {
let routerInfo: uiObserver.RouterPageInfo | undefined = this.queryRouterPageInfo();
if (info.pageId == routerInfo?.pageId) {
if (info.state == uiObserver.RouterPageState.ON_PAGE_SHOW) {
console.log(`SubComponent onPageShow`);
} else if (info.state == uiObserver.RouterPageState.ON_PAGE_HIDE) {
console.log(`SubComponent onPageHide`);
}
}
}
aboutToAppear(): void {
let uiObserver: UIObserver = this.getUIContext().getUIObserver();
uiObserver.on('routerPageUpdate', this.listener);
}
aboutToDisappear(): void {
let uiObserver: UIObserver = this.getUIContext().getUIObserver();
uiObserver.off('routerPageUpdate', this.listener);
}
build() {
Column() {
Text(`SubComponent`)
}
}
}
应用进程内存信息
指南:@ohos.hidebug (Debug调试)-ArkTS API-Performance Analysis Kit(性能分析服务)
import { hidebug } from '@kit.PerformanceAnalysisKit'
let nativeMemInfo: hidebug.NativeMemInfo = hidebug.getAppNativeMemInfo()
console.info(
`pss: ${nativeMemInfo.pss}, vss: ${nativeMemInfo.vss}, rss: ${nativeMemInfo.rss}, ` +
`sharedDirty: ${nativeMemInfo.sharedDirty}, privateDirty: ${nativeMemInfo.privateDirty}, ` +
`sharedClean: ${nativeMemInfo.sharedClean}, privateClean: ${nativeMemInfo.privateClean}`)
FAQ:
启动时验证APP签名
论坛:如何在鸿蒙中检测 HAP 是否被二次打包(Repackaging)?
可以考虑在运行时做签名校验,比如:
// import bundleManager from '@kit.BundleManagerKit';
import { bundleManager } from '@kit.AbilityKit';
async function verifySignature() {
// 应用包名
const bundleName = "xxx.xxx.xxx";
// 预存合法证书指纹
const expectedFingerprint = "YOUR_CERT_SHA256";
try {
const bundleFlags = bundleManager.BundleFlag.GET_BUNDLE_INFO_WITH_SIGNATURE_INFO;
const bundleInfo = await bundleManager.getBundleInfo(bundleName, bundleFlags);
const currentFingerprint = bundleInfo.signatureInfo.certificates.sha256;
if (currentFingerprint !== expectedFingerprint) {
console.error("检测到二次打包!签名指纹不匹配");
// 执行安全策略(如退出应用)
}
} catch (err) {
console.error(`签名验证失败: ${err.code}`);
}
}
应用关闭
import { common } from '@kit.AbilityKit';
private UIContext: UIContext = this.getUIContext()
private ApplicationContext: common.ApplicationContext = this.getUIContext().getHostContext() as common.ApplicationContext
private UIAbilityContext: common.UIAbilityContext = this.getUIContext().getHostContext() as common.UIAbilityContext
// 停止Ability自身。(不影响已其他启动的UIAbility)
this.UIAbilityContext.terminateSelf()
.then(()=>{
hilog.info(DOMAIN, TAG, `#onBackPress#terminateSelf() terminate Ability Sucessfully!`);
})
.catch((error: BusinessError) => {
hilog.error(DOMAIN, TAG, `ERROR: #onBackPress#terminateSelf(): ${JSON.stringify(error)}`)
})
// 终止应用的所有进程,进程退出时不会正常走完应用生命周期。
// this.ApplicationContext.killAllProcesses()
// .then()
// .catch()
提示
- 调用
context.terminateSelf()方法停止当前UIAbility实例,默认会保留该实例的快照(Snapshot),即在最近任务列表中仍然能查看到该实例对应的任务。如不需要保留该实例的快照,可以在其对应UIAbility的module.json5配置文件中,将abilities标签的removeMissionAfterTerminate字段配置为true。 - 如需要关闭应用所有的UIAbility实例,也就是整个应用。可以调用ApplicationContext的killAllProcesses()方法实现关闭应用所有的进程。
可能需要:
应用沙箱目录
应用文件目录结构图

通过ApplicationContext获取应用级别的应用文件路径,此路径是应用全局信息推荐的存放路径,这些文件会跟随应用的卸载而删除。
通过AbilityStageContext、UIAbilityContext、ExtensionContext获取HAP级别的应用文件路径。此路径是HAP相关信息推荐的存放路径,这些文件会跟随HAP的卸载而删除,但不会影响应用级别路径的文件,除非该应用的HAP已全部卸载。
应用需要对应用文件目录下的应用文件进行查看、创建、读写、删除、移动、复制、获取属性等访问操作
1.应用文件目录:
- 这是应用在沙箱保护机制下保存文件的主要目录。应用可以在此目录下保存和处理自己的文件,如用户数据、配置文件、图片、媒体文件等 ![引用1]。
- 路径格式为
/data/storage/<APP_NAME>/files,其中<APP_NAME>是应用的包名。
2.应用缓存文件路径: ![][引用1]
- 应用缓存文件路径用于存储应用的缓存数据,如下载的文件、图片缓存、数据库缓存等 ![引用1]。
- 路径格式为
/data/storage/<APP_NAME>/cache。
3.应用首选项数据路径: ![][引用1]
- 应用首选项数据路径用于存储应用的配置文件和首选项数据,如用户设置、偏好配置等。
- 路径格式为
/data/storage/<APP_NAME>/preferences。
4.应用临时文件路径:
- 应用临时文件路径用于存储应用运行期间产生的临时文件,如数据库缓存、图片缓存、临时日志文件等。
- 路径格式为
/data/storage/<APP_NAME>/temp。
5.数据库路径:
- 应用在el2加密条件下存储私有数据库数据的目录为
/data/storage/<APP_NAME>/database![引用1]。
6.分布式文件路径:
- 应用在el2加密条件下存储分布式文件的目录为
/mnt/hmdfs/<USER_ID>/account/merge_view/data/<APP_NAME>![][引用1]。 - 此路径适用于多设备场景下的数据共享和备份。
MVVM
MVVM 将应用分为Model、View和ViewModel三个核心部分,实现数据、视图与逻辑的分离。
- View:负责用户界面展示数据并与用户交互,不包含任何业务逻辑。它通过绑定ViewModel层提供的数据来动态更新UI。
- ViewModel:负责管理UI状态和交互逻辑。作为连接Model和View的桥梁,通常一个View对应一个ViewModel,ViewModel监控Model数据的变化,通知View更新UI,同时处理用户交互事件并转换为数据操作。
- Model:负责存储和管理应用的数据以及业务逻辑,不直接与用户界面交互。通常从后端接口获取数据,是应用程序的数据基础,确保数据的一致性和完整性。
ArkTS
状态管理
指南:状态管理(V1)
指南:状态管理(V2)
最佳实践:状态管理最佳实践-性能优化
V1
@Watch
(多个状态变量可以绑定同一个 @Watch 回调)
@Entry
@Component
struct Index {
@State
@Watch('onNameChanged')
name: string = 'Enlin'
// build(){}
// 方法在自定义组件的属性变更之后同步执行
onNameChanged(changedPropertyName? : string): void{
// `changedPropertyName` 是被 watch 的属性名
}
}
@Observed + @ObjectLink
参考:嵌套对象-按情况使用注解
V2
提示
写了一个垃圾 V2 Demo 项目。
@Monitor
- 在
@ComponentV2装饰的自定义组件中使用@Monitor监听的变量必须需要被@Local、@Param、@Provider、@Consumer、@Computed装饰。 - 同时监听多个状态变量,变量名之间用逗号
,隔开。 - 监听的类对象时,仅能监听对象整体的变化。监听类属性的变化需要类属性被
@Trace装饰。 @Monitor装饰器具有深度监听的能力,能够监听嵌套类、多维数组、对象数组中指定项的变化。对于嵌套类、对象数组中成员属性变化的监听要求该类被@ObservedV2装饰且该属性被@Trace装饰。- 当
@Monitor监听数组整体时,更改数组的某一项不会被监听到。无法监听内置类型(Array、Map、Date、Set)的 API调用 引起的变化。 @Monitor装饰器支持在类中与 @ObservedV2、@Trace配合使用
示例:
监听嵌套对象中的属性
@ObservedV2
class Info {
@Trace public value: number = 50;
}
@ObservedV2
class UIStyle {
public info: Info = new Info();
@Trace public color: Color = Color.Black;
@Trace public fontSize: number = 45;
// 在 `UI` 中也可以这样使用(对象.属性)
@Monitor('info.value')
onValueChange(monitor: IMonitor) {
let beforeValue: number = monitor.value()?.before as number;
let currentValue: number = monitor.value()?.now as number;
}
}
监听多个:
@ObservedV2
class Info {
@Trace name: string = 'Tom';
@Trace age: number = 25;
@Trace height: number = 175;
@Monitor('name') // 监听一个变量
onNameChange(monitor: IMonitor) {
// 未指定value的入参时,默认使用dirty中的第一个路径作为入参
console.info(`path: ${monitor.value()?.path} change from ${monitor.value()?.before} to ${monitor.value()?.now}`);
}
@Monitor('age', 'height') // 监听多个变量
onRecordChange(monitor: IMonitor) {
// 指定 `value` 的入参时,将返回入参路径 `path` 对应的变量变化值信息
monitor.dirty.forEach((path: string) => {
console.info(`path: ${path} change from ${monitor.value(path)?.before} to ${monitor.value(path)?.now}`);
})
}
}
数组:(数组长度)
@ObservedV2
class Info {
@Type(Config)
@Trace configs: Array<Config> = []
@Monitor('configs.length')
onConfigsChanged(monitor: IMonitor){
// 之前的值
let tempBefore = monitor.value<number>()?.before
// 现在的值
let tempNow = monitor.value<number>()?.now
let tempCurrent = this.configs.map(item => item.name).join(',')
}
}
提示
参考:
IMonitor:当监听的变量变化时,状态管理框架侧将回调开发者注册的函数,并传入变化信息。变化信息的类型即为
IMonitor类型。IMonitor有一个名为dirty的数组属性,有一个value<T>(path?: string): IMonitorValue<T> | undefined方法:名称 类型 只读 可选 说明 dirty Array<string> 否 否 变化路径的数组。 IMonitorValue:名称 类型 只读 可选 说明 before T 否 否 变量变化前的值。 now T 否 否 变量当前的值。 path string 否 否 变量的路径。
@Event
语法糖:!!语法:双向绑定
提示
@Param 标志着组件的输入,表明该变量受父组件影响,而 @Event 标志着组件的输出,可以通过该方法影响父组件。使用 @Event 装饰回调方法是一种规范,表明该回调作为自定义组件的输出。父组件需要判断是否提供对应方法用于子组件更改 @Param 变量的数据源。
@Event 用于装饰组件对外输出的方法:
@Event装饰的回调方法中参数以及返回值由开发者决定。@Event装饰非回调类型的变量不会生效。当@Event没有初始化时,会自动生成一个空的函数作为默认回调。- 当
@Event未被外部初始化,但本地有默认值时,会使用本地默认的函数进行处理。 - 使用
@Event修改父组件的值是立刻生效的,但从父组件将变化同步回子组件的过程是异步的,即在调用完@Event的方法后,子组件内的值 不会立刻变化 。这是因为@Event将子组件值实际的变化能力交由父组件处理,在父组件实际决定如何处理后,将最终值在渲染之前同步回子组件。
装饰器说明:
| @Event | 说明 |
|---|---|
| 参数 | 无。 |
| 允许装饰的变量类型 | 回调方法,例如 () => void、(x:number) => boolean 等。是否含有参数以及返回值由开发者决定。 |
| 允许传入的函数类型 | 箭头函数。 |
示例:
@ComponentV2
struct Child {
@Param index: number = 0;
@Event changeIndex: (val: number) => void;
build() {
Column() {
Text(`Child index: ${this.index}`)
.onClick(() => {
this.changeIndex(20);
hilog.info(DOMAIN, TAG, `after changeIndex ${this.index}`);
})
}
}
}
@Entry
@ComponentV2
struct Index {
@Local index: number = 0;
build() {
Column() {
Child({
index: this.index,
changeIndex: (val: number) => {
this.index = val;
hilog.info(DOMAIN, TAG, `in changeIndex ${this.index}`);
}
})
}
}
}
在上面的示例中,点击文字触发@Event函数事件改变子组件的值,打印出的日志为:
in changeIndex 20
after changeIndex 0
这表明在调用changeIndex之后,父组件中index的值已经变化,但子组件中的index值还没有同步变化。
@ObservedV2 + @Trace
@ObservedV2 用于装饰类,@Trace 用于装饰类中的属性,使得被装饰的类和属性具有深度观测的能力:
@ObservedV2装饰器必须与@Trace装饰器需要配合使用,单独使用@ObservedV2装饰器或@Trace装饰器没有任何作用。- 被
@Trace装饰的属性property变化时,仅会通知property关联的组件进行刷新。 - 在嵌套类中,嵌套类中的属性property被
@Trace装饰且嵌套类被@ObservedV2装饰时,才具有触发UI刷新的能力。 - 在继承类中,父类或子类中的属性property被
@Trace装饰且该property所在类被@ObservedV2装饰时,才具有触发UI刷新的能力。 - 未被
@Trace装饰的属性用在UI中无法感知到变化,也无法触发UI刷新。 - 使用
@ObservedV2与@Trace装饰器的类,需通过new操作符 实例化 后,才具备被观测变化的能力。
装饰器说明:
| @ObservedV2类装饰器 | 说明 |
|---|---|
| 装饰器参数 | 无。 |
| 类装饰器 | 装饰class。需要放在class的定义前,使用new创建类对象。 |
| @Trace成员变量装饰器 | 说明 |
|---|---|
| 装饰器参数 | 无。 |
| 可装饰的变量 | class中成员属性。属性的类型可以为number、string、boolean、class、Array、Date、Map、Set 等类型。 |
@Trace 装饰内置类型时,可以观测各自API导致的变化:
| 类型 | 可观测变化的API |
|---|---|
| Array | push、pop、shift、unshift、splice、copyWithin、fill、reverse、sort |
| Date | setFullYear, setMonth, setDate, setHours, setMinutes, setSeconds, setMilliseconds, setTime, setUTCFullYear, setUTCMonth, setUTCDate, setUTCHours, setUTCMinutes, setUTCSeconds, setUTCMilliseconds |
| Map | set, clear, delete |
| Set | add, clear, delete |
示例代码:
@ObservedV2
class Son {
@Trace public age: number = 100;
}
class Father {
public son: Son = new Son();
}
@Entry
@ComponentV2
struct Index {
father: Father = new Father();
build() {
Column() {
// 当点击改变age时,Text组件会刷新
Text(`${this.father.son.age}`)
.onClick(() => {
this.father.son.age++;
})
}
}
}
@Computed
getter方法装饰器,类似Vue的计算属性。会检测其中被计算(仅状态变量)的属性变化。可用于初始化子组件的
@Param属性。装饰的属性是只读的,无法与双向绑定连用:
// 正确用法 @Computed get fullName() { return this.firstName + ' ' + this.lastName } // 错误:装饰的属性是只读的,无法与双向绑定连用,编译时报错 Child({ fullName: this.fullName!! }) // 错误:装饰的属性是只读的,开发者自己实现的setter不生效,编译时报错 set fullName(newName : string) { this.fullName = newName }多个@Computed一起使用时,警惕循环求解,以防止计算过程中的死循环:
@Local a : number = 1; @Computed get b() { return this.a + ' ' + this.c; // 错误写法,存在循环b -> c -> b } @Computed get c() { return this.a + ' ' + this.b; // 错误写法,存在循环c -> b -> c }可以在 @ObservedV2 装饰的类中使用计算属性
@Computed装饰的属性可以被
@Monitor监听变化
AppStorageV2
API:AppStorageV2
AppStorageV2使用 connect 接口即可实现对AppStorageV2中数据的修改和同步,如果修改的数据被 @Trace 装饰,该数据的修改会同步更新UI。
提示
connect 可以理解为 getOrCrate() ,有就 get 没有就 create ;
第一个参数
type:指定的类型,若未指定key,则使用type的name作为key。name可以看 这里 。指定的类型是一个构造器,在TS中,这叫 构造签名接口 ,构造器就是类的名字,不带括号,例如:class Student{ ... } // 定义 static connect<T extends object>(type: TypeConstructorWithArgs<T>, keyOrDefaultCreator?: string | StorageDefaultCreator<T>, defaultCreator?: StorageDefaultCreator<T>): T | undefined; // 使用:第一个参数 其实就是构造器; // 第三个参数是箭头函数,但返回值要和第一个参数类型匹配。 PersistenceV2.connect(Student, 'student', () => new Student())
猜测这么设计的原因有二:
- 第二个参数没有指定 key 的话,将
构造器.name设置为 key。例如Student.name结果为"Student"。 - 确保第三个参数的箭头函数返回值类型和第一个参数类型相同。
需要注意的是,使用 remove 接口只会将数据从AppStorageV2中删除,不影响组件中已创建的数据。
PersistenceV2
提示
继承自 AppStorageV2 。
API:PersistenceV2
菜狗Demo项目:V2 ,这里也有 PersistenceV2 的用法。
PersistenceV2是应用级单例对象。
对于与PersistenceV2关联的 @ObservedV2 对象,该对象的 @Trace 属性的变化,会触发 整个关联对象的自动持久化 ;非 @Trace 属性的变化则不会,如有必要,可调用PersistenceV2 API手动持久化。
重要
被PersistenceV2持久化的类属性必须要有初值,否则不支持持久化。
PersistenceV2可以和UI组件同步,且可以在应用业务逻辑中被访问。
PersistenceV2支持应用的 主线程 内多个UIAbility实例间的状态共享。
重要
持久化的对象为 Object class以及Array、Date、Map、Set等内嵌类型 时,为了实现序列化类时不丢失属性的复杂类型,需要配合 PersistenceV2 使用 @Type 装饰器装饰类属性:
@ObservedV2
class Setting {
// ...
}
@ObservedV2
class AppConfig {
// 重要!!!否则应用程序在下次冷启动时会反序列化失败导致启动失败(会产生FaultLog)。
@Type(Setting)
@Trace settings: Array<Setting> = []
}
按情况使用注解
提示
以下示例均使用 V1 进行举例,V2 不适用。
简单类型 和 复杂类型 必须要选择 合适的状态管理 注解才能达到合适效果。
- 基础数据类型(包括数组)、对象(非嵌套情况)
非嵌套 意思是对象内属性类型都是基础类型。
- 父组件:
@State age: number = 10,本组件内数据驱动UI,修改此属性值可以驱动对应组件刷新。 - 子组件:
@Link age: number,父子双向绑定,父/子修改均会通知对方数据更新,进而驱动对应组件刷新。 - 子组件:
@Prop age: number,父子单向绑定,父修改单向通知子组件数据更新/组件刷新。子组件修改不通知父组件更新。
数组对象、嵌套对象等
对于嵌套情况,被嵌套的对象就要在定义class的时候使用
Observed修饰。否则对象数组内的对象数组[index].属性 = 新值发生改变不会被观察到,也就不会驱动刷新UI,因为这个索引指向的对象地址并没有发生变化。数组对象
定义class
// 由于是数组包裹对象,所以Item相对于数组来说就是被嵌套的对象 @Observed class Item { // 省略属性 // constructor() { ... } }父组件:和基础类型定义时一样
@State itemList: Item[] = [ new Item(), new Item(), new Item() ]子组件:如果需要在页面中展示该对象的某个属性是否变化,需要单独写一个子组件,父组件将对象传给子组件,子组件中这个对象属性使用
@ObjectLink修饰。@Component struct ItemComponent { @ObjectLink item: Item; build() { // 展示嵌套对象的属性 Text(`${this.item.xxx }`) } }
嵌套对象
定义class
class Parent { // 一些基础数据类型属性 // 1. 这些基础数据类型在第1条“对象(非嵌套情况)”下,修改这些属性值UI会更新。 no: number title: string // 对象属性(这一步就算是嵌套对象了,需要注意下方注释2说明的例外) // 2. 如果直接给对象属性child整体重新赋值为一个新的Child对象,会驱动UI更新。 // 3. 如果仅给child中的某个属性重新赋值,不能驱动UI更新。 // 这个和你问题中数组对象不刷新可以当做同一种问题来对待。根源就是这个对象内存地址并没有发生变化,在Parent这一层就被视为数据没有发生变化。 child: Child // 构造器 // constructor() { ... } } // Child 是被嵌套对象 @Observed class Child { // 省略属性 // constructor() { ... } } // 如果继续嵌套 那么在UI上也要继续写子孙组件的嵌套,这样页面才会被正确驱动更新 // @Observed // class GrandChild { // // 省略属性 // // constructor() { ... } // }父子组件UI编写参考数组对象。
重要
总之,只要对象中有嵌套情况了,被嵌套的对象就要加上
@Observed注解,并且UI上也要同步编写这种嵌套层级的父子孙组件代码,UI中子对象使用@ObjectLink/@Prop。@ObjectLink:父 --- 引用传递 --> 子,双向同步。@Prop:父 --- 深拷贝 --> 子,单向同步。
V1 对比 V2
提示
推荐先查看 状态管理V1和V2更新机制差异 以更好理解其中的部分差异。
在状态管理V2中使用 animateTo 动画效果异常
警告
当在 animationTo 执行之前,更改了状态变量的值,V2 中使用 animationTo 不会达到 V1 中的预期效果。由于当前 animateTo 与 V2 的刷新机制不兼容,执行动画前的额外修改未生效。
原因:与状态管理V1不同的是,状态管理V2修改完状态变量后不会立即 标脏 ,而是抛出一个Promise微任务(优先级低于宏任务),该微任务在当前宏任务执行完成后才会处理自定义组件标脏,具体差异可参考 V1状态变量更新和V2状态变量更新差异 。而 animateTo 动效会立刻刷新已标脏节点来决定动效首帧。如果动效中使用了V2状态变量,并且在动效前修改了该状态变量,由于调用 animateTo 时状态变量的变化尚未标脏,这会导致 animateTo 的动效首帧不符合预期。为此,API 22 引入 applySync、flushUpdates 和 flushUIUpdates 接口,实现状态管理V2的同步标脏,确保动效达到预期效果。
使用 applySync22+ / flushUpdates22+ / flushUIUpdates22+ 接口需要导入 UIUtils 工具。
import { UIUtils } from '@kit.ArkUI';
applySync接口用于同步刷新指定的状态变量,该接口接收一个闭包函数,仅刷新闭包函数内的修改,包括更新 @Computed 计算、@Monitor 回调以及重新渲染UI节点。flushUpdates接口用于同步刷新在调用该函数之前所有的状态变量修改,包括更新@Computed计算、@Monitor回调以及重新渲染UI节点。- 上述的
applySync、flushUpdates接口都会同步执行@Computed计算和@Monitor回调,这会使得在上述示例代码中,一次点击事件里触发了两次@Monitor回调,这可能会与开发者的预期不符,因此引入了flushUIUpdates接口,该接口仅用于同步刷新在调用该函数之前所有的UI节点,不会执行@Computed计算和@Monitor回调。
组件内状态
| 功能 | V1 | V2 |
|---|---|---|
| 注解 | @State | @Local |
| 本地初始化 | 必须 | 必须 |
| 从父组件初始化 | 可选,优先外部 | false |
| 与父组件同步 | false | false |
| 用于初始化子组件 | 常规变量、@State、@Link、@Prop、@Provide 变量 | @Param 变量 |
| 观察变化 | 仅能观察第一层,也就是 Object.keys(obj) 返回的所有属性。无法深度观测;内置对象整体赋值以及自有API( set、push 等方法); | 仅能观察对象整体赋值; 成员属性、嵌套对象属性依赖 @ObservedV2 类装饰器 和 @Trace 成员变量装饰器; 内置对象整体赋值以及自有API( set、push 等方法); |
| 数据传递 | 可以作为数据源和子组件中状态变量同步。 | 可以作为数据源和子组件中状态变量同步。 |
组件外部输入
V1 中的局限性
状态管理V1存在多种可接受外部传入的装饰器,常用的有@State、@Prop、@Link、@ObjectLink。这些装饰器使用有限制且不易区分,不当使用会导致性能问题。
@Observed
class Region {
public x: number;
public y: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
}
}
@Observed
class Info {
public region: Region;
constructor(x: number, y: number) {
this.region = new Region(x, y);
}
}
@Entry
@Component
struct Index {
@State info: Info = new Info(0, 0);
build() {
Column() {
Button('change Info')
.onClick(() => {
this.info = new Info(100, 100);
})
Child({
region: this.info.region,
regionProp: this.info.region,
infoProp: this.info,
infoLink: this.info,
infoState: this.info
})
}
}
}
@Component
struct Child {
@ObjectLink region: Region;
@Prop regionProp: Region;
@Prop infoProp: Info;
@Link infoLink: Info;
@State infoState: Info = new Info(1, 1);
build() {
Column() {
Text(`ObjectLink region: ${this.region.x}-${this.region.y}`)
Text(`Prop regionProp: ${this.regionProp.x}-${this.regionProp.y}`)
}
}
}
在上面的示例中:
@State仅能在初始化时接收info的引用,改变info之后无法同步。@Prop虽然能够进行单向同步,但是对于较复杂的类型来说,深拷贝性能较差。@Link能够接受传入的引用进行双向同步,但它必须要求数据源也是状态变量,因此无法接受info中的成员属性region。@ObjectLink能够接受类成员属性,但是要求该属性类型必须为@Observed装饰的类。
装饰器的不同限制使得父子组件之间的传值规则复杂、不易使用。因此推出 @Param 装饰器,表示组件从外部传入的状态。
| 功能 | V1 | V1 | V2 |
|---|---|---|---|
| 注解 | @Prop(父 -> 子单向) | @Link(父子双向) | @Param(父 -> 子单向) |
| 本地初始化 | true (@Require 必须父组件构造传参) | false | true |
| 本地修改 | true | true | 基本类型不允许; 具体查看下方 @Param 拓展知识点 |
| 从父组件初始化 | 本地初始化后,可选; 本地未初始化,必选; | 必选 | 可选; 本地未初始化,必选; 优先外部; |
| 用于初始化子组件 | 常规变量、@State、@Link、@Prop、@Provide 变量 | 同 @Prop; 从父组件初始化 @State 的语法为: Child({ age: this.value}) 或Child({ age : $value}) | @Param 变量 |
| 观察变化 | 同 @State | 同 @Prop | 同 @Local |
| 数据传递 | 父 -> 子单向 | 父子双向 | 父 -> 子单向 利用 @Event 实现双向,具体查看下方 @Param 拓展知识点 |
@Prop 类型丢失
警告
@Prop装饰变量时会进行深拷贝,在拷贝的过程中除了基本类型、Map、Set、Date、Array外,都会丢失类型。例如,对于通过NAPI提供的复杂类型(如 PixelMap ),由于其部分实现在Native侧,因此无法在ArkTS侧通过深拷贝获得完整的数据;同样,RegExp类型在拷贝过程中会丢失原类型,导致被@Prop装饰后无法调用正则相关函数。
@Param 拓展知识点
如果装饰的变量是对象类型,在子组件中可以修改对象的属性。
若需要修改值:
@Monitor 对比 @Watch
| 用法 | @Watch | @Monitor |
|---|---|---|
| 参数 | 回调方法名。 | 监听状态变量名、属性名。 |
| 监听目标数 | 只能监听单个状态变量。 | 能同时监听多个状态变量。 |
| 监听能力 | 跟随状态变量观察能力(一层)。 | 跟随状态变量观察能力(深层)。 |
| 能否获取变化前的值 | 不能获取变化前的值。 | 能获取变化前的值。 |
| 监听条件 | 监听对象为状态变量。 | 监听对象为 状态变量 或为 @Trace 装饰的类成员属性。 |
| 使用限制 | 仅能在@Component装饰的自定义组件中使用。 | 能在@ComponentV2装饰的自定义组件中使用,也能在@ObservedV2装饰的类中使用。 |
任务并发调度
Function Flow Runtime Kit(任务并发调度服务)-基础功能-系统
ArkUI
导航
动态改变主题色
在 AppTheme.ets 定义好主题之后,在 Index.ets 的 aboutToAppear 方法内使用 ThemeControl.setDefaultTheme 方法就可以更改当前主题,利用调用 ThemeControl.setDefaultTheme 方法 onWillApplyTheme() 回调就会执行的特性,给 @Provide('key') 所修饰变量赋值来使其子组件(使用@Consume('key'))得到新的主题色变更。
注
需确保 ThemeControl.setDefaultTheme() 接口在页面build前执行。若在UIAbility中使用此接口设置应用级默认主题,需确保该接口在onWindowStageCreate阶段里windowStage.loadContent接口调用完成的回调函数中执行。详细代码可参考设置应用内组件自定义主题色。
AppTheme.etsimport { CustomColors, CustomTheme } from '@ohos.arkui.theme' import { HashMap } from '@kit.ArkTS' /* 主题定义 多个主题就创建多个*/ /* 鸿蒙蓝 blueAppTheme */ // 主题颜色 class BlueAppThemeColors implements CustomColors { // 这里以icon主题色为例 iconEmphasize: ResourceColor = $r('sys.color.icon_emphasize') // fontPrimary等颜色 } // 主题配置 class BlueAppTheme implements CustomTheme { public colors: BlueAppThemeColors = new BlueAppThemeColors() } /* 红色 redTheme */ // 主题颜色 class RedAppThemeColors implements CustomColors { iconEmphasize: ResourceColor = Color.Red } // 主题配置 class RedAppTheme implements CustomTheme { public colors: BlueAppThemeColors = new RedAppThemeColors() } /* 导出 */ // 自定义主题名称 TODO 补充不提名称字符串 export type CustomThemeName = 'blueAppTheme' | 'redAppTheme' // 自定义主题HashMap let _CUSTOM_THEME_MAP: HashMap<CustomThemeName, CustomTheme> = new HashMap<CustomThemeName, CustomTheme>() _CUSTOM_THEME_MAP.set('blueAppTheme', new BlueAppTheme()) _CUSTOM_THEME_MAP.set('redAppTheme', new RedAppTheme()) export const CUSTOM_THEME_MAP = _CUSTOM_THEME_MAPIndex.etsimport { Theme, ThemeControl, CustomColors, Colors, CustomTheme, CustomDarkColors } from '@kit.ArkUI' import { CustomThemeName, CUSTOM_THEME_MAP } from '../common/AppTheme' // 官方示例实在这里直接改的主题,测试的时候发现`aboutToAppear()`回调执行早于`onWillApplyTheme()`,索性直接在`aboutToAppear`里写了 // ThemeControl.setDefaultTheme('xxx') @Entry @Component struct Index { // @StorageProp('selectedTheme') // TODO 使用用户首选项之后 删掉`@State`,取消注释上面的`@StorageProp` @State selectedTheme: CustomThemeName = 'redAppTheme' // 系统颜色调用(现在每个主题中只有一个颜色,如果有多个颜色,可以把这个换成主题色对应的`CustomTheme`) @Provide('icon_emphasize') icon_emphasize: ResourceColor = $r('sys.color.icon_emphasize') build(){} /* * 生命周期方法:在build()函数执行前调用。允许在本函数中改变状态变量,更改将在后续执行build()函数中生效。 */ aboutToAppear() { // 在页面build前执行ThemeControl,就可以改变主题颜色 ThemeControl.setDefaultTheme(CUSTOM_THEME_MAP.get(this.selectedTheme)) hilog.info(0x1000, this.componentName, `#aboutToAppear#ThemeControl.setDefaultTheme(${this.selectedTheme})`) } /** * 生命周期方法:获取当前组件上下文的Theme对象,在build()函数之前执行。允许在onWillApplyTheme函数中改变状态变量,更改将在后续执行build()函数中生效。 * @param theme 当前启用的主题 */ onWillApplyTheme(theme: Theme) { hilog.info(0x1000, this.componentName, `#onWillApplyTheme`) this.icon_emphasize = theme.colors.iconEmphasize; } }
UI单位
问:UI布局默认是多少 vp 为基准,以达到不同机器自适应
无论屏幕分辨率或密度如何,组件的视觉效果保持一致。
vp具体计算公式为:vp= px/(DPI/160)
px 是屏幕的真实物理像素值,densityDPI 通常指系统屏幕密度,densityPixels 是屏幕密度与标准DPI的比率,常见取值有 0.75、1.0、1.5、2.0、3.0 等。在HarmonyOS中,标准DPI为 160 。以华为Mate 40 Pro为例, densityDPI 为 560,densityPixels 为 3.5。要查看真机的DPI,可以调用屏幕属性中的 display 接口查询。
import { display } from '@kit.ArkUI';
let displayClass: display.Display | null = null;
try {
displayClass = display.getDefaultDisplaySync();
} catch (exception) {
console.error('Failed to obtain the default display object. Code: ' + JSON.stringify(exception));
}
如果原型图没有提供vp单位的布局,开发者可以根据 densityPixels 把 px转为vp,HarmonyOS也封装了现成的接口 px2vp() 和 vp2px() 供开发者直接调用。
参考链接
连续返回两次退出应用
前提:自定义组件需要被 @Entry 修饰
示例代码:
import { hilog } from '@kit.PerformanceAnalysisKit';
import { systemDateTime } from '@kit.BasicServicesKit';
import { PromptAction } from '@kit.ArkUI';
import { common } from '@kit.AbilityKit';
const DOMAIN = 0xfff0
const TAG = `NavigationExample`
@Entry
@Component
struct NavigationExample {
// Context
private UIContext: UIContext = this.getUIContext()
private ApplicationContext: common.ApplicationContext = this.getUIContext().getHostContext() as common.ApplicationContext
private UIAbilityContext: common.UIAbilityContext = this.getUIContext().getHostContext() as common.UIAbilityContext
private promptAction : PromptAction = this.UIContext.getPromptAction()
// Navigation 页面栈
pageInfos: NavPathStack = new NavPathStack();
// 记录触发返回时的时间
exitTime: number = 0
build() {
Navigation(this.pageInfos) {
// ...
}
}
/**
* 在router路由页面(即@Entry装饰的自定义组件)生效,当用户点击返回按钮时触发。
* @returns 返回true表示页面自己处理返回逻辑,不进行页面路由;返回false表示使用默认的路由返回逻辑,不设置返回值按照false处理。
*/
onBackPress() {
hilog.info(DOMAIN, TAG, `#onBackPress() executed`);
let currentTime = systemDateTime.getTime(false)
// 两次返回操作时间间隔,当前为大于2000ms就执行退出Ability操作。按照业务需求修改此间隔
if (currentTime - this.exitTime > 2000) {
this.exitTime = currentTime
this.promptAction.showToast({ message: '再次返回将退出应用', duration: 2000 })
return true
} else {
// 停止Ability自身。(不影响已其他启动的UIAbility)
this.UIAbilityContext.terminateSelf()
.then(()=>{
hilog.info(DOMAIN, TAG, `#onBackPress#terminateSelf() terminate Ability Sucessfully!`);
})
.catch((error: BusinessError) => {
hilog.error(DOMAIN, TAG, `ERROR: #onBackPress#terminateSelf(): ${JSON.stringify(error)}`)
})
// 终止应用的所有进程,进程退出时不会正常走完应用生命周期。
// this.ApplicationContext.killAllProcesses()
// .then()
// .catch()
return false
}
}
}
提示
如果退出UIAbility之后不想在后台保留快照(Snapshot),可以参考应用关闭,并关注最下方的tip。
Native
指南:
参考:
参考我自己的项目(多.cpp):audio-recorder
N-API使用指导:napi-guidelines.md - OpenHarmony/docs简易Native C++ 示例(单.cpp):NativeTemplateDemo - OpenHarmony Codelabs
示例
native项目(多.cpp):NativeAPI/XComponent示例
native项目(多.cpp):Native/NdkXComponent
逆向 & 反编译
仓库:ohos-decompiler/abc-decompiler
提示
GitCode镜像:abc-decompiler/releases
使用方法
下载发行版:
jadx-dev-all.jar使用鸿蒙SDK拆包工具拆包出
.abc文件# 1. 进入鸿蒙工具链文件夹 cd \DevEco Studio\sdk\default\openharmony\toolchains\lib # 执行解包命令 java -jar app_unpacking_tool.jar --mode hap --hap-path <path> --out-path <path> [--force true]指令 是否必选项 选项 描述 --mode 是 app | hap 拆包类型。 --hap-path 是 NA HAP包路径。 --rpcid 否 true或者false 是否单独将rpcid文件从HAP包中提取到指定目录。如果为true,将仅提取rpcid文件,不对HAP包进行拆包。(app模式时不可用) --out-path 是 NA 拆包目标文件路径。 --force 否 true或者false 默认值为false。如果为true,表示当目标文件存在时,强制删除。 运行逆向工具
# path 是 `.abc` 的路径 java -jar jadx-dev-all.jar <path>或者先运行
jar,然后把.abc拖进工具里java -jar jadx-dev-all.jar
提示
反编译成功可能需要:方舟字节码函数命名规则
DevEco Studio
提示
快捷键请参考:IDEA快捷键
链接:
