第 7 章 Stage模型

211 阅读36分钟

作为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 应用程序框架和应用模型

  1. 应用程序框架

应用程序框架(Application Framework)是一种编程框架,用于简化应用程序的开发过程,提高代码的可重用性和可维护性。它可以帮助开发人员更快速、更高效地开发应用程序。

应用程序框架也是连接开发者和用户的桥梁。对于用户,将应用安装到设备上并打开应用、启动收付款页面、在当前应用中启动浏览器打开网站、启动文件应用浏览或编辑文件等行为,都是需要开发者借助应用程序框架的能力来实现的。对于开发者,使用应用程序框架所提供的能力,如应用进程的创建与销毁、应用组件的运行入口、生命周期调度、组件间的交互等,是实现用户功能的关键。

  1. 应用模型

应用模型是一个应用程序的抽象描述,用于表示应用程序的不同方面,如应用组件、进程模型、线程模型、任务管理和包管理。应用模型提供了一种统一的语言和架构来描述应用程序的各个方面,有助于开发者更好地理解应用程序的架构和设计,从而更有效地利用应用程序框架。

随着系统的发展,先后提供了FA模型和Stage模型。

  • FA(Feature Ability)模型:从API 7开始支持,但已不再作为主推模型。
  • Stage模型:从API 9开始新增,是目前主推的模型,并将长期演进。在该模型中,提供了AbilityState、WindowStage等类,作为应用组件和窗口的“舞台”,因此称为Stage模型。

两种模型的最大区别在于:在Stage模型中,多个应用组件共享同一个ArkTS引擎实例;而在FA模型中,每个应用组件独立拥有一个ArkTS引擎实例。因此,Stage模型使得应用组件之间可以方便地共享对象和状态,并减少复杂应用对内存的占用。

  1. 二者的区别

应用程序框架可以被看做是应用模型的一种实现方式。开发人员可以根据应用模型描述应用程序的结构和行为,然后使用应用程序框架来实现这些描述。

7.1.2 Stage模型设计目标

  1. 为复杂应用而设计

在Stage模型中,多个应用组件共享同一个ArkTS引擎实例,应用组件之间可以方便地共享对象和状态,这样可以减少复杂应用运行时对内存的占用。另外,在开发过程中采用面向对象的开发方式,可以使得复杂应用的代码更具可读性、易维护性和可扩展性。

  1. 原生支持应用组件级的跨端迁移和多端协同

Stage模型提供了应用组件间通信的RPC调用能力。因此,在不同设备上的应用组件可以便捷地迁移应用数据和状态。由于UI和UIAbility之间的分离,可以利用ArkUI的声明式特点,通过组件中数据和状态的变化,更新用户界面,从而实现跨端迁移和多端协同。

  1. 支持多设备和多窗口形态

Stage模型在框架层面实现了应用组件管理和窗口管理的解耦,为系统提供了对应用组件进行裁剪和扩展窗口形态的能力。同时,Stage模型为应用组件设计了一套生命周期管理机制,以适应不同设备,从而减少应用组件在不同设备上的行为差异。

  1. 平衡应用能力和系统管控成本

Stage模型拥有后台应用程序治理机制,防止应用程序随意滞留在后台。同时,应用后台行为会受到管理,避免恶意应用行为的发生。此外,Stage模型还为卡片服务、输入法服务等提供了特定的应用组件,以满足更多的开发场景。

7.1.3 Stage模型有关概念

7.1.3.1 基本概念

  1. UIAblility组件

UIAblility组件是一种包含UI的应用组件,主要是用于和用户交互。在开发态新建一个项目时,IDE默认生成一个持有UlAbility组件的Model作为应用的主模块,该模块用于实现应用的入口界面和主特性功能。对于UIAblility组件,系统提供了具体的UlAbility类承载,支持面向对象的开发方式,如图7-1所示,系统默认生成的EntryAbility类继承了UlAbility类。

图7-1 EntryAbility示意图

  1. ExtensionAbility组件

ExtensionAbility组件是一种面向特定场景的应用组件,每一个具体场景对应一个ExtensionAbilityType,开发者只能使用系统已定义的类型,各类型的ExtensionAbility组件均由相应的系统服务统一管理,例如InputMethodExtensionAbility组件由输入法管理服务统一管理。目前ExtensionAbility组件类型有11种,在IDE中支持选中Module后,创建不同类型的ExtensionAbility组件文件,如图7-2所示。

图7-2 ExtensionAbility组件示意图

  1. HAP

包含UlAbility组件或者ExtensionAbility组件的Module可以单独运行。该类型Module运行时会编译生成一个.hap文件,即HAP,如图7-3所示。

图7-3 HAP包示意图

  1. 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两种共享包的主要区别体现在:

