人脸识别运行时封装与异常白屏修复详细方案

1. 结论

本次采用“兼容式内部重构”,在不改变现有业务和现有交互的前提下,解决人脸识别模型初始化失败后白屏、应用重新进入首页以及菜单消费订单丢失的问题。

第一阶段保持 7 个业务页面现有调用方式、FaceRecognitionPop 公开接口、识别成功确认流程、失败关闭时机和支付接口不变,只调整人脸识别弹框内部的模型、相机和生命周期职责:

现有业务页面
      原构造参数、原监听器、原业务回调保持不变
    
FaceRecognitionPop
      保留现有 UI 和交互,内部委托识别工作
    
FaceRecognitionSession
      管理本次弹框的相机、识别、倒计时和资源释放
    
FaceRecognitionRuntime
       管理应用进程内的人脸模型、人脸库和初始化状态

第二阶段在一期稳定边界上增加统一入口,7 个页面只迁移弹框创建代码,原业务监听器和支付、核验方法继续留在页面:

7 个业务页面
    │  组装场景、金额、副屏和确认文案
    ▼
FaceRecognitionLauncher
    │  生命周期门禁、统一 Tag、防重复展示
    ▼
FaceRecognitionPop → FaceRecognitionSession → FaceRecognitionRuntime

必须删除 FaceRecognitionPop 中模型初始化失败后的 restartSelf()、应用入口重启和 Process.killProcess()。异常时改为记录错误、停止识别、释放相机、关闭弹框并留在原业务页面。

除上述异常路径从“白屏并重启”修正为“安全关闭弹框”以外,用户可见交互和业务结果不得改变。

2. 硬性实施边界

2.1 必须解决

2.2 必须保持不变

2.3 本次不做

3. 当前代码现状

3.1 使用范围

当前共有 7 个业务页面直接使用 FaceRecognitionPop,共 8 个构造位置。MainActivity 因单屏和双屏分支存在两个构造位置。

  1. MainActivity:菜单消费。

  2. FreeActivity:自由消费。

  3. FreeNoKeyboardActivity:无键盘自由消费。

  4. QuotaActivity:定额消费。

  5. QuotaNoKeyboardActivity:无键盘定额消费。

  6. GuoxinFacePayActivity:国信人脸支付。

  7. OrderVerifyActivity:订单核验。

这些页面通过同一个 FaceRecognitionResultListener 接收:

3.2 弹框当前承担的职责过多

FaceRecognitionPop 当前同时承担:

模型属于应用进程级资源,相机和识别属于单次弹框资源,UI 属于视图层。三类生命周期混在一个弹框中,使异常处理容易越过业务页面边界。

3.3 启动页已经初始化模型和人脸库

LaunchActivity 当前启动流程会:

  1. 执行人脸启动流程。

  2. 初始化自定义人脸模型。

  3. 初始化本地人脸数据库。

  4. 把本地人脸数据加载进内存。

  5. 完成后进入主页。

FaceRecognitionPop.onStart() 又调用 initModel()onResume() 又调用 initDataBases() 并直接启动相机。模型初始化回调和相机启动没有形成严格的先后关系,存在重复初始化和就绪竞态。

3.4 白屏根因

FaceRecognitionPop.initModel() 初始化失败且错误码不为 -12 时,当前逻辑会:

  1. 提示“模型加载失败,即将重启应用”。

  2. 延迟调用 restartSelf()

  3. 重新启动应用入口 Activity。

  4. 调用 Process.killProcess() 结束当前进程。

该流程不是普通的弹框关闭,而是整个应用重启。菜单消费订单保存在当前 MainActivity 内存中,进程结束后页面和订单对象都会被销毁,因此表现为白屏、回到首页以及订单丢失。

3.5 菜单消费正常失败时的订单状态

当前普通识别超时会停止预览、回调 recognitionEnd() 并关闭弹框;MainActivity.recognitionEnd() 只显示提示并隐藏副屏人脸区域,没有清空 mSelectDishList

