前言

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

上一篇我们搞定了FlutterPlugin接口,插件已经能被Flutter引擎加载了。但如果你现在就去调用activate方法申请麦克风权限,会发现一个尴尬的问题——没有UIAbilityContext

在OpenHarmony中,很多系统级操作(权限申请、弹窗、页面跳转等)都需要UIAbilityContext。这个上下文不是随便就能拿到的,必须通过AbilityAware接口从Flutter引擎获取。

这个接口是OpenHarmony适配中非常关键的一环。Android开发者可能会觉得奇怪——在Android里,Activity的Context随处可得,为什么到了OpenHarmony就这么麻烦?其实不是麻烦,而是OpenHarmony的Ability模型和Android的Activity模型有本质区别。今天我就把这个区别讲清楚。

💡 核心知识点:AbilityAware接口的两个方法——onAttachedToAbilityonDetachedFromAbility,以及如何通过AbilityPluginBinding获取UIAbilityContext。

一、OpenHarmony Ability 模型概述

1.1 什么是Ability

在OpenHarmony中,Ability是应用的基本组成单元,类似Android的Activity但又不完全一样:

概念 Android OpenHarmony 说明
页面组件 Activity UIAbility 带UI的组件
后台组件 Service ServiceExtensionAbility 后台服务
上下文 Context UIAbilityContext 运行时环境
生命周期 onCreate/onResume… onCreate/onForeground… 状态管理

1.2 UIAbility vs Activity

Android Activity生命周期:
onCreate → onStart → onResume → onPause → onStop → onDestroy

OpenHarmony UIAbility生命周期:
onCreate → onWindowStageCreate → onForeground → onBackground → onWindowStageDestroy → onDestroy

最大的区别是OpenHarmony多了一个WindowStage的概念。UIAbility负责管理生命周期,WindowStage负责管理窗口和UI。这种分离设计让OpenHarmony在多窗口场景下更灵活。

1.3 为什么Flutter插件需要Ability上下文

flutter_speech需要UIAbilityContext来做两件事:

  1. 申请麦克风权限abilityAccessCtrl.createAtManager().requestPermissionsFromUser()需要Context参数
  2. 能力检测:某些系统能力检测需要在Ability上下文中进行
// 这两个操作都需要UIAbilityContext
const atManager = abilityAccessCtrl.createAtManager();
const grantResult = await atManager.requestPermissionsFromUser(
  this.abilityContext,  // ← 需要UIAbilityContext
  ['ohos.permission.MICROPHONE']
);

如果没有UIAbilityContext,权限申请就无法进行,语音识别引擎也就无法激活。

二、AbilityAware 接口的作用与实现

2.1 接口定义

interface AbilityAware {
  onAttachedToAbility(binding: AbilityPluginBinding): void;
  onDetachedFromAbility(): void;
}

就两个方法,非常简洁:

  • onAttachedToAbility:Ability准备好时调用,可以获取上下文
  • onDetachedFromAbility:Ability销毁前调用,需要释放上下文引用

2.2 导入AbilityPluginBinding

AbilityPluginBinding的导入路径比较长,这是flutter_speech源码中的写法:

import { AbilityPluginBinding } from
  '@ohos/flutter_ohos/src/main/ets/embedding/engine/plugins/ability/AbilityPluginBinding';

🤦 吐槽一下:这个导入路径也太长了。希望Flutter-OHOS团队以后能把它加到主包的导出中,这样直接从@ohos/flutter_ohos导入就行了。

2.3 flutter_speech的实现

export default class FlutterSpeechPlugin
    implements FlutterPlugin, MethodCallHandler, AbilityAware {

  private abilityContext: common.UIAbilityContext | null = null;

  onAttachedToAbility(binding: AbilityPluginBinding): void {
    this.abilityContext = binding.getAbility().context;
  }

  onDetachedFromAbility(): void {
    this.abilityContext = null;
  }
}

实现非常简单:

  1. onAttachedToAbility:通过binding.getAbility().context获取UIAbilityContext并保存
  2. onDetachedFromAbility:将保存的引用置为null

2.4 为什么要置null

有人可能会问:onDetachedFromAbility里为什么要把abilityContext置为null?直接不管不行吗?

不行。原因有两个:

  1. 防止内存泄漏:UIAbilityContext持有大量系统资源,如果插件一直引用它,GC就无法回收
  2. 防止野指针:Ability销毁后,原来的Context已经失效了。如果插件还拿着旧的Context去调用系统API,可能会崩溃
// ❌ 危险:使用已失效的Context
onDetachedFromAbility(): void {
  // 不置null,abilityContext还指向已销毁的Ability
}

