前言

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

前几篇我们从原生端的角度看了窗口事件和生命周期回调。这一篇换个视角,从 Dart 层didChangeAppLifecycleState 的完整逻辑。这段代码是 secure_application 最核心的业务逻辑——它决定了什么时候锁定、什么时候触发认证、什么时候解锁。

说实话,第一次读这段代码的时候我也花了不少时间才理清所有分支。今天把它彻底拆解。

一、AppLifecycleState 四种状态

1.1 状态定义

enum AppLifecycleState {
  resumed,   // 应用在前台,可见且可交互
  inactive,  // 应用可见但不可交互(如来电、下拉通知栏)
  paused,    // 应用不可见(进入后台)
  detached,  // 应用即将销毁
}

1.2 状态转换

                    resumed
                   ↗       ↘
            inactive         paused
                   ↘       ↗
                    detached
用户操作 状态变化
正常使用 App resumed
下拉通知栏 resumed → inactive
收起通知栏 inactive → resumed
按 Home 键 resumed → inactive → paused
切回 App paused → inactive → resumed
杀掉 App paused → detached

1.3 在 OpenHarmony 上的行为

操作 Android OpenHarmony
按 Home 键 resumed→inactive→paused resumed→inactive→paused
打开最近任务 resumed→inactive resumed→inactive
切到其他 App resumed→inactive→paused resumed→inactive→paused
下拉通知栏 resumed→inactive resumed→inactive

📌 OpenHarmony 上的 AppLifecycleState 行为与 Android 基本一致,因为 Flutter-OHOS 框架做了对齐。

二、didChangeAppLifecycleState 完整逻辑

2.1 源码


void didChangeAppLifecycleState(AppLifecycleState state) async {
  switch (state) {
    case AppLifecycleState.resumed:
      if (mounted) {
        setState(() => _removeNativeOnNextFrame = true);
      } else {
        _removeNativeOnNextFrame = true;
      }
      if (!secureApplicationController.paused) {
        if (secureApplicationController.secured &&
            secureApplicationController.value.locked) {
          if (widget.onNeedUnlock != null) {
            secureApplicationController.pause();
            var authStatus = await widget.onNeedUnlock!(secureApplicationController);
            if (authStatus != null) {
              secureApplicationController.sendAuthenticationEvent(authStatus);
            }
            WidgetsBinding.instance.addPostFrameCallback((_) {
              secureApplicationController.unpause();
            });
          }
        }
        secureApplicationController.resumed();
      }
      super.didChangeAppLifecycleState(state);
      break;
    case AppLifecycleState.inactive:
      if (!secureApplicationController.paused) {
        if (secureApplicationController.secured) {
          secureApplicationController.lock();
        }
      }
      super.didChangeAppLifecycleState(state);
      break;
    case AppLifecycleState.paused:
      if (!secureApplicationController.paused) {
        if (secureApplicationController.secured) {
          secureApplicationController.lock();
        }
      }
      super.didChangeAppLifecycleState(state);
      break;
    default:
      super.didChangeAppLifecycleState(state);
      break;
  }
}

三、inactive / paused 分支:锁定逻辑

3.1 代码

case AppLifecycleState.inactive:
case AppLifecycleState.paused:
  if (!secureApplicationController.paused) {
    if (secureApplicationController.secured) {
      secureApplicationController.lock();
    }
  }
  break;

3.2 决策树

AppLifecycleState.inactive 或 paused
    │
    ├── controller.paused == true?
    │       └── YES → 什么都不做(暂停状态,跳过锁定)
    │
    └── controller.paused == false?
            │
            ├── controller.secured == true?
            │       └── YES → controller.lock() → 显示模糊遮罩
            │
            └── controller.secured == false?
                    └── 什么都不做(保护未开启)

3.3 两个条件的含义

条件 含义 为 false 时
!paused 没有被暂停 正在进行文件选择等操作,不应锁定
secured 保护已开启 用户没有开启保护,不需要锁定