自定义金额确认后会生成使用 Constants.CUSTOM_DISH_IDDishBean,并加入 mSelectDishList。因此在 Activity 和应用进程没有被销毁的情况下:

本次修复不新增订单恢复逻辑,只需保证人脸识别异常不再主动销毁应用进程,即可保持这条现有业务链路。

4. 方案选择

4.1 采用方案:兼容式 Runtime + Session

采用以下三层结构:

第一阶段不要求业务页面切换调用入口,避免一次性改动 7 个页面的业务代码。

4.2 未采用:只删除重启代码

只删除 restartSelf() 可以立即避免进程重启,但仍然保留:

因此它只能作为紧急补丁,不能作为完整修复方案。

4.3 未采用:模型和相机全部放在弹框

模型初始化耗时长、生命周期属于应用进程;相机属于一次展示会话。全部放在弹框会使每次进入都承担重资源初始化,并继续放大生命周期问题。

4.4 未采用:巨型 FacePayManager

各页面的成功后业务不同:普通消费调用现有支付接口,国信页面调用独立人脸支付接口,订单核验不是支付。把识别和这些业务全部合并会造成高耦合,并增加业务回归范围。

5. 目标架构

5.1 FaceRecognitionRuntime

定位

FaceRecognitionRuntime 是应用进程级的具体封装类,不是 Activity、Fragment 或弹框生命周期组件。

由应用级单例或 CustomApplication 持有,仅保存 ApplicationContext,不得持有 Activity、Dialog、Fragment 或 PreviewView。初始化等待回调必须包装为可取消的一次性登记,页面或 Session 销毁后立即解除回调引用。

职责

不负责

状态

UNINITIALIZED
       initialize/ensureReady
      
INITIALIZING
      ├────────► READY
      └────────► FAILED

READY 状态下再次调用 ensureReady() 必须直接返回成功,不能重复加载模型。

INITIALIZING 状态下再次调用时,只登记等待回调,不能启动第二个初始化任务。

FAILED 状态下是否允许重试由明确的方法触发,不能在弹框内无限自动重试。

建议接口

public final class FaceRecognitionRuntime {

    public static FaceRecognitionRuntime getInstance();

    public InitRequest initialize(Context context, InitCallback callback);

    public InitRequest ensureReady(Context context, InitCallback callback);

    public RuntimeState getState();
}

接口中的 Context 进入 Runtime 后必须转换为 getApplicationContext()LaunchActivity.onDestroy()FaceRecognitionSession.stop() 必须取消各自的 InitRequest;这只移除页面回调,不影响其他页面等待的同一模型初始化任务。

5.2 FaceRecognitionSession

定位

FaceRecognitionSession 表示一次弹框从显示到关闭的人脸识别会话。每次显示弹框创建一个 Session,弹框销毁后 Session 不得继续工作。

职责

状态

IDLE
   start
  
STARTING
   Runtime READY
  
RECOGNIZING
  ├────────► MATCHED
  ├────────► TIMEOUT
  ├────────► ERROR
  └────────► STOPPED

MATCHED 只代表已经识别到用户,仍然保持当前弹框交互:展示用户信息,等待用户点击“重试”或“确认”。只有点击“确认”才调用原 recognitionSuccess(User user)

建议接口

public final class FaceRecognitionSession {

    public void start(
            Context context,
            AutoTexturePreviewView primaryPreview,
            AutoTexturePreviewView secondaryPreview,
            SessionCallback callback
    );

    public void restart();

    public void stop();
}

Session 可以持有本次弹框的预览 View,但不能跨弹框复用;FaceRecognitionPop.onDestroyView() 停止 Session 后必须释放对 Session 的引用。

并发保护

5.3 FaceRecognitionPop

第一阶段保留的公开接口

public FaceRecognitionPop(
        Context context,
        AutoTexturePreviewView previewView,
        String orderAmount
);

public void setFaceRecognitionResultListener(
        FaceRecognitionResultListener listener
);

public void setConfirmText(String confirmText);

FaceRecognitionResultListener 第一阶段继续保留:

void recognitionSuccess(User user);
void recognitionEnd();
void recognitionCancel();

这样 7 个页面的现有调用代码和业务回调不需要同步改写。

保留职责

移除职责

5.4 FaceRecognitionLauncher

统一 Launcher 不属于一期白屏修复的前置条件,现已作为二期兼容改造落地。

二期由 FaceRecognitionRequest 统一组装调用场景、金额、可选副屏、确认文案和弹框 Tag;由 FaceRecognitionLauncher 统一检查宿主生命周期、Fragment 状态保存和重复弹框,并创建唯一的 FaceRecognitionPop

Launcher 不保存 Activity、页面监听器或请求对象,不调用支付、核验和订单接口。7 个页面的原 FaceRecognitionResultListener 及其业务实现保持原位。

展示结果分为:

SHOWNALREADY_SHOWN 表示已有弹框负责后续收尾;其他结果由调用页面恢复自身支付门禁或副屏状态,不能造成按钮被永久锁定。

6. 生命周期与流程

6.1 应用启动

LaunchActivity 开始人脸启动流程
        
        
FaceRecognitionRuntime.initialize()
        
        ├─  READY:直接返回成功
        
        ├─ 正在 INITIALIZING:加入等待队列
        
        └─ 未初始化:调用现有 InitFaceModelManager
                          
                          ├─ 成功:状态改为 READY
                          └─ 失败:状态改为 FAILED

LaunchActivity 现有启动状态提示、许可证异常处理、本地人脸库流程和进入主页时机继续保留。Runtime 只接管模型初始化状态与并发控制,不接管页面跳转。

启动页自身原有的启动失败策略不在本次白屏修复范围内;本次必须移除的是业务弹框内部的重启和杀进程行为。

6.2 打开人脸识别弹框

  1. 业务页面组装 FaceRecognitionRequest,继续传入原结果监听器。

  2. Launcher 检查 Activity、Fragment 状态和相同 Tag 弹框。

  3. 满足展示条件时统一创建 FaceRecognitionPop;重复点击不重复创建。

  4. 弹框继续按当前方式显示布局并创建新的 FaceRecognitionSession

  5. Session 调用 Runtime 的 ensureReady()

  6. Runtime 已就绪时立即启动相机,不产生正常场景下的额外等待。

  7. Runtime 尚未就绪时,弹框保持当前外观,等待初始化结果;不能提前送相机帧进入识别。

  8. Runtime 失败时执行安全结束,不重启应用。

6.3 识别到用户

  1. Session 接收到有效 User

  2. 停止当前持续识别,防止重复匹配。

  3. 弹框按当前方式展示用户信息、金额、“重试”和“确认”。

  4. 用户点击“重试”,继续使用当前弹框重新开始识别。

  5. 用户点击“确认”,调用原 recognitionSuccess(User user)

  6. 业务页面继续执行原支付或核验逻辑。

  7. Session 停止并释放资源,弹框关闭。

6.4 识别超时

保持现有流程:

  1. 停止相机预览。

  2. 停止识别动画和倒计时。

  3. 调用原 recognitionEnd()

  4. 主页面执行当前提示和副屏隐藏逻辑。

  5. 自动关闭弹框。

  6. 不修改任何订单数据。

6.5 用户取消

保持现有流程:

  1. 停止 Session。

  2. 调用原 recognitionCancel()

  3. 主页面执行当前提示和副屏隐藏逻辑。

  4. 自动关闭弹框。

  5. 不修改任何订单数据。

6.6 模型或相机异常

替换当前重启行为:

  1. 记录错误阶段、错误码和错误信息。

  2. 停止倒计时、识别动画和相机预览。

  3. 通过现有结束通道完成页面及副屏收尾。

  4. 关闭弹框。

  5. 留在原业务页面。

  6. 不启动首页。

  7. 不结束进程。

  8. 不调用支付或核验接口。

  9. 不修改订单状态。

异常提示沿用项目当前提示方式,不新增新的确认步骤,不要求用户在弹框内处理异常。

7. 业务兼容设计