// 后续调用activate时:
if (this.abilityContext) {
  // abilityContext不为null,但实际上已经失效了
  // 调用requestPermissionsFromUser可能会崩溃!
  atManager.requestPermissionsFromUser(this.abilityContext, ...);
}
// ✅ 安全:置null后会走错误分支
onDetachedFromAbility(): void {
  this.abilityContext = null;
}

// 后续调用activate时:
if (this.abilityContext) {
  // 不会进入这个分支
} else {
  console.error(TAG, 'abilityContext is null');
  result.error('SPEECH_CONTEXT_ERROR', 'UIAbilityContext not available', null);
}

三、AbilityPluginBinding 获取 UIAbilityContext

3.1 AbilityPluginBinding 的结构

AbilityPluginBinding提供了访问Ability相关信息的方法:

interface AbilityPluginBinding {
  getAbility(): UIAbility;  // 获取UIAbility实例
  // ... 其他方法
}

3.2 获取Context的调用链

// 完整的调用链
binding                    // AbilityPluginBinding
  .getAbility()           // 返回UIAbility实例
  .context                // UIAbility的context属性,类型是UIAbilityContext
AbilityPluginBinding
    │
    └── getAbility(): UIAbility
         │
         └── .context: UIAbilityContext
              │
              ├── 权限申请
              ├── 弹窗显示
              ├── 页面跳转
              └── 其他系统操作

3.3 UIAbilityContext 能做什么

UIAbilityContext是OpenHarmony中最重要的上下文对象之一:

能力 方法/属性 flutter_speech是否用到
权限申请 配合abilityAccessCtrl使用 ✅ 是
启动Ability startAbility() ❌ 否
获取文件路径 filesDir, cacheDir ❌ 否
获取资源管理器 resourceManager ❌ 否
终止Ability terminateSelf() ❌ 否

flutter_speech只用到了权限申请这一个能力。但如果你开发其他类型的插件(比如文件管理、页面跳转),可能会用到更多。

3.4 Context的使用示例

在flutter_speech的activate方法中,UIAbilityContext是这样被使用的:

private async activate(locale: string, result: MethodResult): Promise<void> {
  try {
    // 检查abilityContext是否可用
    if (this.abilityContext) {
      console.info(TAG, `requesting microphone permission...`);

      // 使用abilityContext申请权限
      const atManager = abilityAccessCtrl.createAtManager();
      const grantResult = await atManager.requestPermissionsFromUser(
        this.abilityContext,
        ['ohos.permission.MICROPHONE']
      );

      // 检查权限结果
      const allGranted = grantResult.authResults.every(
        (status: number) => status === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED
      );

      if (!allGranted) {
        result.error('SPEECH_PERMISSION_DENIED', 'Microphone permission denied', null);
        return;
      }
    } else {
      // abilityContext为null,说明Ability还没准备好或已经销毁
      console.error(TAG, `abilityContext is null`);
      result.error('SPEECH_CONTEXT_ERROR', 'UIAbilityContext not available', null);
      return;
    }

    // 权限获取成功,继续创建语音识别引擎...
  } catch (e) {
    console.error(TAG, `activate error: ${JSON.stringify(e)}`);
    result.error('SPEECH_ACTIVATION_ERROR', `Failed: ${JSON.stringify(e)}`, null);
  }
}

🎯 关键逻辑:先检查abilityContext是否为null,再使用它申请权限。这种防御性编程在插件开发中非常重要。

四、onAttachedToAbility / onDetachedFromAbility 生命周期

4.1 调用时机

App启动
  │
  ├── FlutterEngine创建
  │     ├── onAttachedToEngine()      ← 第1步:引擎绑定
  │     │
  │     ├── onAttachedToAbility()     ← 第2步:Ability绑定
  │     │     此时abilityContext可用
  │     │
  │     ├── [用户调用activate]
  │     │     使用abilityContext申请权限
  │     │
  │     ├── [正常运行...]
  │     │
  │     ├── onDetachedFromAbility()   ← 第3步:Ability解绑
  │     │     此时abilityContext不再可用
  │     │
  │     └── onDetachedFromEngine()    ← 第4步:引擎解绑
  │
  └── App退出

4.2 时序保证

Flutter-OHOS保证以下调用顺序:

  1. onAttachedToEngine 一定在 onAttachedToAbility 之前调用
  2. onDetachedFromAbility 一定在 onDetachedFromEngine 之前调用
  3. onAttachedToAbilityonDetachedFromAbility 一定成对出现

这意味着:

  • onAttachedToAbility中,this.channel已经创建好了(因为Engine先绑定)
  • onDetachedFromEngine中,this.abilityContext已经是null了(因为Ability先解绑)

4.3 异常场景处理

场景1:Ability重建(配置变更)

onDetachedFromAbility()   ← 旧Ability销毁
onAttachedToAbility()     ← 新Ability创建