3.4 inactive 和 paused 的处理相同

两个状态用了完全相同的逻辑。为什么?

因为用户按 Home 键时,状态变化是 resumed → inactive → paused。如果只在 paused 时锁定,那么在 inactive 阶段(大约几百毫秒)用户可能在应用切换器中看到未保护的内容。

inactive 时就锁定,可以尽早保护内容。

四、resumed 分支:解锁与认证

4.1 第一部分:原生遮罩移除

case AppLifecycleState.resumed:
  if (mounted) {
    setState(() => _removeNativeOnNextFrame = true);
  } else {
    _removeNativeOnNextFrame = true;
  }

设置标志位,在下一次 build 时移除原生端的保护(主要针对 iOS 的原生模糊视图)。

4.2 第二部分:认证流程

if (!secureApplicationController.paused) {
  if (secureApplicationController.secured &&
      secureApplicationController.value.locked) {
    if (widget.onNeedUnlock != null) {
      secureApplicationController.pause();
      var authStatus = await widget.onNeedUnlock!(secureApplicationController);
      if (authStatus != null) {
        secureApplicationController.sendAuthenticationEvent(authStatus);
      }
      WidgetsBinding.instance.addPostFrameCallback((_) {
        secureApplicationController.unpause();
      });
    }
  }
  secureApplicationController.resumed();
}

4.3 认证流程决策树

AppLifecycleState.resumed
    │
    ├── controller.paused == true?
    │       └── YES → 什么都不做
    │
    └── controller.paused == false?
            │
            ├── secured && locked?
            │       │
            │       ├── onNeedUnlock != null?
            │       │       │
            │       │       ├── YES → 执行认证流程
            │       │       │   ├── pause()(防止认证过程中再次触发)
            │       │       │   ├── await onNeedUnlock()
            │       │       │   ├── sendAuthenticationEvent()
            │       │       │   └── unpause()(恢复正常)
            │       │       │
            │       │       └── NO → 不认证(遮罩保持显示)
            │       │
            │       └── resumed()(通知监听者)
            │
            └── !secured || !locked?
                    └── resumed()(通知监听者)

4.4 pause/unpause 的妙用

secureApplicationController.pause();
var authStatus = await widget.onNeedUnlock!(secureApplicationController);
// ...
WidgetsBinding.instance.addPostFrameCallback((_) {
  secureApplicationController.unpause();
});

认证过程中先 pause(),认证完成后 unpause()。为什么?

因为认证过程可能涉及跳转到其他页面(比如系统的生物识别界面),这会再次触发 inactive 状态。如果不 pause,就会再次锁定,导致认证流程被中断。

时间线 没有 pause 有 pause
t0: resumed 开始认证 开始认证 + pause
t1: 弹出生物识别 inactive → 再次锁定 ❌ inactive → paused=true,跳过锁定 ✅
t2: 认证完成 被锁定了,认证白做 正常完成
t3: 回到 App 又要认证一次 unpause,流程结束

💡 这是整个插件最精妙的设计之一。pause/unpause 机制解决了"认证过程中触发再次锁定"的问题。

4.5 addPostFrameCallback 的作用

WidgetsBinding.instance.addPostFrameCallback((_) {
  secureApplicationController.unpause();
});

为什么不直接 unpause(),而要等到下一帧?

因为 onNeedUnlock 返回后,Flutter 可能还在处理状态变化。如果立即 unpause,可能会在同一帧内触发不必要的重建。等到下一帧再 unpause,确保所有状态变化都已经处理完毕。

五、paused 状态的特殊场景

5.1 文件选择器场景

// 打开文件选择器前
controller.pause();

// 选择文件
final file = await FilePicker.platform.pickFiles();

// 选择完成后
controller.unpause();

