前言

欢迎加入开源鸿蒙跨平台社区: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;

注销后立即置空,防止:

  1. 重复注销
  2. 回调对象持有的闭包引用无法被 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 的资源释放与回调注销:

  1. 清理顺序:先注销回调,再置空引用
  2. 窗口事件注销:off(‘windowEvent’) 移除所有监听器
  3. 生命周期注销:off(‘applicationStateChange’, callback) 精确注销
  4. 引用置空:channel、context、mainWindow 全部置 null
  5. 异常保护:每个注销操作都用 try-catch 包裹

下一篇我们从 Dart 层的视角分析应用生命周期状态机——didChangeAppLifecycleState 的完整逻辑。

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


相关资源:

Logo

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

更多推荐