【拥抱鸿蒙】HarmonyOS NEXT深入理解应用程序框架

1,606 阅读10分钟

本文正在参加华为鸿蒙有奖征文征文活动

HarmonyOS的应用程序框架是应用开发的基础,要理解应用程序在系统中如何运行,先要理解UIAbility的生命周期、启动模式以及跳转等。

这里还涉及到AbilityStage的能力以及应用包结构与使用场景。

理解应用程序框架有助于理解App运行的底层逻辑,有助于理解开发工程在开发态和编译态的组成部分和形式,帮助我们在开发过程中更好的设计工程模块和包组成形式,以便于精准而高效进行开发。

注意:本篇所有内容均基于Stage模型。

目标

  • 掌握UIAbility的概念及其在HarmonyOS应用开发中的重要性。
  • 理解UIAbility内页面的跳转和数据传递机制,掌握如何在应用内实现页面间的顺畅切换和数据交换。
  • 深入学习UIAbility的生命周期,理解如何有效地管理应用的生命周期状态,以优化应用性能和提供更好的用户体验。
  • 了解应用组件相关的基本概念和设计思想,并掌握UIAbility组件的启动模式和UIAbility组件间的交互机制,能够在实际开发中灵活运用。
  • 了解应用程序包结构设计思想,掌握其组织和配置的方法。
  • 了解两种共享包并掌握静态共享包HAR和动态共享包HSP的开发使用。

UIAbility

1. 概述

UIAbility组件是一种包含UI的应用组件,主要用于和用户交互;是应用程序入口和系统调度单元。

一个应用可以包含一个或多个UIAbility组件。

2. UIAbility的生命周期

  • Create(创建)
  • Distroy(销毁)
  • Foreground(前台)
  • Background(后台)

0000000000011111111.20240522160733.92648564926415337545349335781240.png

3. UIAbility页面跳转

跳转到次级页面

  • 第一种方式:router.pushUrl()
router.pushUrl({
  url: 'pages/Next',
  params: {
    src: '',
  }
}, router.RouterMode.Single)
  • 第二种方式:router.replaceUrl()
router.replaceUrl({
  url: 'pages/Second',
  params: {
    src: '',
  }
}, router.RouterMode.Single)

其中params为跳转页面时传递的参数,在次级页面获取上级页面传递过来的参数:

@State src: string = (router.getParams() as Record<string, string>)['src'];

返回页面

  • 返回到上一级页面
Router.back()
  • 返回到指定页面
Router.back( { url: 'pages/Home' } )

4. UIAbility的启动模式

系统提供了三种启动模式:singleton(单实例模式)、multiton(多实例模式)和specified(指定实例模式)。

  • 单实例模式(默认) 每次调用startAbility()方法时,如果应用进程中该类型的UIAbility实例已经存在,则复用系统中的UIAbility实例,否则会创建一个UIAbility实例。系统中只存在唯一一个该UIAbility实例,即在最近任务列表中只存在一个该类型的UIAbility实例。
  • 多实例模式 每次调用startAbility()方法时,都会在应用进程中创建一个新的该类型UIAbility实例。即在最近任务列表中可以看到有多个该类型的UIAbility实例。
  • 指定实例模式 针对一些特殊场景使用,开发者可以为该实例指定一个唯一的字符串Key,这样在调用startAbility()方法时,应用就可以根据指定的Key来识别响应请求的UIAbility实例。
// 在启动指定实例模式的UIAbility时,给每一个UIAbility实例配置一个独立的Key标识
// 例如在文档使用场景中,可以用文档路径作为Key标识
import common from '@ohos.app.ability.common';
import hilog from '@ohos.hilog';
import Want from '@ohos.app.ability.Want';
import { BusinessError } from '@ohos.base';

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%')
  }
}

5. UIAbility的用法和交互

基本用法

i. 指定UIAbility的启动页面

import UIAbility from '@ohos.app.ability.UIAbility';
import window from '@ohos.window';

export default class EntryAbility extends UIAbility {
  onWindowStageCreate(windowStage: window.WindowStage): void {
    // Main window is created, set main page for this ability
    windowStage.loadContent('pages/Index', (err, data) => {
      // ...
    });
  }

  // ...
}

如需设置其他页面为启动页面,将Index替换成其他页面名称即可。

ii. 获取UIAbility的上下文信息

  • 在UIAbility中可以通过this.context获取UIAbility实例的上下文信息。
import UIAbility from '@ohos.app.ability.UIAbility';
import AbilityConstant from '@ohos.app.ability.AbilityConstant';
import Want from '@ohos.app.ability.Want';

export default class EntryAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    // 获取UIAbility实例的上下文
    let context = this.context;
    ...
  }
}
  • 在页面中获取UIAbility实例的上下文信息。
