Stage模型UIAbility

时间:2024-01-22 13:08:19

一、UIAbility概述

1.1.概况介绍

UIAbility组件是HarmonyOS中一种包含UI界面的应用组件,主要用于与用户进行交互。每个UIAbility组件实例对应最近任务列表中的一个任务,可以包含多个页面来实现不同功能模块。

  • UIAbility组件是系统调度的基本单元,为应用提供绘制界面的窗口。
  • 一个UIAbility组件中可以通过多个页面来实现一个功能模块。
  • 每一个UIAbility组件实例,都对应于一个最近任务列表中的任务。

通俗解释:
UIAbility组件类似于是你手机上的一个应用,它负责展示应用的用户界面,可以通过它来和应用进行互动。每个UIAbility组件可以包含多个不同的界面,就像应用中的不同页面。每次你在手机上打开一个应用,实际上是启动了一个UIAbility组件的实例,此应用会在最近任务列表中显示。这个机制让你能够轻松切换和管理不同应用和它们的界面。

1.2 声明配置

为使应用能够正常使用UIAbility,需要在module.json5配置文件的abilities标签中声明UIAbility的名称、入口、标签等相关信息。

Stage模型UIAbility_生命周期

在module.json5中可以设置的参数:

{
  "module": {
    // ...
    "abilities": [
      {
        "name": "EntryAbility", // UIAbility组件的名称
        "srcEntrance": "./ets/entryability/EntryAbility.ts", // UIAbility组件的代码路径文件
        "description": "$string:EntryAbility_desc", // UIAbility组件的描述信息
        "icon": "$media:icon", // UIAbility组件的图标 桌面图标
        "label": "$string:EntryAbility_label", // UIAbility组件的标签 桌面应用文件
        "startWindowIcon": "$media:icon", // UIAbility组件启动页面图标资源文件的索引
        "startWindowBackground": "$color:start_window_background", // UIAbility组件启动页面背景颜色资源文件的索引
        // ...
      }
    ]
  }
}

二、UIAbility组件生命周期

当用户浏览、切换和返回到对应的应用的时候,应用中的UIAbility实例会在其生命周期的不同状态之间转换。UIAbility类提供了很多回调,通过这些回调可以知晓当前UIAbility的某个状态已经发生改变:例如UIAbility的创建和销毁,或者UIAbility发生了前后台的状态切换。例如从桌面点击图库应用图标,到启动图库应用,应用的状态经过了从创建到前台展示的状态变化。如下图所示。

Stage模型UIAbility_UI_02

回到桌面,从最近任务列表,切换回到图库应用,应用的状态经过了从后台到前台展示的状态变化。如下图所示。

Stage模型UIAbility_自定义组件_03

UIAbility的使用过程中,会有多种生命周期状态。掌握UIAbility的生命周期,对于应用的开发非常重要。
为了实现多设备形态上的裁剪和多窗口的可扩展性,系统对组件管理和窗口管理进行了解耦。UIAbility的生命周期包括CreateForegroundBackgroundDestroy四个状态,WindowStageCreateWindowStageDestroy为窗口管理器(WindowStage)在UIAbility中管理UI界面功能的两个生命周期回调,从而实现UIAbility与窗口之间的弱耦合。

2.1 基本概念

UIAbility就像是应用中的一个窗口或界面,它有自己的生命周期,就像一个角色有不同的状态。这个生命周期包括四个状态:

  • Create(创建):当用户打开应用或者切换到应用时,UIAbility被创建。就好像你打开一个应用,应用的界面被呈现在屏幕上。
  • Foreground(前台):当应用界面处于活跃状态时,它处于前台状态。这就像你正在使用一个应用,它是当前的焦点。
  • Background(后台):如果你切换到了其他应用,原来的应用就进入了后台状态。UIAbility也会相应地从前台变成后台状态。
  • Destroy(销毁):当你关闭应用或者应用被系统销毁时,UIAbility也会被销毁。就像你关闭一个应用,它的界面消失了。

这些状态变化可以帮助应用了解UIAbility在不同时刻的状态,从而执行不同的操作或者保持应用的一致性。

Stage模型UIAbility_自定义组件_04

2.2.生命周期状态说明

2.2.1.Create状态

Create状态:Create状态为在应用加载过程中,UIAbility实例创建完成时触发,系统会调用onCreate()回调。可以在该回调中进行应用初始化操作,例如变量定义资源加载等,用于后续的UI界面展示。如何在代码中看到他?如图所示,打开EntryAbility.ts的typescript文件

Stage模型UIAbility_UI_05