7.1 页面业务归属保持不变

Runtime、Session 和 Pop 均不得调用上述业务接口。

7.2 菜单消费订单不变量

人脸识别流程不得直接调用:

识别失败、超时、用户取消、模型异常或相机异常后,必须满足:

只有现有支付成功分支可以继续执行当前订单清空逻辑。支付失败分支保持当前订单,不能由人脸封装代为清空。

7.3 双屏兼容

MainActivity 仍然把副屏 AutoTexturePreviewView 通过 FaceRecognitionRequest 和 Launcher 传给 FaceRecognitionPop。Session 将它作为可选预览目标使用:

8. 错误处理和日志

8.1 错误分类

8.2 日志要求

日志至少包含:

日志不得记录完整人脸图像、特征值或不必要的敏感身份数据。

8.3 用户提示

9. 建议代码结构

app/src/main/java/com/cpt/cusumption/faceRecognition/
├── launcher/
│   ├── FaceRecognitionLauncher.java           # 统一展示入口和生命周期门禁
│   ├── FaceRecognitionLaunchGate.java         # 可测试的防重复展示规则
│   ├── FaceRecognitionRequest.java            # 弹框参数对象
│   └── FaceRecognitionScene.java              # 7 个调用场景枚举
├── model/
│   └── InitFaceModelManager.java              # 保留现有底层初始化能力
├── runtime/
│   ├── FaceRecognitionRuntime.java            # 进程级运行时
│   └── FaceRecognitionRuntimeState.java       # 初始化状态
└── session/
    ├── FaceRecognitionSession.java            # 单次识别会话
    └── FaceRecognitionSessionCallback.java    # Pop 内部回调

app/src/main/java/com/cpt/cusumption/view/
└── FaceRecognitionPop.java                    # 保持原公开接口和交互

第一阶段不为抽象而抽象。如果当前只有一种相机实现,不新增空泛的 FaceCameraAdapter;只有后续出现第二种真实相机实现时再抽取适配接口。

所有新增类和关键生命周期方法需要添加职责、边界和异常处理原因注释。超时时间、状态值、日志 Tag 等使用资源或命名常量,避免散落硬编码。

10. 分阶段实施计划

阶段一:建立回归基线

  1. 记录 7 个页面当前弹框入口、成功回调、结束回调和取消回调。

  2. 记录菜单消费的订单清空位置。

  3. 记录单屏、双屏的弹框与预览行为。

  4. 建立模型失败、超时、取消、成功确认的测试清单。

该阶段不修改业务代码。

阶段二:新增 FaceRecognitionRuntime

  1. 包装现有 InitFaceModelManager,不重写底层 SDK 初始化算法。

  2. 增加 UNINITIALIZEDINITIALIZINGREADYFAILED 状态。

  3. 增加单次初始化和等待回调队列。

  4. 使用 ApplicationContext,不保存页面引用。

  5. LaunchActivity 当前初始化结果映射到 Runtime,但保留原启动后续流程。

  6. 让弹框通过 ensureReady() 获取状态,不再直接重复初始化模型。

阶段三:新增 FaceRecognitionSession

  1. FaceRecognitionPop 抽出相机启动、帧回调和识别调用。

  2. 抽出倒计时、停止和重新识别逻辑。

  3. 增加会话序号和结果只分发一次保护。

  4. 统一停止异步任务、倒计时、动画和相机。

  5. 保持现有主、副屏预览参数。

阶段四:兼容式改造 FaceRecognitionPop

  1. 保留构造方法和 FaceRecognitionResultListener

  2. 保留现有布局、显示内容和按钮行为。

  3. 内部创建 Runtime 和 Session。

  4. onDestroyView() 中停止 Session。

  5. 删除弹框内 initModel()restartSelf() 和进程结束逻辑。

  6. 将 Session 结果映射回现有三个业务回调。

阶段五:菜单消费专项验证

  1. 先验证 MainActivity 单屏。

  2. 再验证 MainActivity 双屏。

  3. 验证已选菜品和自定义金额在所有识别失败场景中保留。

  4. 验证关闭弹框后再次点击支付可以重新识别。

  5. 验证成功支付只清空一次订单。

