Flutter三方库适配OpenHarmony【flutter_speech】— AbilityAware 接口与上下文获取

前言

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

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

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

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

💡 核心知识点 :AbilityAware接口的两个方法------onAttachedToAbility和onDetachedFromAbility,以及如何通过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上下文中进行
typescript 复制代码
// 这两个操作都需要UIAbilityContext
const atManager = abilityAccessCtrl.createAtManager();
const grantResult = await atManager.requestPermissionsFromUser(
  this.abilityContext,  // ← 需要UIAbilityContext
  ['ohos.permission.MICROPHONE']
);

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

二、AbilityAware 接口的作用与实现

2.1 接口定义

typescript 复制代码
interface AbilityAware {
  onAttachedToAbility(binding: AbilityPluginBinding): void;
  onDetachedFromAbility(): void;
}

就两个方法,非常简洁:

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

2.2 导入AbilityPluginBinding

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

typescript 复制代码
import { AbilityPluginBinding } from
  '@ohos/flutter_ohos/src/main/ets/embedding/engine/plugins/ability/AbilityPluginBinding';

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

2.3 flutter_speech的实现

typescript 复制代码
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,可能会崩溃
typescript 复制代码
// ❌ 危险:使用已失效的Context
onDetachedFromAbility(): void {
  // 不置null,abilityContext还指向已销毁的Ability
}

// 后续调用activate时:
if (this.abilityContext) {
  // abilityContext不为null,但实际上已经失效了
  // 调用requestPermissionsFromUser可能会崩溃!
  atManager.requestPermissionsFromUser(this.abilityContext, ...);
}
typescript 复制代码
// ✅ 安全:置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相关信息的方法:

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

3.2 获取Context的调用链

typescript 复制代码
// 完整的调用链
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是这样被使用的:

typescript 复制代码
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. onAttachedToAbility 和 onDetachedFromAbility 一定成对出现

这意味着:

  • 在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 完整的生命周期日志

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

typescript 复制代码
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 上下文获取方式对比

java 复制代码
// Android - 获取Activity
@Override
public void onAttachedToActivity(ActivityPluginBinding binding) {
  this.activity = binding.getActivity();
  // activity类型是android.app.Activity
}
typescript 复制代码
// 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 权限申请代码对比

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

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

六、Context安全使用指南

6.1 使用前必须检查

typescript 复制代码
// ✅ 正确:使用前检查
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的子对象

typescript 复制代码
// ❌ 危险:持有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的资源管理器,可能已失效
}
typescript 复制代码
// ✅ 安全:每次使用时从Context获取
private getResourceManager(): ResourceManager | null {
  return this.abilityContext?.resourceManager ?? null;
}

6.3 线程安全

typescript 复制代码
// 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 权限被拒绝的处理

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

Dart端的处理:

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

7.3 Context不可用的处理

typescript 复制代码
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 日志排查流程

bash 复制代码
# 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 权限状态检查工具

typescript 复制代码
// 在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的语音识别引擎是怎么工作的。

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


相关资源:

相关推荐
2601_949970699 小时前
HumanizeDuration 跨端三方库仓颉适配全流程
前端·javascript·flutter
传奇开心果编程11 小时前
【Flutter踩坑排雷】第3课:const 误用坑|const 错误使用引发 UI 异常与性能损耗
flutter·移动开发·跨平台·dart·编程语言理论·编程框架·声明式ui
程序员老刘13 小时前
你的Flutter项目要不要紧急升级3.47.8?
flutter·客户端
Talyra4215 小时前
不想开会员,我写了一个记账 App
flutter
2601_9495758619 小时前
JSON5 跨端三方库仓颉适配全流程
前端·javascript·flutter
法的空间19 小时前
AI 编程时代,为什么 Dart 分析会拖慢大型项目的开发
android·flutter·ios
恋猫de小郭21 小时前
Flutter 紧急发布 3.47.8 安全升级版本,建议都升级
android·前端·flutter
好的佩奇2 天前
Dart 之模式
android·flutter·dart
淡淡的香烟2 天前
Flutter实现多语言切换
android·flutter·1024程序员节
事圆则缓2 天前
从零搭建一套 Flutter 架构:先跑通数据流
flutter