可以利用创建函数实现资源的初始化,同时也可以使用ts的console.log打印一些提示信息:

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

export default class EntryAbility extends UIAbility {
    onCreate(want, launchParam) {
        // 应用初始化
    }
    // ...
}

2.2.2.WindowStageCreate和WindowStageDestroy状态

当你的应用程序启动时,系统会为每个UIAbility实例创建一个WindowStage(窗口舞台)。这个WindowStage就像是应用界面的舞台,上面可以加载各种UI元素。

在WindowStage创建完成后,系统会调用onWindowStageCreate()这个回调函数。在这个函数里,你可以设置你的UI界面,比如加载各种页面,设置UI元素的样式等。你还可以订阅WindowStage的事件,比如获得焦点(用户正在与应用交互)或失去焦点(应用不在是用户的焦点)、可见或不可见等。这些事件让你的应用能够响应用户的操作。

在onWindowStageCreate()回调函数中,通常会使用loadContent()方法来加载应用要显示的页面。同时,你也可以根据需要订阅各种WindowStage事件,以便在用户操作发生时做出相应的响应。
当用户退出应用或者应用被系统销毁时,对应的UIAbility实例会被销毁。在这之前,系统会先调用onWindowStageDestroy()回调函数。在这个函数里,你可以释放UI界面所占用的资源,做一些清理工作。比如,你可以在这里注销之前订阅的获得焦点或失去焦点等WindowStage事件,以确保资源被正确释放,应用退出时不会出现问题。

这样,通过在WindowStage的创建和销毁过程中设置相应的回调函数,你的应用就能够在不同的阶段执行不同的操作,从而保证用户体验的一致性和应用的稳定

Stage模型UIAbility_自定义组件_06

代码如下:

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

export default class EntryAbility extends UIAbility {
    onWindowStageCreate(windowStage: Window.WindowStage) {
        // 设置WindowStage的事件订阅(获焦/失焦、可见/不可见)

        // 设置UI界面加载
        windowStage.loadContent('pages/Index', (err, data) => {
            // ...
        });
    }
    // ...
}

// 对应onWindowStageCreate回调,在UIAbility实例销毁之前,会先进入onWindowStageDestroy回调,可以在该回调中释放UI界面资源。
export default class EntryAbility extends UIAbility {
    // ...

    onWindowStageDestroy() {
        // 释放UI界面资源
    }
}

2.2.3.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 '@ohos.app.ability.UIAbility';

export default class EntryAbility extends UIAbility {
    onForeground() {
        // 申请系统需要的资源,或者重新申请在onBackground中释放的资源
    }

    onBackground() {
        // 释放UI界面不可见时无用的资源,或者在此回调中执行较为耗时的操作,例如状态保存等
    }
}

上面的函数在EntryAbility.ts中如下:

Stage模型UIAbility_生命周期_07

注意:如果想看到这两个函数的效果,需要使用本地虚拟机/真机,预览模式是无法查看这两个函数执行的。

2.2.4.Destroy状态

Destroy状态在UIAbility实例销毁时触发。可以在onDestroy()回调中进行系统资源的释放、数据的保存等操作。

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

export default class EntryAbility extends UIAbility {
    onDestroy() {
        // 系统资源的释放、数据的保存等
    }
}

例如:调用terminateSelf()方法停止当前UIAbility实例,从而完成UIAbility实例的销毁;或者用户使用最近任务列表关闭该UIAbility实例,完成UIAbility的销毁。

2.3.UIAbility组件生命周期演示

这里利用之前的小鱼游戏页面进行演示,需要注意,在观察的时候发现Log中输出的日志比较多,这时候利用之前EntryAbility.ts中记录的日志testTag,进行过滤:

Stage模型UIAbility_自定义组件_08

在Log中设置过滤,如下:

Stage模型UIAbility_生命周期_09

为了看的清楚,点击删除现有的日志记录,重启一下项目在模拟器中打开,然后点击切换退出,观察日志记录,操作如下:

Stage模型UIAbility_自定义组件_10

查看日志情况,发现出现了之前说到的几个状态变化

Stage模型UIAbility_UI_11

三、页面及组件生命周期

在之前的章节中已经学习了自定义组件,知道了如何自定义组件以及自定义组件的相关注意事项,接下来我们认识一下页面和自定义组件生命周期。

3.1.自定义组件和页面的关系

在开始之前,首先明确自定义组件和页面的关系

  • 自定义组件:@Component装饰的UI单元,可以组合多个系统组件实现UI的复用。
  • 页面:即应用的UI页面。可以由一个或者多个自定义组件组成,@Entry装饰的自定义组件为页面的入口组件,即页面的根节点,一个页面有且仅能有一个@Entry。只有被@Entry装饰的组件才可以调用页面的生命周期。