这种情况下,插件会先失去Context再获得新的Context。如果此时正在进行语音识别,需要妥善处理。

场景2:App切到后台

onBackground()            ← Ability进入后台
// abilityContext仍然可用,但某些操作可能受限
onForeground()            ← Ability回到前台

📌 注意:App切到后台时,onDetachedFromAbility不会被调用。abilityContext仍然有效,但语音识别可能会被系统暂停。

4.4 完整的生命周期日志

建议在开发阶段加上完整的生命周期日志:

onAttachedToEngine(binding: FlutterPluginBinding): void {
  console.info(TAG, '🟢 onAttachedToEngine');
  this.channel = new MethodChannel(binding.getBinaryMessenger(),
      "com.flutter.speech_recognition");
  this.channel.setMethodCallHandler(this);
}

onAttachedToAbility(binding: AbilityPluginBinding): void {
  console.info(TAG, '🟢 onAttachedToAbility');
  this.abilityContext = binding.getAbility().context;
  console.info(TAG, `abilityContext obtained: ${this.abilityContext != null}`);
}

onDetachedFromAbility(): void {
  console.info(TAG, '🔴 onDetachedFromAbility');
  this.abilityContext = null;
}

onDetachedFromEngine(binding: FlutterPluginBinding): void {
  console.info(TAG, '🔴 onDetachedFromEngine');
  if (this.channel != null) {
    this.channel.setMethodCallHandler(null);
  }
  this.destroyEngine();
}

运行时日志输出:

🟢 onAttachedToEngine
🟢 onAttachedToAbility
abilityContext obtained: true
... (正常运行)
🔴 onDetachedFromAbility
🔴 onDetachedFromEngine

五、与 Android ActivityAware 的对比分析

5.1 接口方法对比

Android ActivityAware OpenHarmony AbilityAware 说明
onAttachedToActivity(binding) onAttachedToAbility(binding) 获取上下文
onDetachedFromActivity() onDetachedFromAbility() 释放上下文
onReattachedToActivityForConfigChanges(binding) (无) 配置变更重连
onDetachedFromActivityForConfigChanges() (无) 配置变更断开

Android有4个方法,OpenHarmony只有2个。少了的两个是处理配置变更(如屏幕旋转)的,OpenHarmony的Ability模型不需要这种处理。

5.2 上下文获取方式对比

// Android - 获取Activity
@Override
public void onAttachedToActivity(ActivityPluginBinding binding) {
  this.activity = binding.getActivity();
  // activity类型是android.app.Activity
}
// OpenHarmony - 获取UIAbilityContext
onAttachedToAbility(binding: AbilityPluginBinding): void {
  this.abilityContext = binding.getAbility().context;
  // abilityContext类型是common.UIAbilityContext
}
对比项 Android OpenHarmony
获取对象 Activity UIAbilityContext
获取方式 binding.getActivity() binding.getAbility().context
对象类型 android.app.Activity common.UIAbilityContext
权限申请 ActivityCompat.requestPermissions(activity, …) atManager.requestPermissionsFromUser(context, …)

5.3 权限申请代码对比

// Android权限申请
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.RECORD_AUDIO)
        != PackageManager.PERMISSION_GRANTED) {
    ActivityCompat.requestPermissions(activity,
        new String[]{Manifest.permission.RECORD_AUDIO}, REQUEST_CODE);
}
// 结果通过onRequestPermissionsResult回调获取
// OpenHarmony权限申请
const atManager = abilityAccessCtrl.createAtManager();
const grantResult = await atManager.requestPermissionsFromUser(
  this.abilityContext,
  ['ohos.permission.MICROPHONE']
);
// 结果直接通过await获取,不需要回调

💡 OpenHarmony的优势:权限申请用async/await,代码是线性的,不需要处理回调。Android的权限申请需要在onRequestPermissionsResult回调中处理结果,代码分散在两个方法中,可读性差很多。

六、Context安全使用指南

6.1 使用前必须检查

// ✅ 正确:使用前检查
if (this.abilityContext) {
  const atManager = abilityAccessCtrl.createAtManager();
  await atManager.requestPermissionsFromUser(this.abilityContext, [...]);
} else {
  result.error('SPEECH_CONTEXT_ERROR', 'Context not available', null);
}

// ❌ 错误:不检查直接使用
const atManager = abilityAccessCtrl.createAtManager();
await atManager.requestPermissionsFromUser(this.abilityContext!, [...]); // 可能崩溃

6.2 不要长期持有Context的子对象

// ❌ 危险:持有Context派生的对象
private resourceManager: ResourceManager | null = null;

onAttachedToAbility(binding: AbilityPluginBinding): void {
  this.abilityContext = binding.getAbility().context;
  this.resourceManager = this.abilityContext.resourceManager; // 危险!
}