import common from '@ohos.app.ability.common';
import Want from '@ohos.app.ability.Want';

@Entry
@Component
struct Index {
  startAbilityTest() {
    let context = getContext(this) as common.UIAbilityContext;
    let want: Want = {
      // Want参数信息
    };
    context.startAbility(want);
  }

  // 页面展示
  build() {
    ...
  }
}

交互

UIAbility是系统调度的最小单元。

在设备内的功能模块之间跳转时,会涉及到启动特定的UIAbility,可以是应用内的其他UIAbility,也可以是其他应用的UIAbility(例如启动三方支付UIAbility)。

UIAbility的交互有以下几种情况

  • 启动应用内的UIAbility并获取返回结果
  • 其他其他应用的UIAbility并获取返回结果
  • 启动UIAbility的指定页面

UIAbility组件贱交互的载体:Want

want-98867.png

这里我们以第一种为例,了解一下UIAbility的交互细节。

在EntryAbility中,调用startAbilityForResult()接口启动FuncAbility,异步回调中的data用于接收回调信息。

import common from '@ohos.app.ability.common';
import hilog from '@ohos.hilog';
import Want from '@ohos.app.ability.Want';
import { BusinessError } from '@ohos.base';

const TAG: string = '[Page_UIAbilityComponentsInteractive]';
const DOMAIN_NUMBER: number = 0xFF00;

@Entry
@Component
struct Page_UIAbilityComponentsInteractive {
  build() {
    Button()
      .onClick(() => {
        let context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext; // UIAbilityContext

        let want: Want = {
          deviceId: '', // deviceId为空表示本设备
          bundleName: 'com.samples.stagemodelabilitydevelop',
          moduleName: 'entry', // moduleName非必选
          abilityName: 'FuncAbilityA',
          parameters: { // 自定义信息
            info: '来自EntryAbility UIAbilityComponentsInteractive页面'
          }
        };
        context.startAbilityForResult(want).then((data) => {
          // 接收回调
          
        }).catch((err: BusinessError) => {
          hilog.error(DOMAIN_NUMBER, TAG, `Failed to start ability for result. Code is ${err.code}, message is ${err.message}`);
        });
      })
  }
}

在FuncAbility停止自身时,需要调用terminateSelfWithResult()方法,入参abilityResult为FuncAbility需要返回给EntryAbility的信息。

import common from '@ohos.app.ability.common';
import hilog from '@ohos.hilog';

const TAG: string = '[Page_FuncAbilityA]';
const DOMAIN_NUMBER: number = 0xFF00;

@Entry
@Component
struct Page_FuncAbilityA {
  build() {
    Button()
      .onClick(() => {
        let context: common.UIAbilityContext = getContext(this) as common.UIAbilityContext; // UIAbilityContext
        const RESULT_CODE: number = 1001;
        let abilityResult: common.AbilityResult = {
          resultCode: RESULT_CODE,
          want: {
            bundleName: 'com.samples.stagemodelabilitydevelop',
            moduleName: 'entry', // moduleName非必选
            abilityName: 'FuncAbilityB',
            parameters: {
              info: '来自FuncAbility Index页面'
            },
          },
        };
        context.terminateSelfWithResult(abilityResult, (err) => {
          if (err.code) {
            hilog.error(DOMAIN_NUMBER, TAG, `Failed to terminate self with result. Code is ${err.code}, message is ${err.message}`);
            return;
          }
        });
      })
  }
}

在EntryAbility的startAbilityForResult()中处理回调。

const RESULT_CODE: number = 1001;
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) ?? '');

这里的RESULT_CODE必须与被调用方一致。

AbilityStage

1. 概述

AbilityStage是一个Module级别的组件容器,应用的HAP在首次加载时会创建一个AbilityStage实例,可以对该Module进行初始化等操作。

AbilityStage与Module一一对应,即一个Module拥有一个AbilityStage。

2. 使用AbilityStage能力

AbilityStage文件需要手动创建,可以在对应的ets目录下创建一个名为MainAbilityStage的ets文件。

import AbilityStage from '@ohos.app.ability.AbilityStage';
import type Want from '@ohos.app.ability.Want';

export default class MainAbilityStage extends AbilityStage {
  onCreate(): void {
    // 应用的HAP在首次加载的时,为该Module初始化操作
  }
  onAcceptWant(want: Want): string {
    // 仅specified模式下触发
    return 'MainAbilityStage';
  }
}

module.json5配置文件中,通过配置srcEntry参数来指定模块对应的代码路径,以作为HAP加载的入口。

{
  "module": {
    "name": "entry",
    "type": "entry",
    "srcEntry": "./ets/myabilitystage/MyAbilityStage.ets",
    ...
  }
}

3. 生命周期与事件回调

