Flutter三方库适配OpenHarmony【secure_application】— 资源释放与回调注销
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net插件的创建和初始化大家都很重视,但销毁和清理往往被忽视。不正确的资源释放会导致内存泄漏、回调空指针崩溃、热重载后行为异常等问题。secure_application 在 OpenHarmony 端注册了窗口事件和生命周期回调,这些都需要在插件解绑时正确注销。今天把的清理逻辑讲透。清理顺序:
前言
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
插件的创建和初始化大家都很重视,但销毁和清理往往被忽视。不正确的资源释放会导致内存泄漏、回调空指针崩溃、热重载后行为异常等问题。secure_application 在 OpenHarmony 端注册了窗口事件和生命周期回调,这些都需要在插件解绑时正确注销。
今天把 onDetachedFromEngine 的清理逻辑讲透。
一、onDetachedFromEngine 完整实现
1.1 源码
onDetachedFromEngine(binding: FlutterPluginBinding): void {
if (this.channel != null) {
this.channel.setMethodCallHandler(null);
}
this.unregisterWindowEventCallback();
this.unregisterLifecycleCallback();
this.channel = null;
this.context = null;
this.mainWindow = null;
}
1.2 清理步骤分解
| 步骤 | 操作 | 目的 |
|---|---|---|
| 1 | setMethodCallHandler(null) | 停止接收 Dart 层的方法调用 |
| 2 | unregisterWindowEventCallback() | 停止监听窗口事件 |
| 3 | unregisterLifecycleCallback() | 停止监听应用生命周期 |
| 4 | channel = null | 释放 MethodChannel 引用 |
| 5 | context = null | 释放 Context 引用 |
| 6 | mainWindow = null | 释放 Window 引用 |
1.3 清理顺序的重要性
先注销回调 → 再置空引用
为什么不能反过来?
// ❌ 错误顺序
this.mainWindow = null; // 先置空
this.unregisterWindowEventCallback(); // 再注销 → mainWindow 已经是 null,无法注销!
// ✅ 正确顺序
this.unregisterWindowEventCallback(); // 先注销 → mainWindow 还有效
this.mainWindow = null; // 再置空
📌 原则:先注销所有回调,再释放所有引用。回调注销需要用到引用,所以引用必须在回调注销之后才能置空。
二、窗口事件回调注销
2.1 unregisterWindowEventCallback
private unregisterWindowEventCallback(): void {
if (this.mainWindow == null) {
return;
}
try {
this.mainWindow.off('windowEvent');
Log.i(TAG, "Window event callback unregistered");
} catch (err) {
Log.e(TAG, "Failed to unregister window event callback: " + JSON.stringify(err));
}
}
2.2 off 方法的行为
// 移除所有 windowEvent 监听器
this.mainWindow.off('windowEvent');
// 也可以移除特定的监听器(需要传入回调引用)
this.mainWindow.off('windowEvent', specificCallback);
secure_application 使用了不带回调参数的 off,会移除所有 windowEvent 监听器。这在单插件场景下没问题,但如果有多个插件同时监听同一个窗口事件,可能会误删其他插件的监听器。
2.3 空值保护
if (this.mainWindow == null) {
return; // 窗口从未获取成功,无需注销
}
如果 getMainWindow 失败了(比如插件初始化太早),mainWindow 为 null,此时不需要注销。
2.4 异常保护
try {
this.mainWindow.off('windowEvent');
} catch (err) {
Log.e(TAG, "Failed to unregister window event callback: " + JSON.stringify(err));
}
即使注销失败也不应该阻止后续的清理工作。用 try-catch 包裹,记录日志后继续执行。
三、生命周期回调注销
3.1 unregisterLifecycleCallback
private unregisterLifecycleCallback(): void {
if (this.context == null || this.applicationStateChangeCallback == null) {
return;
}
try {
const applicationContext = this.context.getApplicationContext();
applicationContext.off('applicationStateChange', this.applicationStateChangeCallback);
this.applicationStateChangeCallback = null;
Log.i(TAG, "Lifecycle callback unregistered");
} catch (err) {
Log.e(TAG, "Failed to unregister lifecycle callback: " + JSON.stringify(err));
}
}
3.2 精确注销
applicationContext.off('applicationStateChange', this.applicationStateChangeCallback);
这里传入了具体的回调对象,只注销这个插件注册的回调,不影响其他可能注册了同类回调的组件。
3.3 双重空值检查
| 检查 | 原因 |
|---|---|
this.context == null |
没有上下文就无法获取 applicationContext |
this.applicationStateChangeCallback == null |
回调从未注册成功 |
3.4 回调引用置空
this.applicationStateChangeCallback = null;
注销后立即置空,防止:
- 重复注销
- 回调对象持有的闭包引用无法被 GC
四、引用置空策略
4.1 三个引用的置空
this.channel = null;
this.context = null;
this.mainWindow = null;
4.2 为什么要置空
| 引用 | 不置空的后果 |
|---|---|
| channel | MethodChannel 对象无法被 GC,持有 BinaryMessenger 引用 |
| context | Context 对象无法被 GC,可能持有大量系统资源 |
| mainWindow | Window 对象无法被 GC,持有系统窗口服务的连接 |
4.3 置空的时机
// 在所有使用这些引用的操作完成后再置空
this.unregisterWindowEventCallback(); // 使用 mainWindow
this.unregisterLifecycleCallback(); // 使用 context
this.channel = null; // 最后置空
this.context = null;
this.mainWindow = null;
五、内存泄漏场景分析
5.1 场景一:不注销窗口事件
onDetachedFromEngine 被调用
│
├── 没有调用 off('windowEvent')
│
└── 窗口事件回调仍然存在
│
├── 回调持有 this(插件实例)的引用
├── 插件实例持有 channel、context、mainWindow 的引用
└── 整个引用链无法被 GC → 内存泄漏
5.2 场景二:不注销生命周期回调
onDetachedFromEngine 被调用
│
├── 没有调用 off('applicationStateChange')
│
└── 生命周期回调仍然存在
│
├── 应用进入后台时回调触发
├── 回调中访问 this.channel(已经是 null)
└── 空指针异常 → 崩溃
5.3 场景三:热重载
热重载触发
│
├── 旧插件实例:onDetachedFromEngine
│ │
│ └── 如果不清理 → 旧回调仍然存在
│
└── 新插件实例:onAttachedToEngine
│
└── 注册新的回调
│
└── 旧回调 + 新回调同时存在 → 重复触发
💡 热重载是最容易暴露清理问题的场景。开发过程中频繁热重载,如果清理不彻底,很快就会发现回调被重复触发。
六、与 flutter_speech 清理逻辑的对比
6.1 flutter_speech 的清理
onDetachedFromEngine(binding: FlutterPluginBinding): void {
if (this.channel != null) {
this.channel.setMethodCallHandler(null);
}
this.destroyEngine(); // 销毁语音识别引擎
}
6.2 secure_application 的清理
onDetachedFromEngine(binding: FlutterPluginBinding): void {
if (this.channel != null) {
this.channel.setMethodCallHandler(null);
}
this.unregisterWindowEventCallback(); // 注销窗口事件
this.unregisterLifecycleCallback(); // 注销生命周期回调
this.channel = null;
this.context = null;
this.mainWindow = null;
}
6.3 对比
| 维度 | flutter_speech | secure_application |
|---|---|---|
| 需要销毁的资源 | 语音识别引擎 | 窗口事件 + 生命周期回调 |
| 资源类型 | 系统服务(重量级) | 事件监听器(轻量级) |
| 销毁顺序 | cancel → shutdown → null | 注销回调 → 置空引用 |
| 异步操作 | 有(引擎销毁) | 无(注销是同步的) |
七、清理检查清单
7.1 开发时检查
- onDetachedFromEngine 中移除了 MethodCallHandler
- 所有 Window.on 注册的回调都有对应的 off
- 所有 applicationContext.on 注册的回调都有对应的 off
- 所有对象引用在不再使用后置为 null
- 清理操作用 try-catch 包裹,不会因异常中断
7.2 测试验证
# 1. 热重载测试
flutter run -d ohos_device
# 修改代码,触发热重载
# 检查日志中是否有重复的回调触发
# 2. 内存泄漏测试
hdc hilog | grep "SecureApplicationPlugin"
# 检查是否有 "Window event callback unregistered" 日志
# 检查是否有 "Lifecycle callback unregistered" 日志
# 3. 崩溃测试
# 快速切换前后台,检查是否有空指针异常
7.3 常见遗漏
| 遗漏 | 后果 | 检测方式 |
|---|---|---|
| 忘记 off(‘windowEvent’) | 内存泄漏 + 重复触发 | 热重载后检查日志 |
| 忘记 off(‘applicationStateChange’) | 崩溃 | 切后台时检查 |
| 忘记 channel = null | 轻微内存泄漏 | 内存分析工具 |
| 清理顺序错误 | 注销失败 | 检查错误日志 |
总结
本文详细讲解了 secure_application 的资源释放与回调注销:
- 清理顺序:先注销回调,再置空引用
- 窗口事件注销:off(‘windowEvent’) 移除所有监听器
- 生命周期注销:off(‘applicationStateChange’, callback) 精确注销
- 引用置空:channel、context、mainWindow 全部置 null
- 异常保护:每个注销操作都用 try-catch 包裹
下一篇我们从 Dart 层的视角分析应用生命周期状态机——didChangeAppLifecycleState 的完整逻辑。
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
相关资源:
更多推荐


所有评论(0)