onDetachedFromAbility(): void {
  this.abilityContext = null;
  // resourceManager还指向旧Context的资源管理器,可能已失效
}
// ✅ 安全:每次使用时从Context获取
private getResourceManager(): ResourceManager | null {
  return this.abilityContext?.resourceManager ?? null;
}

6.3 线程安全

// AbilityAware的回调在主线程调用
// 如果你在其他线程使用abilityContext,需要注意线程安全

// ✅ 在主线程使用
onAttachedToAbility(binding: AbilityPluginBinding): void {
  this.abilityContext = binding.getAbility().context;
}

// ⚠️ 如果在异步操作中使用,需要检查是否仍然有效
private async activate(locale: string, result: MethodResult): Promise<void> {
  // await之后,abilityContext可能已经变成null了(虽然概率很低)
  if (!this.abilityContext) {
    result.error('CONTEXT_LOST', 'Context lost during async operation', null);
    return;
  }
  // 继续使用...
}

七、实际应用场景分析

7.1 flutter_speech中的完整使用流程

1. App启动
   └── onAttachedToAbility → 保存abilityContext

2. 用户点击"开始识别"
   └── Dart调用activate("zh_CN")
       └── 原生端activate方法
           ├── 检查abilityContext != null ✅
           ├── 使用abilityContext申请MICROPHONE权限
           ├── 用户授权 ✅
           ├── 创建语音识别引擎
           └── 返回success(true)

3. 语音识别进行中
   └── abilityContext不再需要(引擎已创建)

4. App退出
   └── onDetachedFromAbility → abilityContext = null
   └── onDetachedFromEngine → 销毁引擎

7.2 权限被拒绝的处理

if (!allGranted) {
  console.info(TAG, 'permission denied by user');
  result.error('SPEECH_PERMISSION_DENIED', 'Microphone permission denied', null);
  return;
}

Dart端的处理:

_speech.activate('zh_CN').then((res) {
  setState(() => _speechRecognitionAvailable = res);
}).catchError((e) {
  // 权限被拒绝会走到这里
  print('activate error: $e');
  setState(() => _speechRecognitionAvailable = false);
});

7.3 Context不可用的处理

if (!this.abilityContext) {
  console.error(TAG, 'abilityContext is null, cannot request permission');
  result.error('SPEECH_CONTEXT_ERROR', 'UIAbilityContext not available', null);
  return;
}

这种情况通常发生在:

  1. Ability还没有启动完成就调用了activate
  2. Ability已经销毁但插件还在运行
  3. 热重载过程中的短暂时间窗口

八、调试与问题排查

8.1 常见问题

问题 症状 原因 解决方案
abilityContext为null activate返回CONTEXT_ERROR onAttachedToAbility未调用 检查AbilityAware接口是否正确实现
权限弹窗不出现 权限申请静默失败 module.json5未声明权限 在宿主App的module.json5中添加权限声明
权限申请崩溃 App闪退 Context已失效 添加null检查
重复权限弹窗 每次activate都弹窗 未缓存权限状态 先检查已有权限再申请

8.2 日志排查流程

# 1. 确认生命周期调用顺序
hdc hilog | grep "FlutterSpeechPlugin" | grep -E "onAttached|onDetached"

# 预期输出:
# 🟢 onAttachedToEngine
# 🟢 onAttachedToAbility
# abilityContext obtained: true

# 2. 确认权限申请流程
hdc hilog | grep "FlutterSpeechPlugin" | grep -E "permission|MICROPHONE"

# 预期输出:
# requesting microphone permission...
# permission granted: true

8.3 权限状态检查工具

// 在activate中添加权限状态检查
private async checkPermissionStatus(): Promise<boolean> {
  if (!this.abilityContext) return false;

  const atManager = abilityAccessCtrl.createAtManager();
  const tokenId = this.abilityContext.applicationInfo.accessTokenId;
  const status = atManager.checkAccessTokenSync(
    tokenId,
    'ohos.permission.MICROPHONE'
  );

  return status === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED;
}

总结

本文详细讲解了AbilityAware接口在flutter_speech中的实现:

  1. Ability模型:OpenHarmony的UIAbility类似Android的Activity,但生命周期和上下文管理方式不同
  2. AbilityAware接口:两个方法——onAttachedToAbility获取Context,onDetachedFromAbility释放Context
  3. UIAbilityContext:通过binding.getAbility().context获取,用于权限申请等系统操作
  4. 安全使用:使用前必须检查null,Ability销毁后必须置null
  5. 与Android对比:接口设计类似但更简洁,权限申请用async/await更优雅

下一篇我们进入Core Speech Kit的世界,看看OpenHarmony的语音识别引擎是怎么工作的。

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!


相关资源:

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