3.2.生命周期

下面依次介绍页面和组件的生命周期函数。

3.2.1.页面生命周期

页面生命周期,即被@Entry装饰的组件生命周期,提供以下生命周期接口:

  • onPageShow:页面每次显示时触发。
onPageShow?(): void

说明:页面每次显示时触发一次,包括路由过程、应用进入前台等场景,仅@Entry装饰的自定义组件生效。

  • onPageHide:页面每次隐藏时触发一次。
onPageHide?(): void

说明:页面每次隐藏时触发一次,包括路由过程、应用进入前后台等场景,仅@Entry装饰的自定义组件生效。

  • onBackPress:当用户点击返回按钮时触发。
onBackPress?(): void

说明:当用户点击返回按钮时触发,仅@Entry装饰的自定义组件生效。

onPageShow,onPageHide,onBackPress生命周期案例演示如下:

1)利用之前的小鱼游戏,在Index.ets,页面添加上面的三个方法,代码如下:

import router from '@ohos.router'
@Entry
@Component
struct Index {

  onPageShow(){
    console.log("onPageShow 页面每次显示时触发一次,包括路由过程、应用进入前台等场景,仅@Entry装饰的自定义组件生效。")
  }

  onPageHide() {
    console.info('onPageHide 页面每次隐藏时触发一次,包括路由过程、应用进入前后台等场景,仅@Entry装饰的自定义组件生效。');
  }

  onBackPress() {
    console.info('onBackPress 当用户点击返回按钮时触发,仅@Entry装饰的自定义组件生效。');
  }

  build() {


    Column(){
      Stack(){
        Image($r("app.media.home"))

        Button(){
          Image($r("app.media.button1")).width(260).width(150)
        }.onClick(()=>{
          router.pushUrl({url:"pages/AnimationPage02"},
            router.RouterMode.Single,
            err => {   // 异常响应回调函数
              if(err) {
                console.log(`路由失败, errCode: ${err.code}, errMsg: ${err.message}`)
              }
            })
        })

        Button(){
          Image($r("app.media.button2")).width(260).width(150)
        }.margin({top:150})

        Button(){
          Image($r("app.media.button3")).width(260).width(150)
        }.margin({top:300})

      }
    }
    .width("100%")
    .height("100%")
  }
}

2)游戏页面还是跟之前一样,在 AnimationPage02.ets中,代码如下:

import router from '@ohos.router'
@Entry
@Component
struct Index {

  onPageShow(){
    console.log("onPageShow 页面每次显示时触发一次,包括路由过程、应用进入前台等场景,仅@Entry装饰的自定义组件生效。")
  }

  onPageHide() {
    console.info('onPageHide 页面每次隐藏时触发一次,包括路由过程、应用进入前后台等场景,仅@Entry装饰的自定义组件生效。');
  }

  onBackPress() {
    console.info('onBackPress 当用户点击返回按钮时触发,仅@Entry装饰的自定义组件生效。');
  }

  build() {


    Column(){
      Stack(){
        Image($r("app.media.home"))

        Button(){
          Image($r("app.media.button1")).width(260).width(150)
        }.onClick(()=>{
          router.pushUrl({url:"pages/AnimationPage02"},
            router.RouterMode.Single,
            err => {   // 异常响应回调函数
              if(err) {
                console.log(`路由失败, errCode: ${err.code}, errMsg: ${err.message}`)
              }
            })
        })

        Button(){
          Image($r("app.media.button2")).width(260).width(150)
        }.margin({top:150})

        Button(){
          Image($r("app.media.button3")).width(260).width(150)
        }.margin({top:300})

      }
    }
    .width("100%")
    .height("100%")
  }
}

3)预览代码,随着页面的隐藏和显示,和进入新页面后在返回第一个页面触发,如下:

Stage模型UIAbility_自定义组件_12

执行三个方法输出如下:

Stage模型UIAbility_生命周期_13

3.2.2.组件生命周期

组件生命周期,即一般用@Component装饰的自定义组件的生命周期,提供以下生命周期接口:

  • aboutToAppear:组件即将出现时回调该接口,具体时机为在创建自定义组件的新实例后,在执行其build()函数之前执行。
aboutToAppear?(): void

aboutToAppear函数在创建自定义组件的新实例后,在执行其build()函数之前执行。允许在aboutToAppear函数中改变状态变量,更改将在后续执行build()函数中生效。

  • aboutToDisappear:在自定义组件即将析构销毁时执行。