AbilityStage拥有onCreate()生命周期回调和onAcceptWant()onConfigurationUpdated()onMemoryLevel()事件回调。

  • onCreate():在开始加载对应Module的第一个UIAbility实例之前会先创建AbilityStage,并在AbilityStage创建完成之后执行其onCreate()生命周期回调。
  • onAcceptWant():UIAbility指定实例模式(specified)启动时候触发的事件回调。
  • onConfigurationUpdated():当系统全局配置发生变更时触发的事件,系统语言、深浅色等,配置项目前均定义在Configuration类中。
  • onMemoryLevel():当系统调整内存时触发的事件。

ExtensionAbility

ExtensionAbility组件是基于特定场景(例如服务卡片、输入法等)提供的应用组件,以便满足更多的使用场景。 每一个具体场景对应一个ExtensionAbilityType,开发者只能使用(包括实现和访问)系统已定义的类型。各类型的ExtensionAbility组件均由相应的系统服务统一管理。

应用包结构

不同类型的Module编译后会生成对应的HAP、HAR、HSP等文件。

应用包结构类型

  • HAP(Harmony Ability Package)是应用安装和运行的基本单元。HAP包是由代码、资源、第三方库、配置文件等打包生成的模块包,其主要分为两种类型:entry和feature。
  • HAR(Harmony Archive)是静态共享包,可以包含代码、C++库、资源和配置文件。通过HAR可以实现多个模块或多个工程共享ArkUI组件、资源等相关代码。
  • HSP(Harmony Shared Package)是动态共享包,可以包含代码、C++库、资源和配置文件,通过HSP可以实现应用内的代码和资源的共享。HSP不支持独立发布,而是跟随其宿主应用的APP包一起发布,与宿主应用同进程,具有相同的包名和生命周期。

0000000000011111111.20240522160709.54841734550872008395675963813011.png

工程的开发态主要包含AppScope(配置与资源)、entry(应用程序入口)、feature(基础特性模块)和librarys(库)等部分; 编译态会生成.app文件,其中中可能包含多个.hap和.hsp文件,.har会被编译打包到所有该依赖文件的.hap和.hsp包中。

HAP、HAR、HSP三者的功能和使用场景。

  1. HAP 应用的功能模块,可以独立安装和运行. 必须包含一个entry类型的HAP,可选包含一个或多个feature类型的HAP。

  2. HAR 静态共享包,编译态复用。

  • 支持应用内共享,也可以发布后供其他应用使用。
  • 作为二方库,发布到OHPM私仓,供公司内部其他应用使用。
  • 作为三方库,发布到OHPM中心仓,供其他应用使用。
  • 多包(HAP/HSP)引用相同的HAR时,会造成多包间代码和资源的重复拷贝,从而导致应用包膨大。
  1. HSP 动态共享包,运行时复用。
  • 当前仅支持应用内共享。
  • 当多包(HAP/HSP)同时引用同一个共享包时,采用HSP替代HAR,可以避免HAR造成的多包间代码和资源的重复拷贝,从而减小应用包大小。

HAR和HSP不支持单独安装或运行。HAR只能作为应用模块的依赖项被引用;HSP需要与依赖该HSP的HAP一起安装/运行。 HAR和HSP可以依赖其他HAR或HSP,但不支持循环依赖,也不支持依赖传递。

综述

  1. 运行期和编译期概览

在编译期一个App可能包含一个或多个HAP。对应的,在运行期一个Application可能持有一个或多个AbilityStage。

截屏2024-06-03 22.13.53.png

在运行期,每个AbilityStage持有该Module上定义的UIAbility和ExtensionAbility组件,通过持有实例关联,开发者可以通过AbilityStage获取该UIAbility实例的运行时信息。

UIAbility与WindowStage、WIndow和ArkUI Page又分别有持有关系,利用这种持有关系,通过上下文环境(context)可以调用各种对应的资源和能力。

  1. 组件与窗口生命周期分离 AbilityStage、UIAbility和WindowStage三者的的生命周期如下图所示:

截屏2024-06-03 22.13.29.png

我们可以看出应用组件与窗口的生命周期是分离的。

  • 这有利于我们将业务逻辑和UI逻辑进行分离,开发者可以在UIAbility组件中处理与页面无关的业务逻辑,如打开蓝牙、连接数据库等。
  • 便于系统对组件进行裁剪,对于无屏设备,系统在运行应用时不会创建窗口模块,有利于减少系统ROM空间占用。
  • 在多设备上,应用组件可使用同一套生命周期,系统会自动判断设备的不同窗口形态变换,执行不同的生命周期变化流程。

以上,总结了应用程序框架的内容。

拥抱鸿蒙,拥抱未来,选择远方,风雨兼程。

参考