Flutter三方库适配OpenHarmony【flutter_speech】— AbilityAware 接口与上下文获取
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net上一篇我们搞定了FlutterPlugin接口,插件已经能被Flutter引擎加载了。但如果你现在就去调用activate方法申请麦克风权限,会发现一个尴尬的问题——没有UIAbilityContext。在OpenHarmony中,很多系统级操作(权限申请、弹窗、页面跳转等)都需要。这个上
前言
欢迎加入开源鸿蒙跨平台社区: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来做两件事:
- 申请麦克风权限:
abilityAccessCtrl.createAtManager().requestPermissionsFromUser()需要Context参数 - 能力检测:某些系统能力检测需要在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;
}
}
实现非常简单:
onAttachedToAbility:通过binding.getAbility().context获取UIAbilityContext并保存onDetachedFromAbility:将保存的引用置为null
2.4 为什么要置null
有人可能会问:onDetachedFromAbility里为什么要把abilityContext置为null?直接不管不行吗?
不行。原因有两个:
- 防止内存泄漏:UIAbilityContext持有大量系统资源,如果插件一直引用它,GC就无法回收
- 防止野指针: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保证以下调用顺序:
onAttachedToEngine一定在onAttachedToAbility之前调用onDetachedFromAbility一定在onDetachedFromEngine之前调用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 完整的生命周期日志
建议在开发阶段加上完整的生命周期日志:
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;
}
这种情况通常发生在:
- Ability还没有启动完成就调用了activate
- Ability已经销毁但插件还在运行
- 热重载过程中的短暂时间窗口
八、调试与问题排查
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中的实现:
- Ability模型:OpenHarmony的UIAbility类似Android的Activity,但生命周期和上下文管理方式不同
- AbilityAware接口:两个方法——onAttachedToAbility获取Context,onDetachedFromAbility释放Context
- UIAbilityContext:通过
binding.getAbility().context获取,用于权限申请等系统操作 - 安全使用:使用前必须检查null,Ability销毁后必须置null
- 与Android对比:接口设计类似但更简洁,权限申请用async/await更优雅
下一篇我们进入Core Speech Kit的世界,看看OpenHarmony的语音识别引擎是怎么工作的。
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
相关资源:
更多推荐
所有评论(0)