aboutToDisappear?(): void

aboutToDisappear函数在自定义组件析构销毁之前执行。不允许在aboutToDisappear函数中改变状态变量,特别是@Link变量的修改可能会导致应用程序行为不稳定。

下面展示的是被@Entry装饰的组件生命周期。

Stage模型UIAbility_UI_14

3.3.自定义组件的创建和渲染流程

1)自定义组件的创建:自定义组件的实例由ArkUI框架创建。

2)初始化自定义组件的成员变量:通过本地默认值或者构造方法传递参数来初始化自定义组件的成员变量,初始化顺序为成员变量的定义顺序。

3)如果开发者定义了aboutToAppear,则执行aboutToAppear方法。

4)在首次渲染的时候,执行build方法渲染系统组件,如果子组件为自定义组件,则创建自定义组件的实例。在执行build()函数的过程中,框架会观察每个状态变量的读取状态,将保存两个map:

  • 状态变量 -> UI组件(包括ForEach和if)。
  • UI组件 -> 此组件的更新函数,即一个lambda方法,作为build()函数的子集,创建对应的UI组件并执行其属性方法,示意如下。
build() {
  ...
  this.observeComponentCreation(() => {
    Button.create();
  })

  this.observeComponentCreation(() => {
    Text.create();
  })
  ...
}

当应用在后台启动时,此时应用进程并没有销毁,所以仅需要执行onPageShow。

3.4.自定义组件重新渲染

当事件句柄被触发(比如设置了点击事件,即触发点击事件)改变了状态变量时,或者LocalStorage / AppStorage中的属性更改,并导致绑定的状态变量更改其值时:

  • ArkUI框架检测状态变量到了变化,将启动重新渲染。
  • 根据框架持有的两个map(自定义组件的创建和渲染流程中第4步),框架可以知道该状态变量管理了哪些UI组件,以及这些UI组件对应的更新函数。执行这些UI组件的更新函数,实现最小化更新。

3.5.自定义组件的删除

如果if组件的分支改变,或者ForEach循环渲染中数组的元素个数改变,组件将被删除:

  • 在删除组件之前,将调用其aboutToDisappear生命周期函数,标记着该节点将要被销毁。ArkUI的节点删除机制是:后端节点直接从组件树上摘下,后端节点被销毁,对前端节点解引用,当前端节点已经没有引用时,将被JS虚拟机垃圾回收。
  • 自定义组件和它的变量将被删除,如果其有同步的变量,比如@Link、@Prop、@StorageLink,将从同步源上取消注册。

不建议在生命周期aboutToDisappear内使用async await,如果在生命周期的aboutToDisappear使用异步操作(Promise或者回调方法),自定义组件将被保留在Promise的闭包中,直到回调方法被执行完,这个行为阻止了自定义组件的垃圾回收。

下面案例展示了组件和页面生命周期的调用时机如下,MyComponent.ets内容如下:

import router from '@ohos.router';
@Entry
@Component
struct MyComponent {
  @State showChild: boolean = true;

  // 只有被@Entry装饰的组件才可以调用页面的生命周期
  onPageShow(){
    console.log("onPageShow 页面每次显示时触发一次,包括路由过程、应用进入前台等场景,仅@Entry装饰的自定义组件生效。")
  }

  // 只有被@Entry装饰的组件才可以调用页面的生命周期
  onPageHide() {
    console.info('onPageHide 页面每次隐藏时触发一次,包括路由过程、应用进入前后台等场景,仅@Entry装饰的自定义组件生效。');
  }

  // 只有被@Entry装饰的组件才可以调用页面的生命周期
  onBackPress() {
    console.info('onBackPress 当用户点击返回按钮时触发,仅@Entry装饰的自定义组件生效。');
  }

  // 组件生命周期
  aboutToAppear() {
    console.info('MyComponent aboutToAppear');
  }

  // 组件生命周期
  aboutToDisappear() {
    console.info('MyComponent aboutToDisappear');
  }

  build() {
    Column({space:20}) {
      // this.showChild为true,创建Child子组件,执行Child aboutToAppear
      if (this.showChild) {
        Child()
      }
      // this.showChild为false,删除Child子组件,执行Child aboutToDisappear
      Button('create or delete Child').onClick(() => {
        this.showChild = !this.showChild;
      })
      // push到Page2页面,执行onPageHide
      Button('push to next page')
        .onClick(() => {
          router.pushUrl({ url: 'pages/AnimationPage02' });
        })
    }
    .width("100%")
    .height("100%")
    .padding(20)
    .justifyContent(FlexAlign.Center)
    .alignItems(HorizontalAlign.Center)

  }
}