如果不 pause,打开文件选择器会触发 inactive → paused,导致锁定。用户选完文件回来还要重新认证,体验很差。

5.2 相机/图片选择器场景

controller.pause();
final image = await ImagePicker().pickImage(source: ImageSource.camera);
controller.unpause();

5.3 第三方登录场景

controller.pause();
final result = await signInWithGoogle();
controller.unpause();

跳转到 Google 登录页面会触发 App 进入后台,需要 pause 来避免锁定。

5.4 通用模式

// 任何会导致 App 暂时离开前台的操作
controller.pause();
try {
  await someExternalOperation();
} finally {
  controller.unpause();  // 确保一定会 unpause
}

📌 注意:一定要在 finally 中 unpause,否则如果操作抛出异常,App 会永远处于 paused 状态,再也不会锁定。

六、build 方法中的原生遮罩移除

6.1 代码


Widget build(BuildContext context) {
  if (_removeNativeOnNextFrame && widget.autoUnlockNative) {
    Future.delayed(Duration(milliseconds: widget.nativeRemoveDelay))
        .then((_) => SecureApplicationNative.unlock());
    _removeNativeOnNextFrame = false;
  }
  return SecureApplicationProvider(
    secureData: secureApplicationController,
    child: widget.child,
  );
}

6.2 nativeRemoveDelay 的作用

const SecureApplication({
  this.nativeRemoveDelay = 1000,  // 默认 1000ms
  // ...
});
平台 需要延迟移除 原因
iOS 原生模糊视图需要等 Flutter 渲染完成后再移除
Android 没有原生遮罩
OpenHarmony 没有原生遮罩

在 OpenHarmony 上,SecureApplicationNative.unlock() 会调用原生端的 unlock 方法,但那是空实现,所以这个延迟移除实际上没有效果。不过保留这个逻辑不会造成问题。

七、状态机的完整流转

7.1 正常保护流程

1. 用户调用 controller.secure()
   → secured=true, 原生端开启隐私模式

2. 用户按 Home 键
   → inactive: lock() → locked=true, 遮罩显示
   → paused: lock() → 已锁定,跳过

3. 用户切回 App
   → resumed:
     → secured=true, locked=true → 触发 onNeedUnlock
     → 用户认证成功 → authSuccess(unlock: true)
     → locked=false, 遮罩隐藏

4. 用户调用 controller.open()
   → secured=false, 原生端关闭隐私模式

7.2 暂停场景

1. secured=true, 用户正常使用

2. 用户点击"选择图片"
   → controller.pause() → paused=true

3. 系统图片选择器打开
   → inactive: paused=true → 跳过锁定 ✅

4. 用户选完图片回来
   → resumed: paused=true → 跳过认证
   → controller.unpause() → paused=false

5. 用户按 Home 键
   → inactive: paused=false, secured=true → 正常锁定

7.3 认证失败场景

1. 用户切回 App, secured=true, locked=true

2. onNeedUnlock 被调用
   → 返回 SecureApplicationAuthenticationStatus.FAILED

3. sendAuthenticationEvent(FAILED)
   → authenticationEvents 流发射 FAILED
   → onAuthenticationFailed 回调被调用

4. 遮罩仍然显示(因为没有调用 unlock)
   → 用户需要再次尝试认证

总结

本文全面解析了 secure_application 的应用生命周期状态机:

  1. inactive/paused:检查 paused 和 secured 标志,决定是否锁定
  2. resumed:触发认证流程,pause/unpause 防止认证中断
  3. pause 机制:文件选择器、相机等场景需要临时暂停保护
  4. nativeRemoveDelay:延迟移除原生遮罩,主要针对 iOS
  5. addPostFrameCallback:确保状态变化在下一帧生效

下一篇我们讲认证流程与 authenticationEvents 事件流——onNeedUnlock 的完整工作机制。

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


相关资源:

请添加图片描述请添加图片描述
Flutter 应用生命周期状态转换图

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