作为HarmonyOS应用开发的核心架构,Stage模型为复杂应用提供了高效、灵活且可扩展的设计范式。本章从Stage模型的基本概念切入,系统阐述其设计理念与核心优势。作为FA模型的演进版本,Stage模型通过共享ArkTS引擎实例,显著降低了多组件协同时的内存开销,同时原生支持跨端迁移与多设备协同,为分布式场景下的应用开发奠定基础。本章深入解析应用程序包(HAP、HAR、HSP)的模块化设计机制,探讨如何通过多Module划分实现功能解耦与按需加载,并对比不同包类型的适用场景与优化策略。UIAbility组件作为用户交互的核心载体,其生命周期管理、启动模式(单实例、多实例、指定实例)及跨组件通信(EventHub、状态管理)是本章的重点内容。此外,通过AbilityStage容器与进程模型的剖析,揭示应用资源调度与多线程协作的高效机制。最后,结合“应用间跳转”“拉起系统相机”等实战案例,完整呈现Stage模型从理论到实践的开发链路,帮助开发者掌握复杂应用架构设计、性能优化与跨端适配的核心能力,为构建高性能、高可维护的HarmonyOS应用提供全面指导。
7.1 基本概念
本节探讨了HarmonyOS应用程序框架和应用模型的核心概念。首先,介绍了应用程序框架及其在应用开发中的作用,分析了其如何通过简化开发过程、提高代码重用性和可维护性来支持开发者实现高效应用。接着,讲解了应用模型的演变,尤其是Stage模型的设计目标和优势,包括对复杂应用的优化、跨端迁移能力、多设备协同支持等特性。最后,进一步概述了Stage模型中的关键概念,如UIAbility、ExtensionAbility、HAP和AbilityStage,帮助开发者全面理解模块间的关系和运行机制,提供更清晰的开发思路和技术路径。
7.1.1 应用程序框架和应用模型
- 应用程序框架
应用程序框架(Application Framework)是一种编程框架,用于简化应用程序的开发过程,提高代码的可重用性和可维护性。它可以帮助开发人员更快速、更高效地开发应用程序。
应用程序框架也是连接开发者和用户的桥梁。对于用户,将应用安装到设备上并打开应用、启动收付款页面、在当前应用中启动浏览器打开网站、启动文件应用浏览或编辑文件等行为,都是需要开发者借助应用程序框架的能力来实现的。对于开发者,使用应用程序框架所提供的能力,如应用进程的创建与销毁、应用组件的运行入口、生命周期调度、组件间的交互等,是实现用户功能的关键。
- 应用模型
应用模型是一个应用程序的抽象描述,用于表示应用程序的不同方面,如应用组件、进程模型、线程模型、任务管理和包管理。应用模型提供了一种统一的语言和架构来描述应用程序的各个方面,有助于开发者更好地理解应用程序的架构和设计,从而更有效地利用应用程序框架。
随着系统的发展,先后提供了FA模型和Stage模型。
- FA(Feature Ability)模型:从API 7开始支持,但已不再作为主推模型。
- Stage模型:从API 9开始新增,是目前主推的模型,并将长期演进。在该模型中,提供了AbilityState、WindowStage等类,作为应用组件和窗口的“舞台”,因此称为Stage模型。
两种模型的最大区别在于:在Stage模型中,多个应用组件共享同一个ArkTS引擎实例;而在FA模型中,每个应用组件独立拥有一个ArkTS引擎实例。因此,Stage模型使得应用组件之间可以方便地共享对象和状态,并减少复杂应用对内存的占用。
- 二者的区别
应用程序框架可以被看做是应用模型的一种实现方式。开发人员可以根据应用模型描述应用程序的结构和行为,然后使用应用程序框架来实现这些描述。
7.1.2 Stage模型设计目标
- 为复杂应用而设计
在Stage模型中,多个应用组件共享同一个ArkTS引擎实例,应用组件之间可以方便地共享对象和状态,这样可以减少复杂应用运行时对内存的占用。另外,在开发过程中采用面向对象的开发方式,可以使得复杂应用的代码更具可读性、易维护性和可扩展性。
- 原生支持应用组件级的跨端迁移和多端协同
Stage模型提供了应用组件间通信的RPC调用能力。因此,在不同设备上的应用组件可以便捷地迁移应用数据和状态。由于UI和UIAbility之间的分离,可以利用ArkUI的声明式特点,通过组件中数据和状态的变化,更新用户界面,从而实现跨端迁移和多端协同。
- 支持多设备和多窗口形态
Stage模型在框架层面实现了应用组件管理和窗口管理的解耦,为系统提供了对应用组件进行裁剪和扩展窗口形态的能力。同时,Stage模型为应用组件设计了一套生命周期管理机制,以适应不同设备,从而减少应用组件在不同设备上的行为差异。
- 平衡应用能力和系统管控成本
Stage模型拥有后台应用程序治理机制,防止应用程序随意滞留在后台。同时,应用后台行为会受到管理,避免恶意应用行为的发生。此外,Stage模型还为卡片服务、输入法服务等提供了特定的应用组件,以满足更多的开发场景。
7.1.3 Stage模型有关概念
7.1.3.1 基本概念
- UIAblility组件
UIAblility组件是一种包含UI的应用组件,主要是用于和用户交互。在开发态新建一个项目时,IDE默认生成一个持有UlAbility组件的Model作为应用的主模块,该模块用于实现应用的入口界面和主特性功能。对于UIAblility组件,系统提供了具体的UlAbility类承载,支持面向对象的开发方式,如图7-1所示,系统默认生成的EntryAbility类继承了UlAbility类。
图7-1 EntryAbility示意图
- ExtensionAbility组件
ExtensionAbility组件是一种面向特定场景的应用组件,每一个具体场景对应一个ExtensionAbilityType,开发者只能使用系统已定义的类型,各类型的ExtensionAbility组件均由相应的系统服务统一管理,例如InputMethodExtensionAbility组件由输入法管理服务统一管理。目前ExtensionAbility组件类型有11种,在IDE中支持选中Module后,创建不同类型的ExtensionAbility组件文件,如图7-2所示。
图7-2 ExtensionAbility组件示意图
- HAP
包含UlAbility组件或者ExtensionAbility组件的Module可以单独运行。该类型Module运行时会编译生成一个.hap文件,即HAP,如图7-3所示。
图7-3 HAP包示意图
- AbilityStage
HAP是应用安装的基本单位,一个APP可以包含一个或多个HAP,当HAP中的代码首次被加载到进程中时,也就是Module初始化时,系统首先会创建一个AbilityStage实例,AbilityStage是一个Module级别的组件容器,可以管理Module中的UlAbility组件和ExtensionAbility组件,Abilitystage与编译期的HAP是一一对应的关系。
7.1.3.2 运营期和编译期概览
通过开发态中的项目结构,我们了解了UlAbility、ExtensionAbility、HAP、Abilitystage这4个概念,下面我们将学习这些概念在运行期和编译期的关系。如图7-4所示。
图7-4 运行期和编译期概念图
AbilityStage 是一个模块(Module)级别的组件容器,与编译期的HAP具有一一对应的关系。在开发时,一个项目可能需要包含一个或多个可以独立运行的模块(Module)。对应到运行期,一个应用(Application)可能包含一个或多个AbilityStage;而在编译期,一个应用(APP)可能包含一个或多个HAP。
在运行期,每个AbilityStage持有该模块中定义的UIAbility组件和ExtensionAbility组件。当UIAbility组件首次启动时,系统会为其创建一个实例,并将该实例与持有它的AbilityStage实例关联。开发者可以通过AbilityStage获取该UIAbility实例的运行时信息。UIAbility实例创建后,系统会为该实例创建一个WindowStage实例,并与其一对一绑定。WindowStage在应用进程中充当窗口管理器的角色,持有一个主窗口,为ArkUI提供绘制区域,并管理多个ArkUI页面。
在运行期,系统为Application、AbilityStage、UIAbility组件和ExtensionAbility组件分别提供了对应的上下文环境,即ApplicationContext、AbilityStageContext、UIAbilityContext和ExtensionAbilityContext。开发者可以通过这些上下文环境调用各种资源和系统能力。
在运行期和编译期的基本概念中,UIAbility组件尤为重要。UIAbility组件的核心是其生命周期。当用户启动、使用或退出应用时,应用的UIAbility实例会在其生命周期的不同状态之间进行转换。
7.2 应用程序包
本节介绍了HarmonyOS应用程序包的设计与管理机制,帮助开发者理解如何组织、管理和优化应用程序包。首先,我们概述了应用与应用程序包的关系,强调了应用包的模块化设计和跨设备适配能力。接着,讲解了Module的类型,包括Ability、Library等模块类型及其编译后生成的HAP、HAR、HSP文件。通过分析各类模块及其使用场景,读者能深入理解如何选择合适的包类型来满足不同功能需求。最后,结合Stage模型,我们探讨了开发、编译和发布阶段的包结构,进一步解释了如何优化应用包的大小和性能,确保应用在多设备和多场景下的高效运行。
7.2.1 概述
在基于Stage模型开发应用之前,开发者需要了解应用的设计机制、应用程序包结构等基础知识。
7.2.1.1 应用与应用程序包
用户应用程序泛指运行在设备的操作系统之上,为用户提供特定服务的程序,简称“应用”。一个应用所对应的软件包文件,称为“应用程序包”。
当前系统提供了应用程序包开发、安装、查询、更新、卸载的管理机制,便于开发者开发和管理应用。同时,系统还屏蔽了不同的芯片平台的差异(包括x86/ARM,32位/64位等),应用程序包在不同的芯片平台都能够安装运行,这使得开发者可以聚焦于应用的功能实现。
7.2.1.2 应用的多Module设计机制
支持模块化开发: 一个应用通常会包含多种功能,将不同的功能特性按模块来划分和管理是一种良好的设计方式。在开发过程中,我们可以将每个功能模块作为一个独立的Module进行开发,Module中可以包含源代码、资源文件、第三方库、配置文件等,每一个Module可以独立编译,实现特定的功能。这种模块化、松耦合的应用管理方式有助于应用的开发、维护与扩展。
支持多设备适配: 一个应用往往需要适配多种设备类型,在采用多Module设计的应用中,每个Module都会标注所支持的设备类型。有些Module支持全部类型的设备,有些Module只支持某一种或几种类型的设备(比如平板),那么在应用市场分发应用包时,也能够根据设备类型做精准的筛选和匹配,从而将不同的包合理的组合和部署到对应的设备上。
7.2.1.3 Module类型
Module按照使用场景可以分为两种类型:
Ability类型的Module: 用于实现应用的功能和特性。每一个Ability类型的Module编译后,会生成一个以.hap为后缀的文件,我们称其为HAP(Harmony Ability Package)包。HAP包可以独立安装和运行,是应用安装的基本单位,一个应用中可以包含一个或多个HAP包,具体包含如下两种类型。
- entry类型的Module: 应用的主模块,包含应用的入口界面、入口图标和主功能特性,编译后生成entry类型的HAP。每一个应用分发到同一类型的设备上的应用程序包,只能包含唯一一个entry类型的HAP。
- feature类型的Module: 应用的动态特性模块,编译后生成feature类型的HAP。一个应用中可以包含一个或多个feature类型的HAP,也可以不包含。
Library类型的Module: 用于实现代码和资源的共享。同一个Library类型的Module可以被其他的Module多次引用,合理地使用该类型的Module,能够降低开发和维护成本。Library类型的Module分为Static和Shared两种类型,编译后会生成共享包。
- Static Library: 静态共享库。编译后会生成一个以.har为后缀的文件,即静态共享包HAR(Harmony Archive)。
- Shared Library: 动态共享库。编译后会生成一个以.hsp为后缀的文件,即动态共享包HSP(Harmony Shared Package)。
7.2.1.4 HAR与HSP的区别
HAR与HSP两种共享包的主要区别体现在:
| 共享包类型 | 编译和运行方式 | 发布和引用方式 |
|---|---|---|
| HAR | HAR中的代码和资源跟随使用方编译,如果有多个使用方,它们的编译产物中会存在多份相同拷贝。 | HAR除了支持应用内引用,还可以独立打包发布,供其他应用引用。 |
| HSP | HSP中的代码和资源可以独立编译,运行时在一个进程中代码也只会存在一份。 | HSP一般随应用进行打包,当前支持应用内和集成态HSP。应用内HSP只支持应用内引用,集成态HSP支持发布到ohpm私仓和跨应用引用。 |
图7-5 HAR和HSP在APP包中的形态示意图
7.2.2 Stage模型应用程序包结构
7.2.2.1 开发态包结构
图7-6 项目工程结构示意图(以实际为准)
- 配置文件
包括应用级配置信息、以及Module级配置信息:
- AppScope > app.json5:app.json5配置文件,用于声明应用的全局配置信息,比如应用Bundle名称、应用名称、应用图标、应用版本号等。
- Module_name > src > main > module.json5:module.json5配置文件,用于声明Module基本信息、支持的设备类型、所含的组件信息、运行所需申请的权限等。
- ArkTS源码文件
Module_name > src > main > ets:用于存放Module的ArkTS源码文件(.ets文件)。
- 资源文件
包括应用级资源文件、以及Module级资源文件,支持图形、多媒体、字符串、布局文件等。
- AppScope > resources :用于存放应用需要用到的资源文件。
- Module_name > src > main > resources :用于存放该Module需要用到的资源文件。
- 其他配置文件
用于编译构建,包括构建配置文件、编译构建任务脚本、混淆规则文件、依赖的共享包信息等。
- build-profile.json5:工程级或Module级的构建配置文件,包括应用签名、产品配置等。
- hvigorfile.ts:应用级或Module级的编译构建任务脚本,开发者可以自定义编译构建工具版本、控制构建行为的配置参数。
- obfuscation-rules.txt:混淆规则文件。混淆开启后,在使用Release模式进行编译时,会对代码进行编译、混淆及压缩处理,保护代码资产。
- oh-package.json5:用于存放依赖库的信息,包括所依赖的三方库和共享包。
7.2.2.2 应用配置文件
每个应用项目的代码目录下必须包含应用配置文件,这些配置文件会向编译工具、操作系统和应用市场提供应用的基本信息。
在基于Stage模型开发的应用项目代码下,都存在一个app.json5配置文件、以及一个或多个module.json5配置文件。
app.json5配置文件主要包含以下内容:
- 应用的全局配置信息,包含应用的Bundle名称、开发厂商、版本号等基本信息。
- 特定设备类型的配置信息。
module.json5配置文件主要包含以下内容:
- Module的基本配置信息,包含Module名称、类型、描述、支持的设备类型等基本信息。
- 应用组件信息,包含UIAbility组件和ExtensionAbility组件的描述信息。
- 应用运行过程中所需的权限信息。
app.json5配置文件示例:
{
"app": {
// 标识应用的Bundle名称,用于标识应用的唯一性。
"bundleName": "com.application.myapplication",
// 标识对应用开发厂商的描述
"vendor": "example",
// 标识应用的版本号
"versionCode": 1000000,
// 标识向用户展示的应用版本号
"versionName": "1.0.0",
// 标识应用图标
"icon": "$media:layered-image",
// 标识应用的名称
"label": "$string:app_name",
// 标识应用的描述信息
"description": "$string:description_application",
// 标识应用运行需要的SDK的API最小版本
"minAPIVersion": 9,
// 标识应用运行需要的API目标版本
"targetAPIVersion": 9,
// 标识应用运行需要的API目标版本的类型
"apiReleaseType": "Release",
// 标识应用是否可调试
"debug": false,
// 标识对car设备做的特殊配置,可以配置的属性字段有上文提到的:minAPIVersion
"car": {
"minAPIVersion": 8
},
// 标识当前包所指定的目标应用
"targetBundleName": "com.application.test",
// 标识当前应用的优先级
"targetPriority": 50,
// 标识当前模块配置的应用环境变量
"appEnvironments": [
{
"name":"name1",
"value": "value1"
}
],
// 标识当前应用自身可创建的子进程的最大个数
"maxChildProcess": 5,
// 标识当前应用配置的多开模式
"multiAppMode": {
"multiAppModeType": "multiInstance",
"maxCount": 5
},
// 标识当前应用是否启用端云文件同步能力
"cloudFileSyncEnabled": false,
// 标识当前应用字体大小跟随系统配置的能力
"configuration": "$profile:configuration"
},
}
module.json5配置文件示例:
{
"module": {
// 标识当前Module的名称
"name": "entry",
// 标识当前Module的类型
"type": "entry",
// 标识当前Module的描述信息
"description": "$string:module_desc",
// 标识当前Module的入口UIAbility名称或者ExtensionAbility名称
"mainElement": "EntryAbility",
// 标识当前Module可以运行在哪类设备上
"deviceTypes": [
"tv",
"tablet"
],
// 标识当前Module对应的HAP是否跟随应用一起安装
"deliveryWithInstall": true,
// 标识当前Module是否支持免安装特性
"installationFree": false,
// 标识当前Module的profile资源,用于列举每个页面信息
"pages": "$profile:main_pages",
// 标识当前Module运行的目标虚拟机类型,供云端分发使用
"virtualMachine": "ark",
// 标识当前Module的自定义元信息
"metadata": [
{
"name": "string",
"value": "string",
"resource": "$profile:distributionFilter_config"
}
],
// 标识当前Module中UIAbility的配置信息
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ts",
"description": "$string:EntryAbility_desc",
"icon": "$media:layered_image",
"label": "$string:EntryAbility_label",
"startWindowIcon": "$media:icon",
"startWindowBackground": "$color:start_window_background",
"exported": true,
"skills": [
{
"entities": [
"entity.system.home"
],
"actions": [
"ohos.want.action.home"
]
}
]
}
],
// 标识当前应用运行时需向系统申请的权限集合
"requestPermissions": [
{
"name": "ohos.abilitydemo.permission.PROVIDER",
"reason": "$string:reason",
"usedScene": {
"abilities": [
"FormAbility"
],
"when": "inuse"
}
}
]
},
// 标识当前包所指定的目标module
"targetModuleName": "feature",
// 标识当前Module的优先级
"targetPriority": 50,
// 标识当前Module的多进程配置项
"isolationMode": "nonisolationFirst"
}
7.2.2.3 编译态包结构
不同类型的Module编译后会生成对应的HAP、HAR、HSP等文件,开发态视图与编译态视图的对照关系如下:
图7-7 开发态与编译态的工程结构视图
从开发态到编译态,Module中的文件会发生如下变更:
ets目录: ArkTS源码编译生成.abc文件。
resources目录: AppScope目录下的资源文件会合入到Module下面资源目录中,如果两个目录下存在重名文件,编译打包后只会保留AppScope目录下的资源文件。
module配置文件: AppScope目录下的app.json5文件字段会合入到Module下面的module.json5文件之中,编译后生成HAP或HSP最终的module.json文件。
7.2.2.4 发布态包结构
每个应用中至少包含一个.hap文件,可能包含若干个.hsp文件、也可能不含,一个应用中的所有.hap与.hsp文件合在一起称为Bundle,其对应的bundleName是应用的唯一标识。
当应用发布上架到应用市场时,需要将Bundle打包为一个.app后缀的文件用于上架,这个.app文件称为App Pack(Application Package),与此同时,DevEco Studio工具自动会生成一个pack.info文件。pack.info文件描述了App Pack中每个HAP和HSP的属性,包含APP中的bundleName和versionCode信息、以及Module中的name、type和abilities等信息。
图7-8 编译发布与上架部署流程图
7.2.2.4 选择合适的包类型
- 各种包类型对比
HAP、HAR、HSP、App Pack四者使用场景和业务规则总结对比如下:
| 名称 | 使用场景 | 业务规则 |
|---|---|---|
| HAP(Harmony Ability Package) | entry:应用的主模块(入口模块),包含应用的入口界面、入口图标和主功能特性,必须且唯一。feature:应用的特性模块,如ExtensionAbility可以放在独立的feature包中,非必须且可以有多个 | ✔️包含UIAbility或者ExtensionAbility✔️可以包含资源✔️可以包含so文件✔️可以在设备上独立安装/运行❌不支持导出接口和ArkUI组件给其他模块使用 |
| HAR(Harmony Archive) | 作为二方库:发布到OHPM私仓,供公司内部其他应用依赖使用。作为三方库:发布到OHPM鸿蒙中心仓,供其他应用依赖使用。 | ✔️可以导出接口和ArkUI组件给其他模块使用✔️可以包含资源✔️可以包含so文件✔️可发布到OpenHarmony中心仓供其他应用使用❌不能在设备上单独安装/运行 |
| HSP(Harmony Shared Package) | 共享资源:多模块共用的代码、资源可以使用HSP,提高代码的可重用性和可维护性。按需加载:按需动态下载所需模块(不常用的功能,封装成一个独立的HSP模块按需加载)。 | ✔️可以导出接口和ArkUI组件给其他模块使用✔️可以包含资源✔️可以包含so文件✔️可以依赖其他HAR、HSP,不能循环依赖❌没有数据目录❌不能在设备上单独运行 |
| App Pack(Application Package) | 应用上架格式,上架时使用,是发布到应用市场的基本单元。 | ✔️可以包含多设备的编译产物(HAP、HSP)应用包结构 |
各类型包编译后的文件输出示意图如下:
图7-9 HAP编译后的产物(.hap)
图7-10 HAR编译后的产物(.har)
图7-11 HSP编译后的产物(接口har + .hsp)
图7-12 APP编译后的产物(.app)
- 如何选择HAR和HSP共享包
在HarmonyOS应用开发中,静态共享包(HAR)和动态共享包(HSP)均可用于代码与资源的共享。在多HAP(HarmonyOS Ability Package)场景下,若存在共用资产,建议使用HSP;否则,选择HAR。在单HAP场景下,若应用需要按需加载功能,则宜采用HSP,否则推荐使用HAR。此外,作为二方库或三方库时,应统一选择HAR。以下对单HAP和多HAP场景进行分析。
如图7-13所示,单HAP场景通常采用分层模块设计,指在同一设备上仅部署一个HAP包。这种情况在手机应用开发中较为常见,若应用仅面向手机端,可优先考虑单HAP包架构。在企业实际开发中,单HAP+多HAR包的形式较为常见。若应用无按需加载需求,建议全部采用HAR包,以提升性能。此外,对于功能相对简单的应用,HAR方案亦可有效控制包体积,使其保持较小规模。
图7-13 单HAP场景
在涉及动态加载的场景时,可以考虑按需加载。然而,按需加载通常涉及存储与HAR包之间的交叉引用,若二者直接组合在一起,可能会对应用包体积产生一定影响。因此,如何优化包体积成为一个重要问题。
如图7-14所示,当动态包与HAP包存在共享的HAR包时,可以采用两种不同的优化策略: “应用体积优先” 与 “性能优先” 。
- 性能优先策略
在性能优先的场景下,不考虑包体积的增加,即使动态加载包与HAP包存在重复依赖(如图中所示的har_C和har_D),依然直接在各自的模块中保留这些依赖。尽管这种方式会导致包体积的增大,但只要整体大小仍在可接受范围内,该模式依然可取,其优势在于能减少动态加载带来的额外性能开销,从而提升运行效率。 - 应用体积优先策略
若应用对包体积较为敏感,可采用中间层进行优化。例如,使用一个公共HAP(common_hap)作为桥接层,该HAP内封装har_C和har_D,并通过HSP动态加载的方式供其他模块使用。这样一来,在保证功能完整的前提下,可有效减少应用整体体积,从而提升存储利用率与安装效率。
图7-14 单HAP场景优化策略
如图 7-15 所示,这是一个多 HAP 场景,主要应用于 2-in-1 设备,例如文档应用中分别打开 Word、Excel、PPT 等,每个功能模块可独立生成一个 Feature(Feather)并对应一个 HAP。每个 HAP 需维护自身的 HAR 包,如图中的 1.hap 和 2.hap,分别依赖 业务1.har 和 业务3.har,这些 HAR 包可拆解并独立为模块。
此外,部分公共资源可提取至 HSP 包,HSP 包本身也可引用 HAR 包。对于公共资产的优化,可通过合理的架构设计,使其高效复用,减少冗余依赖,并提高应用的整体性能和可维护性。
图7-15 多HAP场景
如图 7-16 所示,与单 HAP 场景类似,多 HAP 场景同样可分为 APP 大小(Size)优先和性能优先两种策略。对于某些公共资产,可以考虑放入 HSP 以减少包体积,但这可能会对性能产生一定影响。分包策略与单 HAP 场景基本一致,应根据具体需求进行权衡。
综合来看,在企业应用开发中,需合理平衡包的划分,避免盲目拆分。如果模块功能相对简单,且由小团队开发,可选择仅使用一个 HAR 包,以降低开发和维护成本,使架构更为简洁高效。
图7-16 多HAP场景优化策略
7.3 AbilityStage组件容器
AbilityStage是一个Module级别的组件容器,应用的HAP在首次加载时会创建一个AbilityStage实例,可以对该Module进行初始化等操作。
AbilityStage与Module一一对应,即一个Module拥有一个AbilityStage。
DevEco Studio默认工程中未自动生成AbilityStage,如需要使用AbilityStage的能力,可以手动新建一个AbilityStage文件,具体步骤如下。
- 在工程Module对应的ets目录下,右键选择“New > Directory”,新建一个目录并命名为myabilitystage。
- 在myabilitystage目录,右键选择“New > ArkTS File”,新建一个文件并命名为MyAbilityStage.ets。
- 打开MyAbilityStage.ets文件,导入AbilityStage的依赖包,自定义类继承AbilityStage并加上需要的生命周期回调,示例中增加了一个onCreate()生命周期回调。
import { AbilityStage, Want } from '@kit.AbilityKit'
export default class MyAbilityStage extends AbilityStage {
onCreate(): void {
// 应用HAP首次加载时触发,
// 可以在此执行该Module的初始化操作(例如资源预加载、线程创建等)。
}
onAcceptWant(want: Want): string {
// 仅specified模式下触发
return 'MyAbilityStage'
}
}
- 在module.json5配置文件中,通过配置srcEntry参数来指定模块对应的代码路径,以作为HAP加载的入口。
{
"module": {
"name": "entry",
"type": "entry",
"srcEntry": "./ets/myabilitystage/MyAbilityStage.ets",
// ...
}
}
AbilityStage拥有onCreate()生命周期回调和onAcceptWant()、onConfigurationUpdated()、onMemoryLevel()事件回调。
- onCreate()生命周期回调:在开始加载对应Module的第一个UIAbility实例之前会先创建AbilityStage,并在AbilityStage创建完成之后执行其onCreate()生命周期回调。AbilityStage模块提供在Module加载的时候,通知开发者,可以在此进行该Module的初始化(如资源预加载,线程创建等)能力。
- onAcceptWant()事件回调:UIAbility指定实例模式(specified)启动时候触发的事件回调。
- onConfigurationUpdated()事件回调:当系统全局配置发生变更时触发的事件,系统语言、深浅色等。
- onMemoryLevel()事件回调:当系统调整内存时触发的事件。
应用被切换到后台时,系统会将在后台的应用保留在缓存中。即使应用处于缓存中,也会影响系统整体性能。当系统资源不足时,系统会通过多种方式从应用中回收内存,必要时会完全停止应用,从而释放内存用于执行关键任务。为了进一步保持系统内存的平衡,避免系统停止用户的应用进程,可以在AbilityStage中的onMemoryLevel()生命周期回调中订阅系统内存的变化情况,释放不必要的资源。
import { AbilityStage, AbilityConstant } from '@kit.AbilityKit'
export default class MyAbilityStage extends AbilityStage {
onMemoryLevel(level: AbilityConstant.MemoryLevel): void {
// 根据系统可用内存的变化情况,释放不必要的内存
}
}
7.4 UIAbility组件
UIAbility组件是一种包含UI的应用组件,主要用于和用户交互。
UIAbility的设计理念:
- 原生支持应用组件级的跨端迁移和多端协同。
- 支持多设备和多窗口形态。
UIAbility划分原则与建议:
UIAbility组件是系统调度的基本单元,为应用提供绘制界面的窗口。一个应用可以包含一个或多个UIAbility组件。例如,在支付应用中,可以将入口功能和收付款功能分别配置为独立的UIAbility。
每一个UIAbility组件实例都会在最近任务列表中显示一个对应的任务。
对于开发者而言,可以根据具体场景选择单个还是多个UIAbility,划分建议如下:
- 如果开发者希望在任务视图中看到一个任务,建议使用“一个UIAbility+多个页面”的方式,可以避免不必要的资源加载。
- 如果开发者希望在任务视图中看到多个任务,或者需要同时开启多个窗口,建议使用多个UIAbility实现不同的功能。
例如,即时通讯类应用中的消息列表与音视频通话采用不同的UIAbility进行开发,既可以方便地切换任务窗口,又可以实现应用的两个任务窗口在一个屏幕上分屏显示。
为使应用能够正常使用UIAbility,需要在module.json5配置文件的abilities标签中声明UIAbility的名称、入口、标签等相关信息。
{
"module": {
// ...
"abilities": [
{
// UIAbility组件的名称
"name": "EntryAbility",
// UIAbility组件的代码路径
"srcEntry": "./ets/entryability/EntryAbility.ets",
// UIAbility组件的描述信息
"description": "$string:EntryAbility_desc",
// UIAbility组件的图标
"icon": "$media:icon",
// UIAbility组件的标签
"label": "$string:EntryAbility_label",
// UIAbility组件启动页面图标资源文件的索引
"startWindowIcon": "$media:icon",
// UIAbility组件启动页面背景颜色资源文件的索引
"startWindowBackground": "$color:start_window_background",
// ...
}
]
}
}
7.4.1 UIAbility组件生命周期
7.4.1.1 概述
当用户打开、切换和返回到对应应用时,应用中的UIAbility实例会在其生命周期的不同状态之间转换。UIAbility类提供了一系列回调,通过这些回调可以知道当前UIAbility实例的某个状态发生改变,会经过UIAbility实例的创建和销毁,或者UIAbility实例发生了前后台的状态切换。
UIAbility的生命周期包括Create、Foreground、Background、Destroy四个状态,如下图所示。
图7-17 UIAbility生命周期状态
7.4.1.2 生命周期状态说明
- Create状态
Create状态为在应用加载过程中,UIAbility实例创建完成时触发,系统会调用onCreate()回调。可以在该回调中进行页面初始化操作,例如变量定义资源加载等,用于后续的UI展示。
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit'
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
// 页面初始化
}
// ...
}
- WindowStageCreate和WindowStageDestroy状态
UIAbility实例创建完成之后,在进入Foreground之前,系统会创建一个WindowStage。WindowStage创建完成后会进入onWindowStageCreate()回调,可以在该回调中设置UI加载、设置WindowStage的事件订阅。
图7-18 WindowStageCreate和WindowStageDestroy状态
在onWindowStageCreate()回调中通过loadContent()方法设置应用要加载的页面,并根据需要调用on('windowStageEvent')方法订阅WindowStage的事件(获焦/失焦、切到前台/切到后台、前台可交互/前台不可交互)。
import { UIAbility } from '@kit.AbilityKit'
import { window } from '@kit.ArkUI'
import { hilog } from '@kit.PerformanceAnalysisKit'
const TAG: string = '[EntryAbility]'
const DOMAIN_NUMBER: number = 0xFF00
export default class EntryAbility extends UIAbility {
// ...
onWindowStageCreate(windowStage: window.WindowStage): void {
// 设置WindowStage的事件订阅
//(获焦/失焦、切到前台/切到后台、前台可交互/前台不可交互)
try {
windowStage.on('windowStageEvent', (data) => {
let stageEventType: window.WindowStageEventType = data
switch (stageEventType) {
case window.WindowStageEventType.SHOWN: // 切到前台
hilog.info(DOMAIN_NUMBER, TAG, `windowStage foreground.`)
break;
case window.WindowStageEventType.ACTIVE: // 获焦状态
hilog.info(DOMAIN_NUMBER, TAG, `windowStage active.`)
break;
case window.WindowStageEventType.INACTIVE: // 失焦状态
hilog.info(DOMAIN_NUMBER, TAG, `windowStage inactive.`)
break;
case window.WindowStageEventType.HIDDEN: // 切到后台
hilog.info(DOMAIN_NUMBER, TAG, `windowStage background.`)
break;
case window.WindowStageEventType.RESUMED: // 前台可交互状态
hilog.info(DOMAIN_NUMBER, TAG, `windowStage resumed.`)
break;
case window.WindowStageEventType.PAUSED: // 前台不可交互状态
hilog.info(DOMAIN_NUMBER, TAG, `windowStage paused.`)
break;
default:
break;
}
})
} catch (exception) {
hilog.error(DOMAIN_NUMBER, TAG,
` ${JSON.stringify(exception)}`)
}
hilog.info(
DOMAIN_NUMBER,
TAG, `%{public}s`,
`Ability onWindowStageCreate`
)
// 设置UI加载
windowStage.loadContent('pages/Index', (err, data) => {
// ...
})
}
}
对应于onWindowStageCreate()回调。在UIAbility实例销毁之前,则会先进入onWindowStageDestroy()回调,可以在该回调中释放UI资源。
import { UIAbility } from '@kit.AbilityKit'
import { window } from '@kit.ArkUI'
export default class EntryAbility extends UIAbility {
windowStage: window.WindowStage | undefined = undefined
// ...
onWindowStageCreate(windowStage: window.WindowStage): void {
this.windowStage = windowStage
// ...
}
onWindowStageDestroy() {
// 释放UI资源
}
}
- WindowStageWillDestroy状态
对应onWindowStageWillDestroy()回调,在WindowStage销毁前执行,此时WindowStage可以使用。
import { UIAbility } from '@kit.AbilityKit'
import { window } from '@kit.ArkUI'
import { BusinessError } from '@kit.BasicServicesKit'
import { hilog } from '@kit.PerformanceAnalysisKit'
const TAG: string = '[EntryAbility]'
const DOMAIN_NUMBER: number = 0xFF00
export default class EntryAbility extends UIAbility {
windowStage: window.WindowStage | undefined = undefined
// ...
onWindowStageCreate(windowStage: window.WindowStage): void {
this.windowStage = windowStage
// ...
}
onWindowStageWillDestroy(windowStage: window.WindowStage) {
// 释放通过windowStage对象获取的资源
// 在onWindowStageWillDestroy()中注销WindowStage事件订阅
//(获焦/失焦、切到前台/切到后台、前台可交互/前台不可交互)
try {
if (this.windowStage) {
this.windowStage.off('windowStageEvent')
}
} catch (err) {
let code = (err as BusinessError).code
let message = (err as BusinessError).message
hilog.error(
DOMAIN_NUMBER,
TAG,
`Code is ${code}, message is ${message}`
)
}
}
onWindowStageDestroy() {
// 释放UI资源
}
}
- Foreground和Background状态
Foreground和Background状态分别在UIAbility实例切换至前台和切换至后台时触发,对应于onForeground()回调和onBackground()回调。
onForeground()回调,在UIAbility的UI可见之前,如UIAbility切换至前台时触发。可以在onForeground()回调中申请系统需要的资源,或者重新申请在onBackground()中释放的资源。
onBackground()回调,在UIAbility的UI完全不可见之后,如UIAbility切换至后台时候触发。可以在onBackground()回调中释放UI不可见时无用的资源,或者在此回调中执行较为耗时的操作,例如状态保存等。
例如应用在使用过程中需要使用用户定位时,假设应用已获得用户的定位权限授权。在UI显示之前,可以在onForeground()回调中开启定位功能,从而获取到当前的位置信息。
当应用切换到后台状态,可以在onBackground()回调中停止定位功能,以节省系统的资源消耗。
import { UIAbility } from '@kit.AbilityKit'
export default class EntryAbility extends UIAbility {
// ...
onForeground(): void {
// 申请系统需要的资源,或者重新申请在onBackground()中释放的资源
}
onBackground(): void {
// 释放UI不可见时无用的资源,或者在此回调中执行较为耗时的操作
// 例如状态保存等
}
}
当应用的UIAbility实例已创建,且UIAbility配置为singleton启动模式时,再次调用startAbility()方法启动该UIAbility实例时,只会进入该UIAbility的onNewWant()回调,不会进入其onCreate()和onWindowStageCreate()生命周期回调。应用可以在该回调中更新要加载的资源和数据等,用于后续的UI展示。
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit'
export default class EntryAbility extends UIAbility {
// ...
onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam) {
// 更新资源、数据
}
}
- Destroy状态
Destroy状态在UIAbility实例销毁时触发。可以在onDestroy()回调中进行系统资源的释放、数据的保存等操作。
例如,调用terminateSelf()方法停止当前UIAbility实例,执行onDestroy()回调,并完成UIAbility实例的销毁。
import { UIAbility } from '@kit.AbilityKit'
export default class EntryAbility extends UIAbility {
// ...
onDestroy() {
// 系统资源的释放、数据的保存等
}
}
7.4.2 UIAbility组件启动模式
UIAbility的启动模式是指UIAbility实例在启动时的不同呈现状态。针对不同的业务场景,系统提供了三种启动模式:
- singleton(单实例模式)
- multiton(多实例模式)
- specified(指定实例模式)
7.4.2.1 singleton启动模式
singleton启动模式为单实例模式,也是默认情况下的启动模式。
每次调用startAbility()方法时,如果应用进程中该类型的UIAbility实例已经存在,则复用系统中的UIAbility实例。系统中只存在唯一一个该UIAbility实例,即在最近任务列表中只存在一个该类型的UIAbility实例。
如果需要使用singleton启动模式,在module.json5配置文件中的launchType字段配置为singleton即可。
{
"module": {
// ...
"abilities": [
{
"launchType": "singleton",
// ...
}
]
}
}
效果演示见案例视频。
7.4.2.2 multiton启动模式
multiton启动模式为多实例模式,每次调用startAbility()方法时,都会在应用进程中创建一个新的该类型UIAbility实例。即在最近任务列表中可以看到有多个该类型的UIAbility实例。这种情况下可以将UIAbility配置为multiton(多实例模式)。
multiton启动模式的开发使用,在module.json5配置文件中的launchType字段配置为multiton即可。
{
"module": {
// ...
"abilities": [
{
"launchType": "multiton",
// ...
}
]
}
}
效果演示见案例视频。
7.4.2.3 specified启动模式
specified启动模式为指定实例模式,针对一些特殊场景使用(例如文档应用中每次新建文档希望都能新建一个文档实例,重复打开一个已保存的文档希望打开的都是同一个文档实例)。
图7-19 指定实例启动模式原理
假设应用有两个UIAbility实例,即EntryAbility和SpecifiedAbility。EntryAbility以specified模式启动SpecifiedAbility。基本原理如下:
- EntryAbility调用startAbility()方法,并在Want的parameters字段中设置唯一的Key值,用于标识SpecifiedAbility。
- 系统在拉起SpecifiedAbility之前,会先进入对应的AbilityStage的onAcceptWant()生命周期回调,获取用于标识目标UIAbility的Key值。
- 系统会根据获取的Key值来匹配UIAbility。
-
- 如果匹配到对应的UIAbility,则会启动该UIAbility实例,并进入onNewWant()生命周期回调。
- 如果无法匹配对应的UIAbility,则会创建一个新的UIAbility实例,并进入该UIAbility实例的onCreate()生命周期回调和onWindowStageCreate()生命周期回调。
具体实现步骤如下:
- 在SpecifiedAbility中,需要将module.json5配置文件的launchType字段配置为specified。
{
"module": {
// ...
"abilities": [
{
"launchType": "specified",
// ...
}
]
}
}
- 在EntryAbility中,调用startAbility()方法时,可以在want参数中传入了自定义参数instanceKey作为唯一标识符,以此来区分不同的UIAbility实例。示例中instanceKey的value值设置为字符串'KEY'。
// 在启动指定实例模式的UIAbility时,给每一个UIAbility实例配置一个独立的Key标识
// 例如在文档使用场景中,可以用文档路径作为Key标识
import { common, Want } from '@kit.AbilityKit'
import { hilog } from '@kit.PerformanceAnalysisKit'
import { BusinessError } from '@kit.BasicServicesKit'
const TAG: string = '[Page_StartModel]'
const DOMAIN_NUMBER: number = 0xFF00
function getInstance(): string {
return 'KEY'
}
@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: 'SpecifiedFirstAbility',
moduleName: 'entry', // moduleName非必选
parameters: {
// 自定义信息
instanceKey: this.KEY_NEW
}
}
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'
})
// ...
Button()
.onClick(() => {
let context: common.UIAbilityContext
= getContext(this) as common.UIAbilityContext
// context为调用方UIAbility的UIAbilityContext
let want: Want = {
deviceId: '', // deviceId为空表示本设备
bundleName: 'com.samples.stagemodelabilitydevelop',
abilityName: 'SpecifiedSecondAbility',
moduleName: 'entry', // moduleName非必选
parameters: {
// 自定义信息
instanceKey: getInstance()
}
}
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'
})
// ...
}
.width('100%')
}
.height('100%')
}
}
- 开发者根据业务在SpecifiedAbility的onAcceptWant()生命周期回调设置该UIAbility的标识。示例中标识设置为SpecifiedAbilityInstance_KEY。
import { AbilityStage, Want } from '@kit.AbilityKit'
export default class MyAbilityStage extends AbilityStage {
onAcceptWant(want: Want): string {
// 在被调用方的AbilityStage中,针对启动模式为specified的UIAbility
// 返回一个UIAbility实例对应的一个Key值
// 当前示例指的是module1 Module的SpecifiedAbility
if (
want.abilityName === 'SpecifiedFirstAbility'
|| want.abilityName === 'SpecifiedSecondAbility'
) {
// 返回的字符串KEY标识为自定义拼接的字符串内容
if (want.parameters) {
return `SpecifiedAbilityInstance_${want.parameters.instanceKey}`
}
}
// ...
return 'MyAbilityStage'
}
}
例如在文档应用中,可以为不同的文档实例内容绑定不同的Key值。每次新建文档时,可以传入一个新的Key值(例如可以将文件的路径作为一个Key标识),此时AbilityStage中启动UIAbility时都会创建一个新的UIAbility实例;当新建的文档保存之后,回到桌面,或者新打开一个已保存的文档,回到桌面,此时再次打开该已保存的文档,此时AbilityStage中再次启动该UIAbility时,打开的仍然是之前原来已保存的文档界面。
效果演示见案例视频。
7.4.3 UIAbility组件基本用法
UIAbility组件的基本用法包括:指定UIAbility的启动页面以及获取UIAbility的上下文UIAbilityContext。
7.4.3.1 指定UIAbility的启动页面
应用中的UIAbility在启动过程中,需要指定启动页面,否则应用启动后会因为没有默认加载页面而导致白屏。可以在UIAbility的onWindowStageCreate()生命周期回调中,通过WindowStage对象的loadContent()方法设置启动页面。
import { UIAbility } from '@kit.AbilityKit'
import { window } from '@kit.ArkUI'
export default class EntryAbility extends UIAbility {
onWindowStageCreate(windowStage: window.WindowStage): void {
// 主窗口已创建,为此 ability 设置主页面
windowStage.loadContent('pages/Index', (err, data) => {
// ...
})
}
// ...
}
7.4.3.2 获取UIAbility的上下文信息
UIAbility类拥有自身的上下文信息,该信息为UIAbilityContext类的实例,UIAbilityContext类拥有abilityInfo、currentHapModuleInfo等属性。通过UIAbilityContext可以获取UIAbility的相关配置信息,如包代码路径、Bundle名称、Ability名称和应用程序需要的环境状态等属性信息,以及可以获取操作UIAbility实例的方法(如startAbility()、connectServiceExtensionAbility()、terminateSelf()等)。
如果需要在页面中获得当前Ability的Context,可调用getContext接口获取当前页面关联的UIAbilityContext或ExtensionContext。
- 在UIAbility中可以通过this.context获取UIAbility实例的上下文信息。
import { UIAbility, AbilityConstant, Want } from '@kit.AbilityKit'
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
// 获取UIAbility实例的上下文
let context = this.context
// ...
}
}
- 在页面中获取UIAbility实例的上下文信息,包括导入依赖资源context模块和在组件中定义一个context变量两个部分。
import { common, Want } from '@kit.AbilityKit'
@Entry
@Component
struct Page_EventHub {
private context = getContext(this) as common.UIAbilityContext
startAbilityTest(): void {
let want: Want = {
// Want参数信息
}
this.context.startAbility(want)
}
// 页面展示
build() {
// ...
}
}
也可以在导入依赖资源context模块后,在具体使用UIAbilityContext前进行变量定义。
import { common, Want } from '@kit.AbilityKit'
@Entry
@Component
struct Page_UIAbilityComponentsBasicUsage {
startAbilityTest(): void {
let context = getContext(this) as common.UIAbilityContext
let want: Want = {
// Want参数信息
}
context.startAbility(want)
}
// 页面展示
build() {
// ...
}
}
- 当业务完成后,开发者如果想要终止当前UIAbility实例,可以通过调用terminateSelf()方法实现。
import { common } from '@kit.AbilityKit'
import { BusinessError } from '@kit.BasicServicesKit'
@Entry
@Component
struct Page_UIAbilityComponentsBasicUsage {
// 页面展示
build() {
Column() {
//...
Button('FuncAbilityB')
.onClick(() => {
let context = getContext(this) as common.UIAbilityContext
try {
context.terminateSelf((err: BusinessError) => {
if (err.code) {
// 处理业务逻辑错误
console.error(
`code is ${err.code},
message is ${err.message}`
)
return;
}
// 执行正常业务
console.info('terminateSelf succeed')
});
} catch (err) {
// 捕获同步的参数错误
let code = (err as BusinessError).code
let message = (err as BusinessError).message
console.error(`code is ${code}, message is ${message}`)
}
})
}
}
}
7.4.4 UIAbility组件与UI的数据同步
基于当前的应用模型,可以通过以下几种方式来实现UIAbility组件与UI之间的数据同步。
- 使用EventHub进行数据通信:在基类Context中提供了EventHub对象,可以通过发布订阅方式来实现事件的传递。在事件传递前,订阅者需要先进行订阅,当发布者发布事件时,订阅者将接收到事件并进行相应处理。
- 使用V1版状态管理AppStorage/LocalStorage进行数据同步:ArkUI提供了AppStorage和LocalStorage两种应用级别的状态管理方案,可用于实现应用级别和UIAbility级别的数据同步。
- 使用V2版状态管理AppStorageV2进行数据同步:ArkUI提供了AppStorage应用级别的状态管理方案,可用于实现应用级别和UIAbility级别的数据同步。
7.4.4.1 使用EventHub进行数据通信
EventHub为UIAbility组件提供了事件机制,使它们能够进行订阅、取消订阅和触发事件等数据通信能力。
在基类Context中,提供了EventHub对象,可用于在UIAbility组件实例内通信。使用EventHub实现UIAbility与UI之间的数据通信需要先获取EventHub对象,本节将以此为例进行说明。
- 在UIAbility中调用eventHub.on()方法注册一个自定义事件“event1”,eventHub.on()有如下两种调用方式,使用其中一种即可。
import { 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)
eventhub.on('event1', (data: string) => {
// 触发事件,完成相应的业务操作
})
hilog.info(DOMAIN_NUMBER, TAG, '%{public}s', 'Ability onCreate')
}
// ...
eventFunc(argOne: Context, argTwo: Context): void {
hilog.info(DOMAIN_NUMBER, TAG, '1. ' + `${argOne}, ${argTwo}`)
return
}
}
- 在UI中通过eventHub.emit()方法触发该事件,在触发事件的同时,根据需要传入参数信息。
import { common } from '@kit.AbilityKit'
import { promptAction } from '@kit.ArkUI'
@Entry
@Component
struct Page_EventHub {
private context = getContext(this) as common.UIAbilityContext
eventHubFunc(): void {
// 不带参数触发自定义“event1”事件
this.context.eventHub.emit('event1')
// 带1个参数触发自定义“event1”事件
this.context.eventHub.emit('event1', 1)
// 带2个参数触发自定义“event1”事件
this.context.eventHub.emit('event1', 2, 'test')
// 开发者可以根据实际的业务场景设计事件传递的参数
}
build() {
Column() {
// ...
List({ initialIndex: 0 }) {
ListItem() {
Row() {
// ...
}
.onClick(() => {
this.eventHubFunc()
promptAction.showToast({
message: 'EventHubFuncA'
});
})
}
// ...
ListItem() {
Row() {
// ...
}
.onClick(() => {
this.context.eventHub.off('event1');
promptAction.showToast({
message: 'EventHubFuncB'
});
})
}
// ...
}
// ...
}
// ...
}
}
- 在UIAbility的注册事件回调中可以得到对应的触发事件结果,运行日志结果如下所示。
[Example].[Entry].[EntryAbility] 1. []
[Example].[Entry].[EntryAbility] 1. [1]
[Example].[Entry].[EntryAbility] 1. [2,"test"]
4. 在自定义事件“event1”使用完成后,可以根据需要调用eventHub.off()方法取消该事件的订阅。
import { UIAbility } from '@kit.AbilityKit'
export default class EntryAbility extends UIAbility {
// ...
onDestroy(): void {
this.context.eventHub.off('event1')
}
}
7.4.4.2 使用V1版状态管理AppStorage/LocalStorage进行数据同步
在V1版本的状态管理中,ArkUI提供了AppStorage和LocalStorage两种应用级别的状态管理方案,可用于实现应用级别和UIAbility级别的数据同步。使用这些方案可以方便地管理应用状态,提高应用性能和用户体验。其中,AppStorage是一个全局的状态管理器,适用于多个UIAbility共享同一状态数据的情况;而LocalStorage则是一个局部的状态管理器,适用于单个UIAbility内部使用的状态数据。通过这两种方案,开发者可以更加灵活地控制应用状态,提高应用的可维护性和可扩展性。
7.4.4.3 使用V2版状态管理AppStorageV2进行数据同步
在V2版本的状态管理中,ArkUI提供了AppStorageV2应用级别的状态管理方案,可用于实现应用级别和UIAbility级别的数据同步。使用这个方案可以方便地管理应用状态,提高应用性能和用户体验。AppStorageV2是提供状态变量在应用级全局共享的能力,开发者可以通过connect绑定同一个key,进行跨ability的数据共享。
7.4.5 UIAbility组件间交互
UIAbility是系统调度的最小单元。在设备内的功能模块之间跳转时,会涉及到启动特定的UIAbility,包括应用内的其他UIAbility、或者其他应用的UIAbility(例如启动三方支付UIAbility)。
本小节主要介绍启动应用内的UIAbility组件的方式。
7.4.5.1 启动应用内的UIAbility
当一个应用内包含多个UIAbility时,存在应用内启动UIAbility的场景。例如在支付应用中从入口UIAbility启动收付款UIAbility。
假设应用中有两个UIAbility:EntryAbility和FuncAbility(可以在同一个Module中,也可以在不同的Module中),需要从EntryAbility的页面中启动FuncAbility。
- 在EntryAbility中,通过调用startAbility()方法启动UIAbility,want为UIAbility实例启动的入口参数,其中bundleName为待启动应用的Bundle名称,abilityName为待启动的Ability名称,moduleName在待启动的UIAbility属于不同的Module时添加,parameters为自定义信息参数。
import { common, Want } from '@kit.AbilityKit'
import { hilog } from '@kit.PerformanceAnalysisKit'
import { BusinessError } from '@kit.BasicServicesKit'
const TAG: string = '[Page_UIAbilityComponentsInteractive]'
const DOMAIN_NUMBER: number = 0xFF00
@Entry
@Component
struct Page_UIAbilityComponentsInteractive {
private context = getContext(this) as common.UIAbilityContext
build() {
Column() {
//...
List({ initialIndex: 0 }) {
ListItem() {
Row() {
//...
}
.onClick(() => {
// context为Ability对象的成员,在非Ability对象内部调用需要
// 将Context对象传递过去
let wantInfo: Want = {
deviceId: '', // deviceId为空表示本设备
bundleName: 'com.samples.stagemodelabilitydevelop',
moduleName: 'entry', // moduleName非必选
abilityName: 'FuncAbilityA',
parameters: {
// 自定义信息
info: `来自EntryAbility ${TAG}页面`
},
}
// 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.')
})
})
}
//...
}
//...
}
//...
}
}
- 在FuncAbility的onCreate()或者onNewWant()生命周期回调文件中接收EntryAbility传递过来的参数。
import { 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
}
//...
}
- 在FuncAbility业务完成之后,如需要停止当前UIAbility实例,在FuncAbility中通过调用terminateSelf()方法实现。
import { common } from '@kit.AbilityKit'
import { hilog } from '@kit.PerformanceAnalysisKit'
const TAG: string = '[Page_FromStageModel]'
const DOMAIN_NUMBER: number = 0xFF00
@Entry
@Component
struct Page_FromStageModel {
build() {
Column() {
//...
Button('FuncAbilityB')
.onClick(() => {
// UIAbilityContext
let context: common.UIAbilityContext
= getContext(this) as common.UIAbilityContext
// context为需要停止的UIAbility实例的AbilityContext
context.terminateSelf((err) => {
if (err.code) {
hilog.error(
DOMAIN_NUMBER,
TAG,
`Code is ${err.code}, message is ${err.message}`
)
return
}
})
})
}
//...
}
}
- 如需要关闭应用所有的UIAbility实例,可以调用ApplicationContext的killAllProcesses()方法实现关闭应用所有的进程。
7.4.5.2 启动应用内的UIAbility并获取返回结果
在一个EntryAbility启动另外一个FuncAbility时,希望在被启动的FuncAbility完成相关业务后,能将结果返回给调用方。例如在应用中将入口功能和账号登录功能分别设计为两个独立的UIAbility,在账号登录UIAbility中完成登录操作后,需要将登录的结果返回给入口UIAbility。
- 在EntryAbility中,调用startAbilityForResult()接口启动FuncAbility,异步回调中的data用于接收FuncAbility停止自身后返回给EntryAbility的信息。
import { common, Want } from '@kit.AbilityKit'
import { hilog } from '@kit.PerformanceAnalysisKit'
import { promptAction } from '@kit.ArkUI'
import { BusinessError } from '@kit.BasicServicesKit'
const TAG: string = '[Page_UIAbilityComponentsInteractive]'
const DOMAIN_NUMBER: number = 0xFF00
@Entry
@Component
struct Page_UIAbilityComponentsInteractive {
build() {
Column() {
//...
List({ initialIndex: 0 }) {
ListItem() {
Row() {
//...
}
.onClick(() => {
// UIAbilityContext
let context: common.UIAbilityContext
= getContext(this) as common.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,
`Code is ${err.code}, message is ${err.message}`
)
})
})
}
//...
}
//...
}
//...
}
}
- 在FuncAbility停止自身时,需要调用terminateSelfWithResult()方法,入参abilityResult为FuncAbility需要返回给EntryAbility的信息。
import { common } from '@kit.AbilityKit'
import { hilog } from '@kit.PerformanceAnalysisKit'
const TAG: string = '[Page_FuncAbilityA]'
const DOMAIN_NUMBER: number = 0xFF00
@Entry
@Component
struct Page_FuncAbilityA {
build() {
Column() {
//...
List({ initialIndex: 0 }) {
ListItem() {
Row() {
//...
}
.onClick(() => {
// UIAbilityContext
let context: common.UIAbilityContext
= getContext(this) as common.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,
`Code is ${err.code}, message is ${err.message}`
)
return
}
})
})
}
//...
}
//...
}
//...
}
}
- FuncAbility停止自身后,EntryAbility通过startAbilityForResult()方法回调接收被FuncAbility返回的信息,RESULT_CODE需要与前面的数值保持一致。
import { common, Want } from '@kit.AbilityKit'
import { hilog } from '@kit.PerformanceAnalysisKit'
import { promptAction } from '@kit.ArkUI'
import { BusinessError } from '@kit.BasicServicesKit'
const TAG: string = '[Page_UIAbilityComponentsInteractive]'
const DOMAIN_NUMBER: number = 0xFF00
@Entry
@Component
struct Page_UIAbilityComponentsInteractive {
build() {
Column() {
//...
List({ initialIndex: 0 }) {
ListItem() {
Row() {
//...
}
.onClick(() => {
let 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,
`Code is ${err.code}, message is ${err.message}`
)
})
})
}
//...
}
//...
}
//...
}
}
7.4.5.3 启动UIAbility的指定页面
- 概述
一个UIAbility可以对应多个页面,在不同的场景下启动该UIAbility时需要展示不同的页面,例如从一个UIAbility的页面中跳转到另外一个UIAbility时,希望启动目标UIAbility的指定页面。
UIAbility的启动分为两种情况:UIAbility冷启动和UIAbility热启动。
- UIAbility冷启动:指的是UIAbility实例处于完全关闭状态下被启动,这需要完整地加载和初始化UIAbility实例的代码、资源等。
- UIAbility热启动:指的是UIAbility实例已经启动并在前台运行过,由于某些原因切换到后台,再次启动该UIAbility实例,这种情况下可以快速恢复UIAbility实例的状态。
本小节主要讲解目标UIAbility冷启动和目标UIAbility热启动两种启动指定页面的场景,以及在讲解启动指定页面之前会讲解到在调用方如何指定启动页面。
- 调用方UIAbility指定启动页面
调用方UIAbility启动另外一个UIAbility时,通常需要跳转到指定的页面。例如FuncAbility包含两个页面(Index对应首页,Second对应功能A页面),此时需要在传入的want参数中配置指定的页面路径信息,可以通过want中的parameters参数增加一个自定义参数传递页面跳转信息。
import { common, Want } from '@kit.AbilityKit'
import { hilog } from '@kit.PerformanceAnalysisKit'
import { BusinessError } from '@kit.BasicServicesKit'
const TAG: string = '[Page_UIAbilityComponentsInteractive]'
const DOMAIN_NUMBER: number = 0xFF00
@Entry
@Component
struct Page_UIAbilityComponentsInteractive {
build() {
Column() {
//...
List({ initialIndex: 0 }) {
ListItem() {
Row() {
//...
}
.onClick(() => {
// UIAbilityContext
let context: common.UIAbilityContext
= getContext(this) as common.UIAbilityContext
let want: Want = {
deviceId: '', // deviceId为空表示本设备
bundleName: 'com.samples.stagemodelabilityinteraction',
moduleName: 'entry', // moduleName非必选
abilityName: 'FuncAbility',
parameters: { // 自定义参数传递页面信息
router: '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,
`Code is ${err.code}, message is ${err.message}`
)
})
})
}
//...
}
//...
}
//...
}
}
- 目标UIAbility冷启动
目标UIAbility冷启动时,在目标UIAbility的onCreate()生命周期回调中,接收调用方传过来的参数。然后在目标UIAbility的onWindowStageCreate()生命周期回调中,解析调用方传递过来的want参数,获取到需要加载的页面信息url,传入windowStage.loadContent()方法。
import { AbilityConstant, Want, UIAbility } from '@kit.AbilityKit'
import { hilog } from '@kit.PerformanceAnalysisKit'
import { window, UIContext } from '@kit.ArkUI'
const DOMAIN_NUMBER: number = 0xFF00
const TAG: string = '[EntryAbility]'
export default class EntryAbility extends UIAbility {
funcAbilityWant: Want | undefined = undefined
uiContext: UIContext | undefined = undefined
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
// 接收调用方UIAbility传过来的参数
this.funcAbilityWant = want
}
onWindowStageCreate(windowStage: window.WindowStage): void {
hilog.info(
DOMAIN_NUMBER,
TAG,
'%{public}s',
'Ability onWindowStageCreate'
)
// 主窗口已创建,为此ability设置主页面
let url = 'pages/Index'
if (
this.funcAbilityWant?.parameters?.router
&& this.funcAbilityWant.parameters.router === 'funcA'
) {
url = 'pages/Page_ColdStartUp'
}
windowStage.loadContent(url, (err, data) => {
// ...
})
}
}
- 目标UIAbility热启动
在应用开发中,会遇到目标UIAbility实例之前已经启动过的场景,这时再次启动目标UIAbility时,不会重新走初始化逻辑,只会直接触发onNewWant()生命周期方法。为了实现跳转到指定页面,需要在onNewWant()中解析参数进行处理。
例如短信应用和联系人应用配合使用的场景。
- 用户先打开短信应用,短信应用的UIAbility实例启动,显示短信应用的主页。
- 用户将设备回到桌面界面,短信应用进入后台运行状态。
- 用户打开联系人应用,找到联系人张三。
- 用户点击联系人张三的短信按钮,会重新启动短信应用的UIAbility实例。
- 由于短信应用的UIAbility实例已经启动过了,此时会触发该UIAbility的onNewWant()回调,而不会再走onCreate()和onWindowStageCreate()等初始化逻辑。
图7-20 目标UIAbility热启动
开发步骤如下所示。
- 冷启动短信应用的UIAbility实例时,在onWindowStageCreate()生命周期回调中,通过调用getUIContext()接口获取UI上下文实例UIContext对象。
import { hilog } from '@kit.PerformanceAnalysisKit'
import { Want, UIAbility } from '@kit.AbilityKit'
import { window, UIContext } from '@kit.ArkUI'
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 {
// 主窗口已创建,为此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,
`Code is ${err.code}, message is ${err.message}`
)
return
}
windowClass = data
this.uiContext = windowClass.getUIContext()
})
hilog.info(
DOMAIN_NUMBER,
TAG,
'Data: %{public}s', JSON.stringify(data) ?? ''
)
})
}
}
- 在短信应用UIAbility的onNewWant()回调中解析调用方传递过来的want参数,通过调用UIContext中的getRouter()方法获取Router对象,并进行指定页面的跳转。此时再次启动该短信应用的UIAbility实例时,即可跳转到该短信应用的UIAbility实例的指定页面。
import { AbilityConstant, Want, UIAbility } from '@kit.AbilityKit'
import { hilog } from '@kit.PerformanceAnalysisKit'
import type { Router, UIContext } 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
// ...
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,
`Code is ${err.code}, message is ${err.message}`
)
})
}
}
}
}
7.5 应用间跳转
应用跳转是指从一个应用跳转至另外一个应用,传递相应的数据、执行特定的功能。通过应用跳转可以满足用户更为真实丰富的场景诉求、提升交互体验的便捷性和流畅性。
7.5.1 概述
7.5.1.1 应用场景
应用间跳转在社交分享、推广营销等场景广泛使用。举例如下:
- 社交分享: 在社交软件中分享位置链接、美食推荐链接、购物链接、游戏链接等,可以通过该链接快速跳转到匹配的导航App、美食App、购物App、游戏App等应用。
- 推广营销: 在视频类App、社交类App、浏览器App等广告投放平台中,嵌入需要推广的应用链接(该链接可能以文本、卡片、视频等形式呈现),通过该链接信息可以跳转到目标应用的指定页面;也可以在推送短信、发送邮件时,在正文中携带需要推广的应用链接,通过该链接信息可以跳转到目标应用的指定页面。
7.5.1.2 应用跳转的两种类型
- 拉起指定应用: 拉起方应用明确指定跳转的目标应用,来实现应用跳转。指向性跳转通过openLink或startAbility接口来指定应用链接,拉起目标应用页面。
- 拉起指定类型的应用: 拉起方应用通过指定应用类型,拉起垂类应用面板。该面板将展示目标方接入的垂域应用,由用户选择打开指定应用。
7.5.1.3 典型场景:拉起系统应用
拉起系统应用是应用间跳转的一种典型场景。系统提供了一些能力和接口,在确保访问安全的前提下,可以让开发者快捷地实现系统应用跳转。
拉起系统应用除了采用openlink拉起指定应用、使用startAbilitybyType指定类型的应用,还可以采用如下方式。
- 使用系统Picker组件
相机、文件管理、联系人等系统应用提供了系统Picker组件,支持开发者无需申请权限、即可使用系统应用的一些常用功能,比如访问用户的资源文件。
应用拉起系统Picker组件(文件选择器、照片选择器、联系人选择器等)后,由用户在Picker上选择对应的文件、照片、联系人等资源,应用即可获取到Picker的返回结果。例如,一个音频播放器应用可以通过AudioViewPicker让用户选择音频文件,然后获取所选的音频文件路径进行播放。
- 使用特定接口
设置、电话、日历等应用提供了一些接口,通过这些接口可以直接跳转系统应用。
7.5.2 拉起指定应用
本小节主要介绍如何通过应用链接跳转的方式拉起指定应用。
7.5.2.1 概述
- 应用链接
应用链接是指可以将用户引导至应用内特定位置或相关网页的URL,常见的格式如下。
scheme://host[:port]/path
2. 运作机制
- 目标应用在配置文件中注册自己的URL,并对外提供URL。
- 拉起方应用在跳转接口中传入目标应用的URL等信息。
- 系统接收到URL等相关信息,会寻找对应匹配项,并跳转至目标应用。
- 应用链接分类
按照应用链接的scheme以及校验机制的不同,可以分为Deep Linking与App Linking两种方式。
- Deep Linking:是一种通过链接跳转至应用特定页面的技术,其特点是支持开发者定义任意形式的scheme。由于缺乏域名校验机制,容易被其他应用所仿冒。
- App Linking:其限定了scheme必须为https,同时通过增加域名校验机制,可以从已匹配到的应用中筛选过滤出目标应用,消除应用查询和定位中产生的歧义,直达受信的目标应用。
Deep Linking与App Linking均可以使用openLink接口实现,不同条件下的跳转效果如下。
| 应用链接类型 | App Linking(推荐) | Deep Linking |
|---|---|---|
| appLinkingOnly为false且目标应用已安装 | 直接跳转打开目标应用。 | 跳转目标应用(如果有多个符合条件的应用时,展示应用选择弹框)。 |
| appLinkingOnly为false且目标应用未安装 | 跳转默认浏览器打开网页。 | 返回失败,系统不跳转,由应用自行处理;当前会展示“链接无法打开”弹框。 |
| appLinkingOnly为true且目标应用已安装 | 直接跳转打开目标应用。 | 返回失败,系统不跳转,由应用自行处理。 |
| appLinkingOnly为true且目标应用未安装 | 返回失败,系统不跳转由应用自行处理。 | 返回失败,系统不跳转,由应用自行处理。 |
7.5.2.2 使用canOpenLink判断应用是否可访问
- 使用场景
在应用A想要拉起应用B的场景中,应用A可先调用canOpenLink接口判断应用B是否可访问,如果可访问,再拉起应用B。
- 约束限制
在entry模块的module.json5文件中的querySchemes字段中,最多允许配置50个URL scheme。
- 接口说明
canOpenLink是bundleManager提供的支持判断目标应用是否可访问的接口。
- 操作步骤
- 调用方操作步骤
第一步:在entry模块的module.json5文件中配置querySchemes属性,声明想要查询的URL scheme。
{
"module": {
//...
"querySchemes": [
"app1Scheme"
]
}
}
第二步:导入ohos.bundle.bundleManager模块。
第三步:调用canOpenLink接口。
import { bundleManager } from '@kit.AbilityKit'
import { BusinessError } from '@kit.BasicServicesKit'
import { hilog } from '@kit.PerformanceAnalysisKit'
try {
let link = 'app1Scheme://test.example.com/home'
let canOpen = bundleManager.canOpenLink(link)
hilog.info(
0x0000,
'testTag',
'canOpenLink successfully: %{public}s',
JSON.stringify(canOpen)
)
} catch (err) {
let message = (err as BusinessError).message
hilog.error(
0x0000,
'testTag',
'canOpenLink failed: %{public}s',
message
)
}
- 目标方操作步骤
在module.json5文件中配置uris属性。
{
"module": {
//...
"abilities": [
{
//...
"skills": [
{
"uris": [
{
"scheme": "app1Scheme",
"host": "test.example.com",
"pathStartWith": "home"
}
]
}
]
}
]
}
}
7.5.2.3 使用Deep Linking实现应用间跳转
采用Deep Linking进行跳转时,系统会根据接口中传入的uri信息,在本地已安装的应用中寻找到符合条件的应用并进行拉起。当匹配到多个应用时,会拉起应用选择框。
- 实现原理
Deep Linking基于隐式Want匹配机制中的uri匹配来查询、拉起目标应用。
- 目标应用操作指导
- 配置module.json5文件
为了能够支持被其他应用访问,目标应用需要在module.json5配置文件中配置skills标签。
配置示例如下:
{
"module": {
// ...
"abilities": [
{
// ...
"skills": [
{
"entities": [
"entity.system.home"
],
"actions": [
"action.system.home"
]
},
{
"actions": [
// actions不能为空,actions为空会造成目标方匹配失败。
"ohos.want.action.viewData"
],
"uris": [
{
// scheme必选,可以自定义,以link为例,需要替换为实际的scheme
"scheme": "link",
// host必选,配置待匹配的域名
"host": "www.example.com"
}
]
}
// 新增一个skill对象,用于跳转场景。
// 如果存在多个跳转场景,需配置多个skill对象。
]
}
]
}
}
- 获取并解析拉起方传入的应用链接
在目标应用的UIAbility的onCreate()或者onNewWant()生命周期回调中,获取、解析拉起方传入的应用链接。
// 以EntryAbility.ets为例
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit'
import { url } from '@kit.ArkTS'
export default class EntryAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
// 从want中获取传入的链接信息。
// 如传入的url为:link://www.example.com/programs?action=showall
let uri = want?.uri
if (uri) {
// 从链接中解析query参数,拿到参数后,开发者可根据自己的业务需求进行后续的处理。
let urlObject = url.URL.parseURL(want?.uri)
let action = urlObject.params.get('action')
// 例如,当action为showall时,展示所有的节目。
if (action === "showall") {
// ...
}
}
}
}
- 拉起方应用实现应用跳转
下面通过三个案例,分别介绍如何使用openLink()与startAbility()接口实现应用跳转,以及如何在Web组件中实现应用跳转。
- 使用openLink实现应用跳转
在openLink接口的link字段中传入目标应用的URL信息,并将options字段中的appLinkingOnly配置为false。
示例代码如下:
import { common, OpenLinkOptions } from '@kit.AbilityKit'
import { BusinessError } from '@kit.BasicServicesKit'
import { hilog } from '@kit.PerformanceAnalysisKit'
const TAG: string = '[UIAbilityComponentsOpenLink]'
const DOMAIN_NUMBER: number = 0xFF00
@Entry
@Component
struct Index {
build() {
Button(
'start link',
{ type: ButtonType.Capsule, stateEffect: true }
)
.width('87%')
.height('5%')
.margin({ bottom: '12vp' })
.onClick(() => {
let context: common.UIAbilityContext
= getContext(this) as common.UIAbilityContext
let link: string = "link://www.example.com"
let openLinkOptions: OpenLinkOptions = {
appLinkingOnly: false
}
try {
context.openLink(link, openLinkOptions)
.then(() => {
hilog.info(DOMAIN_NUMBER, TAG, 'open link success.')
}).catch((err: BusinessError) => {
hilog.error(
DOMAIN_NUMBER,
TAG,
`Code is ${err.code}, message is ${err.message}`
)
})
} catch (paramError) {
hilog.error(
DOMAIN_NUMBER,
TAG,
`Code is ${paramError.code}, message is ${paramError.message}`
)
}
})
}
}
- 使用startAbility实现应用跳转
startAbility接口是将应用链接放入want中,通过调用隐式want匹配的方法触发应用跳转。通过startAbility接口启动时,还需要调用方传入待匹配的action和entity。
示例代码如下:
import { common, Want } from '@kit.AbilityKit'
import { BusinessError } from '@kit.BasicServicesKit'
import { hilog } from '@kit.PerformanceAnalysisKit'
const TAG: string = '[UIAbilityComponentsOpenLink]'
const DOMAIN_NUMBER: number = 0xFF00
@Entry
@Component
struct Index {
build() {
Button(
'start ability',
{ type: ButtonType.Capsule, stateEffect: true }
)
.width('87%')
.height('5%')
.margin({ bottom: '12vp' })
.onClick(() => {
let context: common.UIAbilityContext
= getContext(this) as common.UIAbilityContext
let want: Want = {
uri: "link://www.example.com"
}
try {
context.startAbility(want).then(() => {
hilog.info(DOMAIN_NUMBER, TAG, 'start ability success.')
}).catch((err: BusinessError) => {
hilog.error(
DOMAIN_NUMBER,
TAG,
`start ability failed. Code is ${err.code},
message is ${err.message}`
)
})
} catch (paramError) {
hilog.error(
DOMAIN_NUMBER,
TAG,
`Code is ${paramError.code}, message is ${paramError.message}`
)
}
})
}
}
- 使用Web组件实现应用跳转
Web组件需要跳转DeepLink链接应用时,可通过拦截回调onLoadIntercept中对定义的事件进行处理,实现应用跳转。
示例代码如下:
// index.ets
import { webview } from '@kit.ArkWeb'
import { BusinessError } from '@kit.BasicServicesKit'
import { common } from '@kit.AbilityKit'
@Entry
@Component
struct WebComponent {
controller: webview.WebviewController = new webview.WebviewController()
build() {
Column() {
Web({ src: $rawfile('index.html'), controller: this.controller })
.onLoadIntercept((event) => {
const url: string = event.data.getRequestUrl()
if (url === 'link://www.example.com') {
(getContext() as common.UIAbilityContext).openLink(url)
.then(() => {
console.log('openLink success')
}).catch((err: BusinessError) => {
console.error(
'openLink failed, err:' + JSON.stringify(err)
)
})
return true
}
// 返回true表示阻止此次加载,否则允许此次加载
return false
})
}
}
}
前端页面代码:
// index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<h1>Hello World</h1>
<!--方式一、通过绑定事件window.open方法实现跳转-->
<button class="doOpenLink" onclick="doOpenLink()">跳转其他应用一</button>
<!--方式二、通过超链接实现跳转-->
<a href="link://www.example.com">跳转其他应用二</a>
</body>
</html>
<script>
function doOpenLink() {
window.open("link://www.example.com")
}
</script>
7.5.2.4 使用App Linking实现应用间跳转
使用App Linking进行跳转时,系统会根据接口传入的uri信息(HTTPS链接)将用户引导至目标应用中的特定内容,无论应用是否已安装,用户都可以访问到链接对应的内容,跳转体验相比Deep Linking方式更加顺畅。
例如:当开发者使用App Linking接入“扫码直达”服务后,用户可通过控制中心扫一扫等系统级扫码入口,扫描应用的二维码、条形码并跳转到开发者应用对应服务页,实现一步直达的体验。
- 适用场景
- 适用于应用的扫码直达、社交分享、沉默唤醒、广告引流等场景。
- 适用于对安全性要求较高的场景,避免出现被其它应用仿冒的问题。
- 适用于对体验要求较高的应用,不管目标应用是否安装,用户点击该链接都可以正常访问。
- 实现原理
- App Linking在Deep Linking基础上增加了域名校验环节,通过域名校验,可帮助用户消除歧义,识别合法归属于域名的应用,使链接更加安全可靠。
- App Linking要求对于同一HTTPS网址,有应用和网页两种内容的呈现方式。当应用安装时则优先打开应用去呈现内容;当应用未安装时,则打开浏览器呈现Web版的内容。
- 开发概述
若要实现App Linking跳转体验,需被拉起方和拉起方的不同角色相互配合,共同完成。
各个角色的分工如下。
- 被拉起方
| 序号 | ⾓⾊ | 职责 |
|---|---|---|
| 1 | 云端开发 | 在AGC控制台开通App Linking服务。 |
| 2 | 云端开发 | 在开发者网站上关联应用。 |
| 3 | 云端开发 | 在AGC控制台关联网址域名。 |
| 4 | 客户端开发 | 在DevEco Studio中配置关联的网址域名。 |
| 5 | 客户端开发 | 处理传入的链接。 |
| 6 | 前端开发 | 开发链接对应的H5网页,应用未安装时呈现网页版内容。 |
- 拉起方
| 序号 | ⾓⾊ | 职责 |
|---|---|---|
| 1 | 客户端开发 | 调用系统接口,触发链接跳转。 |
具体开发流程请观看视频《扫码直达》。
7.6 进程模型与线程模型
本节讲解了HarmonyOS的进程和线程模型。进程模型中,UIAbility和ExtensionAbility通常运行在独立进程中,WebView有独立的渲染进程,系统支持多进程配置。进程间通信通过公共事件机制实现。线程模型包括主线程、TaskPool Worker线程和Worker线程,分别负责UI绘制、任务调度和耗时操作。EventHub用于线程间事件的发送和处理。
7.6.1 进程模型
进程是系统进行资源分配的基本单位,是操作系统结构的基础。系统的进程模型如下图所示。
- 通常情况下,应用中(同一Bundle名称)的所有UIAbility均是运行在同一个独立进程(主进程)中,如下图中绿色部分的“Main Process”。
- 应用中(同一Bundle名称)的所有同一类型ExtensionAbility均是运行在一个独立进程中,如下图中蓝色部分的“FormExtensionAbility Process”、“InputMethodExtensionAbility Process”、其他ExtensionAbility Process。
- WebView拥有独立的渲染进程,如下图中黄色部分的“Render Process”。
图7-21 进程模型示意图
在上述模型基础上,对于系统应用可以通过申请多进程权限(如图7-24所示),为指定HAP配置一个自定义进程名,该HAP中的UIAbility就会运行在自定义进程中。不同的HAP可以通过配置module.json5中的process属性,使HAP运行在不同进程中。
图7-22 多进程示意图
基于当前的进程模型,针对应用间和应用内存在多个进程的情况,系统提供了如下进程间通信机制:公共事件机制:多用于一对多的通信场景,公共事件发布者可能存在多个订阅者同时接收事件。
7.6.2 线程模型
线程是操作系统进行运算调度的基本单位,是进程中的执行流,共享进程的资源。一个进程可以包含多个线程。
- 线程类型
Stage模型下的线程主要有如下三类:
- 主线程
-
- 执行UI绘制。
- 管理主线程的ArkTS引擎实例,使多个UIAbility组件能够运行在其之上。
- 管理其他线程的ArkTS引擎实例,例如使用TaskPool(任务池)创建任务或取消任务、启动和终止Worker线程。
- 分发交互事件。
- 处理应用代码的回调,包括事件处理和生命周期管理。
- 接收TaskPool以及Worker线程发送的消息。
-
- 用于执行耗时操作,支持设置调度优先级、负载均衡等功能,推荐使用。
-
- 用于执行耗时操作,支持线程间通信。
图7-23 线程模型示意图
- 使用EventHub进行线程内通信
EventHub提供了线程内发送和处理事件的能力,包括对事件订阅、取消订阅、触发事件等。
7.7 案例实战
本节通过两个实战案例讲解了如何在Stage模型下创建和使用Ability,以及如何拉起系统相机。
7.7.1 Stage模型下Abliity的创建和使用
本案例基于Stage模型,对Ability的创建和使用进行讲解。首先使用DevEco Studio创建一个Stage模型Ability,并使用UIAbilityContext启动另一个Ability,然后借助Want,在Ability之间传递参数,最后使用HiLog打印Ability的生命周期。效果如下图所示:
图7-24 案例效果图
7.2.1.1 案例运用到的知识点
- 核心知识点
- UIAbility:UIAbility组件是系统调度的基本单元,为应用提供绘制界面的窗口;一个UIAbility组件中可以通过多个页面来实现一个功能模块。每一个UIAbility组件实例,都对应于一个最近任务列表中的任务。
- UIAbilityContext:UIAbilityContext模块提供允许访问特定Ability的资源的能力,包括对Ability的启动、停止的设置、获取caller通信接口、拉起弹窗请求用户授权等。
- Want:Want是对象间信息传递的载体, 可以用于应用组件间的信息传递。 Want的使用场景之一是作为startAbility的参数, 其包含了指定的启动目标, 以及启动时需携带的相关数据。
- HiLog:HiLog日志系统,让应用可以按照指定类型、指定级别、指定格式字符串输出日志内容,帮助开发者了解应用的运行状态,更好地调试程序。
- 其他知识点
- ArkTS 语言基础
- V2版状态管理:@ComponentV2/@Local/@Param
- 自定义组件和组件生命周期
- @Builder装饰器:自定义构建函数
- @BuilderParam装饰器:引用@Builder函数
- @Extend装饰器:定义扩展组件样式
- @Styles装饰器:定义组件重用样式
- ForEach:循环渲染
- 内置组件:Column/Row/Flex/Scroll/List/Swiper/Tabs/Stack/Text/Span/TextInput/Image/Button/Blank/Divider
- 日志管理类的编写
- 常量与资源分类的访问
- 组件导航 (Navigation)
- MVVM模式
7.2.1.2 代码结构
├──entry/src/main/ets // 代码区
│ ├──common // 公共资源目录
│ ├──DetailsAbility
│ │ └──DetailsAbility.ets // 关联详情页面的UIAbility
│ ├──entryability
│ │ └──EntryAbility.ets // 程序入口类
│ ├──model
│ │ └──DataModel.ets // 业务逻辑文件
│ ├──pages
│ │ ├──DetailsPage.ets // 详情页面
│ │ └──NavPage.ets // 导航页面
│ ├──view // 自定义组件目录
│ └──viewmodel // 视图业务逻辑文件目录
└──entry/src/main/resources // 资源文件目录
7.2.1.3 核心代码解读
- 创建Ability和Page页面
在本案例中,我们需要创建两个Ability:EntryAbility,DetailsAbility。其中EntryAbility是由工程默认创建的,这里我们只讲如何创建DetailsAbility。
- 使用DevEco Studio,选中对应的模块,单击鼠标右键,选择New > Ability,在对话框中修改名字后,即可创建相关的Ability。
图7-25 新建Ability
这里填入Ability的名字,会自动生成目录和ets文件。
图7-26 命名Ability
图7-27 创建好的Ablility
- 创建完Ability后,需要我们为Ability设置page页面,选中pages目录,单击鼠标右键,选择New > Page,在对话框中修改名字后,即可创建相关的Page页面。
图7-28 为Ability设置page页面
图7-29 命名页面
图7-30 创建好的页面
DetailsPage.ets的主要代码如下:
// entry/src/main/ets/pages/DetailsPage.ets
...
@Entry
@ComponentV2
struct DetailsPage {
private goodsDetails: GoodsData = new GoodsData()
aboutToAppear() {
if (position) {
this.goodsDetails = viewModel.loadDetails(position)
}
}
...
build() {
Column() {
Scroll() {
Column() {
Stack({ alignContent: Alignment.Top }) {
// GoodsPreviewer 显示商品图片
PreviewerComponent({ goodsImg: this.goodsDetails.goodsImg })
this.topBarLayout()
}
.height(DetailsPageStyle.TOP_LAYOUT_HEIGHT)
.width(PERCENTAGE_100)
.backgroundColor($r('app.color.background1'))
// 关于商品信息的卡片布局样式
this.cardsLayout()
}
.width(PERCENTAGE_100)
}
.height(DetailsPageStyle.SCROLL_LAYOUT_WEIGHT)
.backgroundColor($r('app.color.background'))
// 底部工具栏
BottomBarComponent()
.height(DetailsPageStyle.TOOLBAR_WEIGHT)
}
.height(PERCENTAGE_100)
.width(PERCENTAGE_100)
}
...
}
使用windowStage.loadContent为指定Ability设置相关的Page页面。
// entry/src/main/ets/detailsability/DetailsAbility.ets
...
export default class DetailsAbility extends UIAbility {
...
onWindowStageCreate(windowStage: window.WindowStage) {
...
windowStage.loadContent('pages/DetailsPage', (err, data) => {
if (err.code) {
hilog.error(
DETAIL_ABILITY_DOMAIN, TAG,
'Failed. Cause: %{public}s',
JSON.stringify(err) ?? ''
)
return
}
hilog.info(
DETAIL_ABILITY_DOMAIN, TAG,
'Succeeded. Data: %{public}s',
JSON.stringify(data) ?? ''
)
})
}
...
}
2.UIAbilityContext模块启动Ability的能力
UIAbilityContext模块提供允许访问特定Ability的资源的能力,包括对Ability的启动、停止的设置、获取caller通信接口、拉起弹窗请求用户授权等。
在本案例中,我们点击首页商品列表中的某一项商品,即可跳转到商品的详情页面。此处使用到UIAbilityContext模块的启动Ability的能力。关于获取UIAbilityContext的方法,推荐使用getContext(this)方式来获取UIAbilityContext。
HomePage.ets的主要代码如下:
// entry/src/main/ets/view/home/HomePage.ets
...
build() {
Column() {
Blank()
.height(HomePageStyle.BLANK_HEIGHT)
// Logo和二维码区域
TopBarComponent()
.padding({
top: HomePageStyle.PADDING_VERTICAL,
bottom: HomePageStyle.PADDING_VERTICAL,
left: HomePageStyle.PADDING_HORIZONTAL,
right: HomePageStyle.PADDING_HORIZONTAL
})
SearchComponent()
TabsComponent({ tabMenus: this.tabMenus })
BannerComponent({ bannerList: this.bannerList })
MenusComponent({ menus: this.menus })
// 商品列表组件
GoodsComponent({
goodsList: this.goodsList, startPage: (index) => {
let handler = getContext(this) as common.UIAbilityContext
viewModel.startDetailsAbility(handler, index)
}
})
}
.width(PERCENTAGE_100)
.height(PERCENTAGE_100)
.backgroundImage(
$rawfile('index/index_background.png'),
ImageRepeat.NoRepeat
)
.backgroundImageSize(ImageSize.Cover)
}
...
startDetailsAbility方法调用了UIAbilityContext模块启动Ability的能力。
// entry/src/main/ets/viewmodel/HomeViewModel.ets
...
public startDetailsAbility(
context: common.UIAbilityContext,
index: number
): void {
const want: Want = {
bundleName: getContext(context).applicationInfo.name,
abilityName: DETAILS_ABILITY_NAME,
parameters: {
position: index
}
}
try {
context.startAbility(want)
} catch (error) {
hilog.error(HOME_PAGE_DOMAIN, TAG, '%{public}s', error)
}
}
...
- 信息传递载体Want
Want是对象间信息传递的载体, 可以用于应用组件间的信息传递。Want的使用场景之一是作为startAbility的参数, 其包含了指定的启动目标, 以及启动时需携带的相关数据。
在本案例的EntryAbility中,我们使用startDetailsAbility方法启动DetailsAbility,并在代码中指定了Want的具体参数,并使用parameters参数传递商品信息。
在DetailsAbility中通过AppStorage来存储detailWant对象。
// entry/src/main/ets/detailsability/DetailsAbility.ets
...
export default class DetailsAbility extends UIAbility {
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) {
let index: number = want?.parameters?.position as number
hilog.info(
DETAIL_ABILITY_DOMAIN,
TAG, '新页面参数:bundle name',
want?.bundleName,
want?.abilityName,
index
)
AppStorage.setOrCreate(KEY, index)
hilog.info(
DETAIL_ABILITY_DOMAIN,
TAG,
'%{public}s', 'Ability onCreate'
)
}
...
}
在DetailsPage页面中,使用AppStorage来获取detailWant对象,解析detailWant对象中的商品信息参数,调用loadDetails方法来展示商品详情。
// entry/src/main/ets/pages/DetailsPage.ets
...
let viewModel: DetailsViewModel = new DetailsViewModel()
const KEY: string = 'GoodsPosition'
let position = AppStorage.get<number>(KEY)
...
@Entry
@Component
struct DetailsPage {
private goodsDetails: GoodsData = new GoodsData()
aboutToAppear() {
if (position) {
this.goodsDetails = viewModel.loadDetails(position)
}
}
...
}
- 使用HiLog打印生命周期函数
我们在编写日志管理类时学习过HiLog,这里我们再回顾一下。HiLog日志系统可以让应用按照指定类型、指定级别、指定格式字符串打印日志内容,帮助开发者了解应用/服务的运行状态,更好地调试程序。
HiLog提供了debug、info、warn、error以及fatal接口,在本案例中,我们使用hilog打印EntryAbility 、DetailsAbility的生命周期。
在打印之前,我们需要了解三个参数:
- domain:日志对应的领域标识,范围是0x0~0xFFFF。建议开发者在应用内根据需要自定义划分。
- tag:指定日志标识,可以为任意字符串,建议用于标识调用所在的类或者业务行为。
- level:日志级别。
- format:格式字符串,用于日志的格式化输出。格式字符串中可以设置多个参数,参数需要包含参数类型、隐私标识。隐私标识分为{public}和{private},缺省为{private}。标识{public}的内容明文输出,标识{private}的内容以过滤回显。
下面我们在EntryAbility中演示如何使用hilog对象打印Ability的生命周期函数 onBackground。
// entry/src/main/ets/entryability/EntryAbility.ets
...
export default class EntryAbility extends UIAbility {
...
onBackground() {
// Ability 已返回后台
hilog.isLoggable(ENTRY_ABILITY_DOMAIN, TAG, hilog.LogLevel.INFO)
hilog.info(
ENTRY_ABILITY_DOMAIN,
TAG,
'%{public}s', 'Ability onBackground'
)
}
}
7.2.1.4 代码与视频教程
完整案例代码与视频教程请参见:
代码:Code-07-02.zip。
视频:《Stage模型下Ability的创建和使用案例实战》。
7.7.2 拉起系统相机
本案例实现了如何拉起系统相机拍一张照片并返回应用。效果如下图所示:
图7-31 案例效果图
7.7.2.1 案例运用到的知识点
- 核心知识点
- 使用cameraPicker拉起系统相机。
- 其他知识点
- ArkTS 语言基础
- V2版状态管理:@ComponentV2/@Local
- 自定义组件
- 内置组件:Column/Image/Button
- 常量与资源分类的访问
7.7.2.2 代码结构
├──entry/src/main/ets
│ ├──common
│ │ └──constants
│ │ └──CommonConstants.ets // 常量类
│ ├──entryability
│ │ └──EntryAbility.ets // 程序入口类
│ └──pages
│ └──MainPage.ets // 主页面
└──entry/src/main/resources // 应用静态资源目录
7.7.2.3 核心代码解读
// entry/src/main/ets/pages/MainPage.ets
import { cameraPicker } from '@kit.CameraKit'
import { camera } from '@kit.CameraKit'
import { BusinessError } from '@ohos.base'
import { hilog } from '@kit.PerformanceAnalysisKit'
import {
CommonConstants as Const
} from '../common/constants/CommonConstants'
@Entry
@ComponentV2
struct ImagePickerPage {
// 存储选择的图像或视频的 URI,初始为 undefined
@Local uri: Resource | string | undefined = undefined
// 定义相机位置数组
private cameraPosition: Array<camera.CameraPosition> = [
// 未指定相机位置
camera.CameraPosition.CAMERA_POSITION_UNSPECIFIED,
// 后置摄像头
camera.CameraPosition.CAMERA_POSITION_BACK,
// 前置摄像头
camera.CameraPosition.CAMERA_POSITION_FRONT
]
// 定义支持的媒体类型,包含图片和视频
private mediaType: Array<cameraPicker.PickerMediaType> = [
// 图片
cameraPicker.PickerMediaType.PHOTO,
// 视频
cameraPicker.PickerMediaType.VIDEO
]
build() {
Row() {
Column() {
Image(this.uri)
.height($r('app.float.image_height'))
.alt($r('app.media.startIcon'))
Button($r('app.string.capture'))
.width($r('app.float.button_width'))
.margin({ top: $r('app.float.margin') })
.onClick(async () => {
try {
// 配置启动后置摄像头
let pickerProfile: cameraPicker.PickerProfile = {
cameraPosition: this.cameraPosition[1]
}
// 配置拍照模式
let pickerResult: cameraPicker.PickerResult
= await cameraPicker.pick(
getContext(this), // 获取上下文对象
[this.mediaType[0]], // 配置选择媒体类型为照片(PHOTO)
pickerProfile // 配置相机配置
)
// 获取视频 URI
this.uri = pickerResult.resultUri
// 输出日志,显示选择结果
hilog.info(
0x0000,
' ',
"the pick pickerResult is:" + JSON.stringify(pickerResult)
)
} catch (error) {
// 如果出现错误,捕获异常并输出错误日志
let err = error as BusinessError
hilog.error(0x0000,
'',
`the pick call failed. error code: ${err.code}`
)
}
})
}
.width(Const.FULL_SIZE)
}
.height(Const.FULL_SIZE)
}
}
以上为本案例的核心页面代码,通过构建一个包含图像显示和按钮操作的页面,让用户能够点击按钮启动相机,配置相机为后置摄像头并选择照片或视频。操作完成后,获取到选中的媒体 URI,并展示在界面上,同时处理了可能的错误并进行了日志记录。
7.7.2.4 代码与视频教程
完整案例代码与视频教程请参见:
代码:Code-07-02.zip。
视频:《拉起系统相机》。
7.8 本章小结
本章围绕 HarmonyOS 的 Stage 模型展开,全面介绍了其概念、应用程序包、组件、应用间跳转、进程与线程模型及案例实战,具体内容如下:
- Stage 模型基础概念:应用程序框架用于简化开发,应用模型则抽象描述应用,FA 和 Stage 模型是系统先后提供的应用模型,当前主推 Stage 模型,其多个应用组件共享同一个 ArkTS 引擎实例,减少内存占用,方便组件间共享对象和状态。
- 应用程序包:应用程序包由应用与应用程序包、多Module设计机制、Module类型、HAR与HSP区别、开发态包结构、应用配置文件、编译态包结构、发布态包结构、选择合适的包类型等内容构成。开发者需了解这些知识,才能更好地开发和管理应用。
- 应用组件:介绍了AbilityStage、UIAbility组件,包括UIAbility组件生命周期、启动模式、基本用法、组件间交互。开发者可根据业务场景选择合适的启动模式和交互方式,提升应用性能和用户体验。
- 应用间跳转:涵盖应用跳转的概述、拉起指定应用等内容。应用跳转在社交分享、推广营销等场景广泛应用,拉起指定应用可通过多种方式实现,开发者可根据需求选择合适的跳转方式和链接类型。
- 进程与线程模型:进程是系统资源分配基本单位,应用内UIAbility和ExtensionAbility通常运行在特定进程,系统提供进程间通信机制;线程是运算调度基本单位,Stage模型下有主线程、TaskPool、Worker线程,EventHub可用于线程内通信。
- 案例实战:通过两个案例,展示了Stage模型下Ability的创建和使用,以及拉起系统相机的功能实现,涉及多个知识点和代码实践,帮助开发者更好地理解和应用Stage模型。