@Component
struct Child {
  @State title: string = 'Hello World';
  // 组件生命周期
  aboutToDisappear() {
    console.info('Child aboutToDisappear')
  }
  // 组件生命周期
  aboutToAppear() {
    console.info('Child aboutToAppear')
  }

  build() {
    Text(this.title).fontSize(50).onClick(() => {
      this.title = 'Hi ArkUI';
    })
  }
}

注意:跳转的页面是AnimationPage02.ets就是利用的小鱼游戏哪里的,就不在单独列出

上面的案例中,MyComponent页面包含两个自定义组件,一个是被@Entry装饰的MyComponent,也是页面的入口组件,即页面的根节点;一个是Child,是MyComponent的子组件。只有@Entry装饰的节点才可以生效页面的生命周期方法,所以MyComponent中声明了当前页面的页面生命周期函数。MyComponent和其子组件Child也同时也声明了组件的生命周期函数。

  1. 应用冷启动的初始化流程为:MyComponent aboutToAppear --> MyComponent build --> Child aboutToAppear --> Child build --> Child build执行完毕 --> MyComponent build执行完毕 --> onPageShow。
  2. 点击“delete Child”,if绑定的this.showChild变成false,删除Child组件,会执行Child aboutToDisappear方法。
  3. 点击“push to next page”,调用router.pushUrl接口,跳转到另外一个页面,当前MyComponent页面隐藏,执行页面生命周期方法onPageHide。此处调用的是router.pushUrl接口,MyComponent页面被隐藏,并没有销毁,所以只调用onPageHide。跳转到新页面后,执行初始化新页面的生命周期的流程。
  4. 如果调用的是router.replaceUrl,则当前MyComponent页面被销毁,执行的生命周期流程将变为:onPageHide --> MyComponent aboutToDisappear --> Child aboutToDisappear。上文已经提到,组件的销毁是从组件树上直接摘下子树,所以先调用父组件的aboutToDisappear,再调用子组件的aboutToDisappear,然后执行初始化新页面的生命周期流程。
  5. 点击返回按钮,触发页面生命周期方法onBackPress执行,且触发返回一个页面后会导致当前页面被销毁。
  6. 最小化应用或者应用进入后台,触发onPageHide。当前MyComponent 页面没有被销毁,所以并不会执行组件的aboutToDisappear。应用回到前台,执行onPageShow。
  7. 退出应用,执行onPageHide --> MyComponent aboutToDisappear --> Child aboutToDisappear。

四、UIAbility的启动模式

UIAbility的启动模式是指UIAbility实例在启动时的不同呈现状态。针对不同的业务场景,系统提供了三种启动模式:

  • singleton:单实例模式
  • standard:标准实例模式
  • specified:指定实例模式

4.1 singleton启动模式

"Singleton启动模式"是一种应用程序的启动方式,通常是默认的启动方式。在这个模式下,每当你使用startAbility()方法来启动一个UIAbility(用户界面能力)时,如果已经存在相同类型的UIAbility实例,系统会复用现有的实例,而不会创建新的。这意味着在最近任务列表中,只会存在一个该类型的UIAbility实例。

简答来说,就好像你有一个应用,当你多次打开同一功能页面时,不会为每次打开都创建新的页面,而是会重新使用已经存在的页面。这样可以节省系统资源,提高应用的运行效率,而且在最近任务列表中只会看到一个相同类型的页面。这个模式通常用于确保应用不会出现大量相同的界面实例,以提供更好的用户体验和系统性能。

4.1.1.说明

应用的UIAbility实例已创建,该UIAbility配置为单实例模式,再次调用startAbility()方法启动该UIAbility实例,此时只会进入该UIAbility的onNewWant()回调,不会进入其onCreate()和onWindowStageCreate()生命周期回调。

4.1.2.进行配置singleton模式

如果需要使用singleton启动模式,在module.json5配置文件中的launchType字段配置为singleton即可。如下所示:

Stage模型UIAbility_UI_15

4.1.3.singleton模式演示

利用之前的小鱼游戏,进行演示,在模拟器中演示,然后在Log中根据TestTag进行日志过滤查询

Stage模型UIAbility_自定义组件_16

会发现上面的singleton模式在切换到桌面,在重新打开的时候,还是使用的第一次启动软件是创建的UIAbility实例,系统中只能有一个实例,如下:

Stage模型UIAbility_UI_17

4.2.multiton启动模式

multiton启动模式每次调用startAbility()方法时,都会在应用进程中创建一个新的该类型UIAbility实例,但是旧的实例会被清除。即在最近任务列表中只能看到一个该类型的UIAbility实例。