共享包类型编译和运行方式发布和引用方式
HARHAR中的代码和资源跟随使用方编译,如果有多个使用方,它们的编译产物中会存在多份相同拷贝。HAR除了支持应用内引用,还可以独立打包发布,供其他应用引用。
HSPHSP中的代码和资源可以独立编译,运行时在一个进程中代码也只会存在一份。HSP一般随应用进行打包,当前支持应用内和集成态HSP。应用内HSP只支持应用内引用,集成态HSP支持发布到ohpm私仓和跨应用引用。

图7-5 HAR和HSP在APP包中的形态示意图

7.2.2 Stage模型应用程序包结构

7.2.2.1 开发态包结构

图7-6 项目工程结构示意图(以实际为准)

  1. 配置文件

包括应用级配置信息、以及Module级配置信息:

  • AppScope > app.json5:app.json5配置文件,用于声明应用的全局配置信息,比如应用Bundle名称、应用名称、应用图标、应用版本号等。
  • Module_name > src > main > module.json5:module.json5配置文件,用于声明Module基本信息、支持的设备类型、所含的组件信息、运行所需申请的权限等。
  1. ArkTS源码文件

Module_name > src > main > ets:用于存放Module的ArkTS源码文件(.ets文件)。

  1. 资源文件

包括应用级资源文件、以及Module级资源文件,支持图形、多媒体、字符串、布局文件等。

  • AppScope > resources :用于存放应用需要用到的资源文件。
  • Module_name > src > main > resources :用于存放该Module需要用到的资源文件。
  1. 其他配置文件

用于编译构建,包括构建配置文件、编译构建任务脚本、混淆规则文件、依赖的共享包信息等。

  • 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 选择合适的包类型

  1. 各种包类型对比

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)

  1. 如何选择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_Char_D),依然直接在各自的模块中保留这些依赖。尽管这种方式会导致包体积的增大,但只要整体大小仍在可接受范围内,该模式依然可取,其优势在于能减少动态加载带来的额外性能开销,从而提升运行效率。
  • 应用体积优先策略
    若应用对包体积较为敏感,可采用中间层进行优化。例如,使用一个公共HAP(common_hap)作为桥接层,该HAP内封装har_Char_D,并通过HSP动态加载的方式供其他模块使用。这样一来,在保证功能完整的前提下,可有效减少应用整体体积,从而提升存储利用率与安装效率。

图7-14 单HAP场景优化策略

如图 7-15 所示,这是一个多 HAP 场景,主要应用于 2-in-1 设备,例如文档应用中分别打开 Word、Excel、PPT 等,每个功能模块可独立生成一个 Feature(Feather)并对应一个 HAP。每个 HAP 需维护自身的 HAR 包,如图中的 1.hap2.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文件,具体步骤如下。

  1. 在工程Module对应的ets目录下,右键选择“New > Directory”,新建一个目录并命名为myabilitystage。
  2. 在myabilitystage目录,右键选择“New > ArkTS File”,新建一个文件并命名为MyAbilityStage.ets。
  3. 打开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'
  }
}
  1. 在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的设计理念:

  1. 原生支持应用组件级的跨端迁移和多端协同。
  2. 支持多设备和多窗口形态。

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 生命周期状态说明

  1. 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 {
    // 页面初始化
  }
  // ...
}
  1. 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资源
  }
}
  1. 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资源
  }
}
  1. 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) {
    // 更新资源、数据
  }
}
  1. 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。基本原理如下:

  1. EntryAbility调用startAbility()方法,并在Want的parameters字段中设置唯一的Key值,用于标识SpecifiedAbility。
  2. 系统在拉起SpecifiedAbility之前,会先进入对应的AbilityStage的onAcceptWant()生命周期回调,获取用于标识目标UIAbility的Key值。
  3. 系统会根据获取的Key值来匹配UIAbility。
    • 如果匹配到对应的UIAbility,则会启动该UIAbility实例,并进入onNewWant()生命周期回调。
    • 如果无法匹配对应的UIAbility,则会创建一个新的UIAbility实例,并进入该UIAbility实例的onCreate()生命周期回调和onWindowStageCreate()生命周期回调。

具体实现步骤如下:

  1. 在SpecifiedAbility中,需要将module.json5配置文件的launchType字段配置为specified。
{
  "module": {
    // ...
    "abilities": [
      {
        "launchType": "specified",
        // ...
      }
    ]
  }
}
  1. 在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%')
   }
 }
  1. 开发者根据业务在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对象,本节将以此为例进行说明。

  1. 在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
  }
}
  1. 在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'
            });
          })
        }
        // ...
      }
      // ...
    }
    // ...
  }
}
  1. 在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。

  1. 在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.')
            })
          })
        }
        //...
      }
      //...
    }
    //...
  }
}
  1. 在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
  }
  //...
}
  1. 在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
            }
          })
        })
    }
    //...
  }
}
  1. 如需要关闭应用所有的UIAbility实例,可以调用ApplicationContext的killAllProcesses()方法实现关闭应用所有的进程。