阶段六:其他页面回归

逐个验证其余 6 个页面,不修改现有业务接口:

  1. FreeActivity

  2. FreeNoKeyboardActivity

  3. QuotaActivity

  4. QuotaNoKeyboardActivity

  5. GuoxinFacePayActivity

  6. OrderVerifyActivity

阶段七:二期统一入口,已实施

一期完成后实施:

仍作为后续可选项:

可选项不得与本次白屏修复或二期 Launcher 迁移强制绑定。

11. 测试方案

11.1 Runtime 测试

11.2 Session 测试

11.3 菜单消费专项验收

11.4 Launcher 测试

11.5 七页面交互回归矩阵

每个页面都需要验证:

11.6 构建验证

使用项目兼容的 JDK 8 执行:

JAVA_HOME=/Users/liang/Library/Java/JavaVirtualMachines/corretto-1.8.0_482/Contents/Home \
sh ./gradlew :app:assembleDebug

如果项目已有相关单元测试,同时执行对应 Debug 单元测试任务。

12. 风险与控制措施

12.1 启动流程风险

风险:Runtime 接管初始化状态时改变 LaunchActivity 原有进入主页时机。

控制:保留 LaunchActivity 原回调顺序和后续数据库流程,Runtime 只统一状态与初始化并发,不接管导航。

12.2 业务回调风险

风险:抽取 Session 后改变成功、结束或取消回调时机。

控制:以现有 FaceRecognitionResultListener 为兼容契约,逐条对照当前时机,不新增业务回调依赖。

12.3 双屏风险

风险:异常关闭时主屏弹框关闭,但副屏区域未隐藏。

控制:异常仍通过现有结束通道通知页面收尾,同时 Session 停止副屏相机数据输出。

12.4 重复支付风险

风险:相机连续返回同一用户,导致多次执行成功回调。

控制:Session 使用最终结果只分发一次保护;确认按钮在提交后立即禁用,沿用当前关闭流程。

12.5 内存泄漏风险

风险:Runtime 保存 Activity,或 Session 在弹框关闭后继续持有预览 View。

控制:Runtime 只使用 ApplicationContext;Session 在 stop() 中清理预览、回调和任务引用。

12.6 Launcher 拒绝展示风险

风险:Activity 状态已保存时安全拒绝展示,但页面支付门禁或菜单副屏没有恢复。

控制:Launcher 返回结构化 LaunchResult;自由和定额消费页在没有活动弹框时恢复 paymentIndex,菜单消费隐藏副屏人脸区域。

13. 验收标准

以下条件全部满足才允许判定实施完成:

  1. 模型初始化失败时不再执行弹框内的应用重启或进程结束。

  2. 人脸识别异常后不出现白屏,不返回首页。

  3. 菜单消费识别失败、超时、取消、模型异常、相机异常后,订单信息完整保留。

  4. 用户关闭弹框后可以再次点击支付重新识别。

  5. 识别弹框布局、文案、动画、按钮和操作步骤与当前版本一致。

  6. 识别到用户后仍由用户点击“确认”才进入业务。

  7. 7 个页面原支付或核验接口、参数和结果处理不变。

  8. 单屏和双屏行为一致且资源正确释放。

  9. 不产生重复识别成功回调和重复支付。

  10. 7 个页面统一使用 Launcher,页面内不再直接构造 FaceRecognitionPop

  11. Launcher 内只有一个弹框创建位置,且不包含支付、核验或订单接口调用。

  12. Debug APK 构建通过,并完成 7 个页面的真机回归。

14. 交付和变更管理

15. 最终决策摘要

本次不通过修改业务流程规避问题,也不通过重做交互解决问题。最终方案是在现有 FaceRecognitionPop 外部契约不变的前提下:

最终验收基准为:除“异常时不再白屏、不再重启应用”这一修复结果外,当前业务、当前交互和当前页面状态规则均不改变。