4.2.1.multiton模式设置

multiton启动模式的开发使用,在module.json5配置文件中的"launchType"字段配置为"multiton"即可。

4.2.2.multiton模式案例演示

利用之前的小鱼游戏,进行演示,在模拟器中演示,然后在Log中根据TestTag进行日志过滤查询:

Stage模型UIAbility_UI_18

查看日志,会发现UIAbility实例被创建了多次,但是只保留了一个,之前的实例已经被清除了,日志如下:

Stage模型UIAbility_生命周期_19

只剩下一个实例,如下:

Stage模型UIAbility_生命周期_20

4.3.standard启动模式

standard启动模式为标准实例模式,每次调用startAbility()方法时,都会在应用进程中创建一个新的该类型UIAbility实例。即在最近任务列表中可以看到有多个该类型的UIAbility实例。这种情况下可以将UIAbility配置为standard(标准实例模式)。

4.3.1.standard模式设置

standard启动模式的开发使用,在module.json5配置文件中的"launchType"字段配置为"standard"即可。

4.3.2.standard模式设置演示

准备之前的小鱼游戏,在模拟器中演示,然后在Log中根据TestTag进行日志过滤查询:

Stage模型UIAbility_生命周期_21

查看日志发现每次切换一次,就会创建一个新的UIAbility实例,并且之前的实例依然存在,如下:

Stage模型UIAbility_生命周期_22

查看创建的三个UIAbility实例都存在,并没有清除,如下:

Stage模型UIAbility_生命周期_23

4.4.specified启动模式

"Specified启动模式"是一种特殊的启动模式,通常用于特定场景,比如文档编辑应用。允许开发者在创建UIAbility实例之前为该实例指定一个唯一的标识字符串(称为Key)。当UIAbility实例与这个Key绑定之后,在后续调用startAbility()方法时,系统会询问应用要打开哪个与特定Key绑定的UIAbility实例。

这种方式可以理解为每个UIAbility实例都有一个特定的身份标识(Key)。每次开发者启动UIAbility时,系统会根据这个Key来判断是复用已存在的特定Key绑定的UIAbility实例,还是创建一个新的实例。例如,在文档编辑应用中,如果你想新建一个文档,可以使用一个新的Key,这将创建一个新的UIAbility实例;但如果你想打开一个已保存的文档,可以使用该文档对应的Key,这将打开与之绑定的已存在的UIAbility实例。

所以,这种模式允许应用在运行时根据特定的Key来决定是否创建新实例或复用已有实例,以便满足不同的业务需求,比如确保每个文档有自己独立的编辑界面或者确保打开相同文档时共享同一个界面。

4.4.1.说明

应用的UIAbility实例已创建,该UIAbility配置为指定实例模式,再次调用startAbility()方法启动该UIAbility实例,且AbilityStage的onAcceptWant()回调匹配到一个已创建的UIAbility实例。此时,再次启动该UIAbility时,只会进入该UIAbility的onNewWant()回调,不会进入其onCreate()和onWindowStageCreate()生命周期回调

4.4.2.specified启动模式设置

specified启动模式设置主要经过下面几个步骤

1)当前UIAbility调用startAbility方法拉起目标UIAbility

// 1.1 获取上下文
context = getContext(this) as common.UIAbilityContext

// 1.2 指定要跳转到的UIAbility的信息
let want = {
  deviceId: '', // 为空表示本设备
  bundleName: 'com.example.myapplication',
  abilityName: 'DocumentAbility',
  moduleName: 'entry', // 非必选
  parameters: {
    // getInstanceKey: 自定义方法,生成目标UIAbility实例的key
    instanceKey: this.getInstanceKey()
  }
}

// 1.3 尝试拉起目标UIAbility实例
this.context.startAbility(want)

2).在AbilityStage的生命周期回调中为目标UIAbility实例生成key

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

//创建AbilityStage舞台
export default class MyAbilityStage extends AbilityStage{
  onAcceptWant(want:Want):string{
    //判断当前要拉取的是否是DocumentAbility 如果不是则直接返回空
    if(want.abilityName === "DocumentAbility"){
      //这里返回的是生成的UIAbility实例的key, 这里可以在给拼接一个前缀DocumentAbilityKey_,当然也可以不拼接
      return `DocumentAbilityKey_${want.parameters.instanceKey}`
    }
    return ""
  }
}

3).在module.json5配置文件中,通过srcEntry参数指定AbilityStage路径

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

4.4.3.specified启动模式案例