7.4.5.2 启动应用内的UIAbility并获取返回结果

在一个EntryAbility启动另外一个FuncAbility时,希望在被启动的FuncAbility完成相关业务后,能将结果返回给调用方。例如在应用中将入口功能和账号登录功能分别设计为两个独立的UIAbility,在账号登录UIAbility中完成登录操作后,需要将登录的结果返回给入口UIAbility。

  1. 在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}`
              )
            })
          })
        }
        //...
      }
      //...
    }
    //...
  }
}
  1. 在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
              }
            })
          })
        }
        //...
      }
      //...
    }
    //...
  }
}
  1. 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的指定页面

  1. 概述

一个UIAbility可以对应多个页面,在不同的场景下启动该UIAbility时需要展示不同的页面,例如从一个UIAbility的页面中跳转到另外一个UIAbility时,希望启动目标UIAbility的指定页面。

UIAbility的启动分为两种情况:UIAbility冷启动和UIAbility热启动。

  • UIAbility冷启动:指的是UIAbility实例处于完全关闭状态下被启动,这需要完整地加载和初始化UIAbility实例的代码、资源等。
  • UIAbility热启动:指的是UIAbility实例已经启动并在前台运行过,由于某些原因切换到后台,再次启动该UIAbility实例,这种情况下可以快速恢复UIAbility实例的状态。

本小节主要讲解目标UIAbility冷启动和目标UIAbility热启动两种启动指定页面的场景,以及在讲解启动指定页面之前会讲解到在调用方如何指定启动页面。

  1. 调用方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}`
              )
            })
          })
        }
        //...
      }
      //...
    }
    //...
  }
}
  1. 目标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) => {
      // ...
    })
  }
}
  1. 目标UIAbility热启动

在应用开发中,会遇到目标UIAbility实例之前已经启动过的场景,这时再次启动目标UIAbility时,不会重新走初始化逻辑,只会直接触发onNewWant()生命周期方法。为了实现跳转到指定页面,需要在onNewWant()中解析参数进行处理。

例如短信应用和联系人应用配合使用的场景。

  1. 用户先打开短信应用,短信应用的UIAbility实例启动,显示短信应用的主页。
  2. 用户将设备回到桌面界面,短信应用进入后台运行状态。
  3. 用户打开联系人应用,找到联系人张三。
  4. 用户点击联系人张三的短信按钮,会重新启动短信应用的UIAbility实例。
  5. 由于短信应用的UIAbility实例已经启动过了,此时会触发该UIAbility的onNewWant()回调,而不会再走onCreate()和onWindowStageCreate()等初始化逻辑。

图7-20 目标UIAbility热启动

开发步骤如下所示。

  1. 冷启动短信应用的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) ?? ''
      )
    })
  }
}
  1. 在短信应用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 概述

  1. 应用链接

应用链接是指可以将用户引导至应用内特定位置或相关网页的URL,常见的格式如下。

scheme://host[:port]/path

2. 运作机制

  • 目标应用在配置文件中注册自己的URL,并对外提供URL。
  • 拉起方应用在跳转接口中传入目标应用的URL等信息。
  • 系统接收到URL等相关信息,会寻找对应匹配项,并跳转至目标应用。
  1. 应用链接分类

按照应用链接的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判断应用是否可访问

  1. 使用场景

在应用A想要拉起应用B的场景中,应用A可先调用canOpenLink接口判断应用B是否可访问,如果可访问,再拉起应用B。

  1. 约束限制

在entry模块的module.json5文件中的querySchemes字段中,最多允许配置50个URL scheme。

  1. 接口说明

canOpenLink是bundleManager提供的支持判断目标应用是否可访问的接口。

  1. 操作步骤
  • 调用方操作步骤

第一步:在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信息,在本地已安装的应用中寻找到符合条件的应用并进行拉起。当匹配到多个应用时,会拉起应用选择框。

  1. 实现原理

Deep Linking基于隐式Want匹配机制中的uri匹配来查询、拉起目标应用。

  1. 目标应用操作指导
  • 配置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对象。
        ]
      }
    ]
  }
}
  1. 获取并解析拉起方传入的应用链接

在目标应用的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") {
         // ...
      }
    }
  }
}
  1. 拉起方应用实现应用跳转