1).创建主页,Index.ets页面内容如下:

import router from '@ohos.router'
@Entry
@Component
struct Index {

  build() {
    Column(){
      Button(){
        Text("文档列表").fontSize(25).fontColor(Color.White).width(260).height(50).textAlign(TextAlign.Center)
      }.onClick(()=>{
        router.pushUrl({url:"pages/DocumentList"},
          router.RouterMode.Single,
          err => {   // 异常响应回调函数
            if(err) {
              console.log(`路由失败, errCode: ${err.code}, errMsg: ${err.message}`)
            }
          })
      })
    }
    .width("100%")
    .height("100%")
    .alignItems(HorizontalAlign.Center)
    .justifyContent(FlexAlign.Center)
  }
}

2)创建文档对应的UIAbility,创建DocumentAbility.ts文件

这里选择Ability,会自动创建报名

Stage模型UIAbility_UI_24

文件内容如下:

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

export default class DocumentAbility extends UIAbility {
  onCreate(want, launchParam) {
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate');
  }

  onDestroy() {
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onDestroy');
  }

  onWindowStageCreate(windowStage: window.WindowStage) {
    // Main window is created, set main page for this ability
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageCreate');

    // TODO 这里需要修改成UIAbility对应的页面
    windowStage.loadContent('pages/Document', (err, data) => {
      if (err.code) {
        hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');
        return;
      }
      hilog.info(0x0000, 'testTag', 'Succeeded in loading the content. Data: %{public}s', JSON.stringify(data) ?? '');
    });
  }

  onWindowStageDestroy() {
    // Main window is destroyed, release UI related resources
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onWindowStageDestroy');
  }

  onForeground() {
    // Ability has brought to foreground
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onForeground');
  }

  onBackground() {
    // Ability has back to background
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onBackground');
  }
}

3)设置module.json5中创建的DocumentAbility的UIAbility模式

注意下的DocumentAbility是在创建完成后,自己添加的,这里只需要设置模式为specified 

Stage模型UIAbility_自定义组件_25

4)创建文件列表页面,DocumentList.ets内容如下

import router from '@ohos.router'
import common from '@ohos.app.ability.common'
import Want from '@ohos.app.ability.Want'

@Entry
@Component
struct DocumentList {
  private index:number = 1
  @State docs:number[] = []

  //1.获取context   as common.UIAbilityContext这是类型的强制转换
  private context = getContext(this) as common.UIAbilityContext


  build() {
    Column(){
      Header()
      Divider()
      Button(){
        Text("新建文档").fontSize(25).fontColor(Color.White)
      }.width(150).height(40)
        .margin({top:20,bottom:20})
        .onClick(()=>{
          //点击添加文档
          this.docs.push(this.index)
          //跳转到文档编辑的UIAbility
          //准备跳转的目的地want
          let want:Want = {
            deviceId:"",//空表示当前设置
            bundleName: "com.augus.helloworld", //当前应用的,在全局的app.json5中
            moduleName: "entry",//跳转到那个模块。写当前模块的名字,在module.json5中
            abilityName: "DocumentAbility", //跳转的ability的名字,创建ability后在module.json5中即可查看
            parameters: {//指定对应的key
              instanceKey:"idx_" + this.index++ //为每个UIAbility实例都有一个特定的身份标识(Key).每次完成后加1作为下一次实例的key
            }
          }
          //跳转
          this.context.startAbility(want)
        })
      ForEach(this.docs, (n,idx:number)=>{
        Row({space:10}){
          Image($r("app.media.icon_word")).width(30).height(30)
          Text(`文档${idx+1}`)
            .fontSize(20)
            .onClick(()=>{
              //跳转文档编辑的UIAbility

              //准备跳转的目的地want
              let want:Want = {
                deviceId:"",//空表示当前设置
                bundleName: "com.augus.helloworld", //当前应用的,在全局的app.json5中
                moduleName: "entry",//跳转到那个模块。写当前模块的名字,在module.json5中
                abilityName: "DocumentAbility", //跳转的ability的名字,创建ability后在module.json5中即可查看
                parameters: {//指定对应的key
                  instanceKey:"idx_" + idx+1 //利用本次索引加1,为每个UIAbility实例都有一个特定的身份标识(Key)
                }
              }
              //跳转
              this.context.startAbility(want)
            })
        }
        .width("100%")
        .height(20)
        .justifyContent(FlexAlign.Start)
        .padding({left:20,right:20})
        .margin({top:10, right:10})
      })
    }.width("100%").height("100%").backgroundColor(Color.White)
  }
}


@Component
struct Header {
  build() {
    Row(){
      Row({space:20}){
        Image($r("app.media.icon_back_arrow"))
          .width(30)
          .height(30)
          .onClick(()=>{
            //返回
            router.back()
          })
        Text("文档列表").fontSize(35).fontWeight(FontWeight.Bold)
      }.margin({left:20})

      Row(){
        Image($r("app.media.icon_refresh"))
          .width(30)
          .height(30)
      }.margin({right:20})

    }.width("100%").height(80).justifyContent(FlexAlign.SpaceBetween).alignItems(VerticalAlign.Center)
  }
}

5)创建文档编辑页面 Document.ets内容如下

import Want from '@ohos.app.ability.Want'
import common from '@ohos.app.ability.common'

@Entry
@Component
struct Document {
  //保存文档内容
  @State title:string = "标题"
  @State value:string = ""

  //控制title状态
  @State editTitle:boolean = true

  //1.获取context   as common.UIAbilityContext这是类型的强制转换
  private context = getContext(this) as common.UIAbilityContext


  build() {
    Column(){
      Row({space:10}){
        Image($r("app.media.icon_back_button"))
          .width(30)
          .height(30)
          .margin({left:20})
          .onClick(()=>{
            //跳转到文档列表的UIAbility
            //准备跳转的目的地want
            let want:Want = {
              deviceId:"",//空表示当前设置
              bundleName: "com.augus.helloworld", //当前应用的,在全局的app.json5中
              moduleName: "entry",//跳转到那个模块。写当前模块的名字,在module.json5中
              abilityName: "EntryAbility", //跳转的ability的名字,这里是点击后又跳转回去
              //这里是跳转回去。不需要指定参数
            }
            //跳转
            this.context.startAbility(want)
          })

        if(!this.editTitle){
          Text(this.title)
            .fontSize(30)
            .fontWeight(FontWeight.Bold)
        }else {
          TextInput({
            placeholder: this.title
          })
            .width(200)
            .fontSize(30)
            .onChange((value)=>{
              this.title = value
            })
        }

        //准备确定按钮
        Button(){
          Text("确定").fontColor(Color.White).fontWeight(FontWeight.Bold)
        }.size({width:80, height:30})
        .margin({right:20})
        .onClick(()=>{
          //获取到的信息
          console.log(`${this.title},${this.value}`);
        })

      }
      .margin({top:20})
      .width("100%")
      .height(50)
      .justifyContent(FlexAlign.SpaceBetween)
      .alignItems(VerticalAlign.Center)

      Divider()

      Row(){
        TextArea({placeholder:this.value})  // 创建一个文本域,设置占位符为"请输入反馈意见内容"
          .width("95%")  // 设置文本域宽度为100%
          .height("90%")  // 设置文本域高度为500
          .fontColor(Color.Green)  // 设置文本颜色为绿色
          .fontStyle(FontStyle.Italic)  // 设置字体样式为斜体
          .caretColor(Color.Red)  // 设置光标颜色为红色
          .placeholderColor(Color.Black)  // 设置占位符文本颜色为黑色
          .placeholderFont({  // 设置占位符文本字体样式
            size: 18,  // 字体大小为18
            style: FontStyle.Italic,  // 字体样式为斜体
            weight: FontWeight.Bold  // 字体粗细为粗体
          })
          .onChange((value) => {  // 监听文本域内容变化事件
            this.value = value;  // 将文本域的值赋给状态变量 value
          })
      }

    }.width("100%").height("100%")
  }
}

6)创建包myabilitystage然后创建MyAbilityStage.ets

在AbilityStage的生命周期回调中为目标UIAbility实例生成key内容如下:

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

//创建AbilityStage舞台
export default class MyAbilityStage extends AbilityStage{
  onAcceptWant(want:Want):string{
    //判断当前要拉取的是否是DocumentAbility 如果不是则直接返回空
    if(want.abilityName === "DocumentAbility"){
      //这里返回的是生成的UIAbility实例的key, 这里可以在给拼接一个前缀DocumentAbilityKey_,当然也可以不拼接
      return `DocumentAbilityKey_${want.parameters.instanceKey}`
    }
    return ""
  }
}

7)在module.json5配置文件中,通过srcEntry参数指定AbilityStage路径

 

Stage模型UIAbility_自定义组件_26

8)在模拟器中预览

会发现每次新创建一个文件,会生成一个DocumentAbility的实例,对应创建的一个文件,每次创建完成

Stage模型UIAbility_自定义组件_27

查看创建几个文档,就会有几个UIAbility实例都存在,并没有清除,如下:

Stage模型UIAbility_自定义组件_28