下面通过三个案例,分别介绍如何使用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接入“扫码直达”服务后,用户可通过控制中心扫一扫等系统级扫码入口,扫描应用的二维码、条形码并跳转到开发者应用对应服务页,实现一步直达的体验。

  1. 适用场景
  • 适用于应用的扫码直达、社交分享、沉默唤醒、广告引流等场景。
  • 适用于对安全性要求较高的场景,避免出现被其它应用仿冒的问题。
  • 适用于对体验要求较高的应用,不管目标应用是否安装,用户点击该链接都可以正常访问。
  1. 实现原理
  • App Linking在Deep Linking基础上增加了域名校验环节,通过域名校验,可帮助用户消除歧义,识别合法归属于域名的应用,使链接更加安全可靠。
  • App Linking要求对于同一HTTPS网址,有应用和网页两种内容的呈现方式。当应用安装时则优先打开应用去呈现内容;当应用未安装时,则打开浏览器呈现Web版的内容。
  1. 开发概述

若要实现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 线程模型

线程是操作系统进行运算调度的基本单位,是进程中的执行流,共享进程的资源。一个进程可以包含多个线程。

  1. 线程类型

Stage模型下的线程主要有如下三类:

  • 主线程
    • 执行UI绘制。
    • 管理主线程的ArkTS引擎实例,使多个UIAbility组件能够运行在其之上。
    • 管理其他线程的ArkTS引擎实例,例如使用TaskPool(任务池)创建任务或取消任务、启动和终止Worker线程。
    • 分发交互事件。
    • 处理应用代码的回调,包括事件处理和生命周期管理。
    • 接收TaskPool以及Worker线程发送的消息。
    • 用于执行耗时操作,支持设置调度优先级、负载均衡等功能,推荐使用。
    • 用于执行耗时操作,支持线程间通信。

图7-23 线程模型示意图

  1. 使用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 案例运用到的知识点

  1. 核心知识点
  • UIAbility:UIAbility组件是系统调度的基本单元,为应用提供绘制界面的窗口;一个UIAbility组件中可以通过多个页面来实现一个功能模块。每一个UIAbility组件实例,都对应于一个最近任务列表中的任务。
  • UIAbilityContext:UIAbilityContext模块提供允许访问特定Ability的资源的能力,包括对Ability的启动、停止的设置、获取caller通信接口、拉起弹窗请求用户授权等。
  • Want:Want是对象间信息传递的载体, 可以用于应用组件间的信息传递。 Want的使用场景之一是作为startAbility的参数, 其包含了指定的启动目标, 以及启动时需携带的相关数据。
  • HiLog:HiLog日志系统,让应用可以按照指定类型、指定级别、指定格式字符串输出日志内容,帮助开发者了解应用的运行状态,更好地调试程序。
  1. 其他知识点
  • 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 核心代码解读

  1. 创建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)
    }
  }
...
  1. 信息传递载体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)
    }
  }
 ...
}
  1. 使用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 案例运用到的知识点

  1. 核心知识点
  • 使用cameraPicker拉起系统相机。
  1. 其他知识点
  • 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 模型展开,全面介绍了其概念、应用程序包、组件、应用间跳转、进程与线程模型及案例实战,具体内容如下:

  1. Stage 模型基础概念:应用程序框架用于简化开发,应用模型则抽象描述应用,FA 和 Stage 模型是系统先后提供的应用模型,当前主推 Stage 模型,其多个应用组件共享同一个 ArkTS 引擎实例,减少内存占用,方便组件间共享对象和状态。
  2. 应用程序包:应用程序包由应用与应用程序包、多Module设计机制、Module类型、HAR与HSP区别、开发态包结构、应用配置文件、编译态包结构、发布态包结构、选择合适的包类型等内容构成。开发者需了解这些知识,才能更好地开发和管理应用。
  3. 应用组件:介绍了AbilityStage、UIAbility组件,包括UIAbility组件生命周期、启动模式、基本用法、组件间交互。开发者可根据业务场景选择合适的启动模式和交互方式,提升应用性能和用户体验。
  4. 应用间跳转:涵盖应用跳转的概述、拉起指定应用等内容。应用跳转在社交分享、推广营销等场景广泛应用,拉起指定应用可通过多种方式实现,开发者可根据需求选择合适的跳转方式和链接类型。
  5. 进程与线程模型:进程是系统资源分配基本单位,应用内UIAbility和ExtensionAbility通常运行在特定进程,系统提供进程间通信机制;线程是运算调度基本单位,Stage模型下有主线程、TaskPool、Worker线程,EventHub可用于线程内通信。
  6. 案例实战:通过两个案例,展示了Stage模型下Ability的创建和使用,以及拉起系统相机的功能实现,涉及多个知识点和代码实践,帮助开发者更好地理解和应用Stage模型。