15、指纹认证生命周期管理:Activity/Fragment生命周期绑定、取消认证、防止内存泄漏、屏幕旋转处理

指纹认证这东西,说白了就是个“一次性会话”。你调用 authenticate() 之后,系统会拉起一个系统级的认证对话框,然后等用户按指纹。但这里有个坑——这个会话的生命周期,跟你的 Activity 或 Fragment 是绑定的。如果不处理好,屏幕一转、页面一退,轻则回调不执行,重则内存泄漏、应用崩溃。

我个人习惯是把指纹认证的生命周期管理,当作一个“独立模块”来设计。别把它散落在 Activity 的各个生命周期回调里,那样后期维护起来真的很痛苦。

15.1 生命周期绑定的核心思路

先看一张图,理解一下整体流程:

指纹认证生命周期管理流程 Activity/Fragment onResume() → 创建认证实例 authenticate() 进行中 onPause() → cancelAuthentication() 认证成功/失败 → 清理资源

嗯,这里要注意:指纹认证的 BiometricPrompt 实例,必须跟 Activity 或 Fragment 的生命周期对齐。你想想看,如果用户在认证过程中按了 Home 键,Activity 进入 onPause(),这时候认证对话框还在吗?答案是——系统会帮你关掉对话框,但你的 AuthenticationCallback 可能还在后台挂着。

15.2 标准绑定写法(Activity 示例)

我建议把 BiometricPrompt 的创建和销毁,放在 onResume()onPause() 里。这样最安全。

public class FingerprintActivity extends AppCompatActivity {

    private BiometricPrompt biometricPrompt;
    private BiometricPrompt.PromptInfo promptInfo;
    private Executor executor;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_fingerprint);

        executor = ContextCompat.getMainExecutor(this);
        setupBiometricPrompt();
    }

    private void setupBiometricPrompt() {
        biometricPrompt = new BiometricPrompt(this, executor,
            new BiometricPrompt.AuthenticationCallback() {
                @Override
                public void onAuthenticationSucceeded(
                        BiometricPrompt.AuthenticationResult result) {
                    // 认证成功
                    handleAuthSuccess();
                }

                @Override
                public void onAuthenticationError(int errorCode,
                        CharSequence errString) {
                    // 认证错误(包括用户取消、系统取消等)
                    handleAuthError(errorCode, errString);
                }

                @Override
                public void onAuthenticationFailed() {
                    // 指纹不匹配
                    handleAuthFailed();
                }
            });

        promptInfo = new BiometricPrompt.PromptInfo.Builder()
                .setTitle("指纹验证")
                .setSubtitle("请按指纹")
                .setNegativeButtonText("取消")
                .build();
    }

    @Override
    protected void onResume() {
        super.onResume();
        // 页面可见时,可以开始认证
        // 但注意:不要在这里直接调用 authenticate()
        // 应该由用户点击按钮触发
    }

    @Override
    protected void onPause() {
        super.onPause();
        // 页面不可见时,立即取消认证
        cancelAuthentication();
    }

    public void startAuthentication() {
        if (biometricPrompt != null) {
            biometricPrompt.authenticate(promptInfo);
        }
    }

    private void cancelAuthentication() {
        if (biometricPrompt != null) {
            // 注意:cancel() 会触发 onAuthenticationError()
            // 错误码为 BiometricPrompt.ERROR_CANCELED
            biometricPrompt.cancelAuthentication();
        }
    }
}

核心原则:onPause() 里必须 cancel(),onResume() 里重新准备。别想着“用户只是按了 Home 键,回来还能继续用同一个会话”——系统不保证。

15.3 Fragment 里的生命周期绑定

Fragment 的情况稍微复杂一点。因为 Fragment 有 onPause()onStop(),还有 onDetach()。我个人习惯在 onPause() 里取消认证,在 onResume() 里重新初始化。

但有一个坑:Fragment 的 onPause() 可能在 Activity 的 onPause() 之前调用。所以如果你在 Fragment 里管理认证,一定要确保 cancelAuthentication() 不会被重复调用。

public class FingerprintFragment extends Fragment {

    private BiometricPrompt biometricPrompt;
    private boolean isAuthenticating = false;

    @Override
    public void onResume() {
        super.onResume();
        // 重新创建 BiometricPrompt 实例
        // 因为之前的实例可能已经被 cancel 了
        setupBiometricPrompt();
    }

    @Override
    public void onPause() {
        super.onPause();
        if (isAuthenticating) {
            cancelAuthentication();
        }
    }

    private void setupBiometricPrompt() {
        biometricPrompt = new BiometricPrompt(this,
            ContextCompat.getMainExecutor(requireContext()),
            new BiometricPrompt.AuthenticationCallback() {
                // ... 回调实现
            });
    }

    private void cancelAuthentication() {
        if (biometricPrompt != null) {
            biometricPrompt.cancelAuthentication();
            isAuthenticating = false;
        }
    }
}

注意:千万不要在 onDestroy() 里调用 cancelAuthentication()。因为 onDestroy() 调用时,Fragment 可能已经 detached,这时候调用 cancelAuthentication() 会抛出 IllegalStateException

15.4 防止内存泄漏

内存泄漏是 Android 开发的老大难问题。指纹认证这块,泄漏点主要在 Callback 持有外部引用

我曾经在一个项目里遇到过:AuthenticationCallback 里匿名内部类持有了 Activity 的引用,而 Activity 被销毁后,这个 Callback 还在后台等着回调。结果就是 Activity 泄漏了,OOM 警告天天弹。

解决办法其实很简单:

  • 使用弱引用:在 Callback 里用 WeakReference<Activity>WeakReference<Fragment>
  • 及时清理:在 onPause()onStop() 里把 biometricPrompt 置为 null
  • 避免匿名内部类:把 Callback 写成静态内部类,或者单独抽成一个类
private static class AuthCallback 
        extends BiometricPrompt.AuthenticationCallback {
    
    private final WeakReference<FingerprintActivity> activityRef;

    AuthCallback(FingerprintActivity activity) {
        this.activityRef = new WeakReference<>(activity);
    }

    @Override
    public void onAuthenticationSucceeded(
            BiometricPrompt.AuthenticationResult result) {
        FingerprintActivity activity = activityRef.get();
        if (activity != null && !activity.isFinishing()) {
            activity.handleAuthSuccess();
        }
    }
}

小技巧:onPause() 里调用 biometricPrompt = null; 之后,记得把 executor 也置空。虽然 ContextCompat.getMainExecutor() 返回的是单例,但显式置空是个好习惯。

15.5 屏幕旋转处理

屏幕旋转,说白了就是 Activity 重建。这时候指纹认证会话肯定断了。你想想看,系统对话框还在,但 Activity 已经重建了,回调往哪发?

处理方式有两种:

方案 做法 优缺点
方案一:禁止旋转 在 Manifest 里设置 android:screenOrientation="portrait" 简单粗暴,但用户体验差
方案二:保存状态 onSaveInstanceState() 里保存认证状态,重建后恢复 灵活,但需要处理状态恢复逻辑
方案三:使用 Fragment 把认证逻辑放在 setRetainInstance(true) 的 Fragment 里 Fragment 不随旋转重建,但已废弃,不推荐

我个人推荐方案二。虽然代码量多一点,但最可控。

private static final String KEY_AUTH_STATE = "auth_state";
private boolean isAuthInProgress = false;

@Override
protected void onSaveInstanceState(Bundle outState) {
    super.onSaveInstanceState(outState);
    outState.putBoolean(KEY_AUTH_STATE, isAuthInProgress);
}

@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
    super.onRestoreInstanceState(savedInstanceState);
    if (savedInstanceState != null) {
        boolean wasAuthInProgress = 
            savedInstanceState.getBoolean(KEY_AUTH_STATE, false);
        if (wasAuthInProgress) {
            // 旋转前正在认证,重新拉起对话框
            // 但注意:不要自动弹,最好让用户手动触发
            showAuthResumeDialog();
        }
    }
}

我的经验:屏幕旋转后不要自动重新弹认证对话框。用户可能只是不小心转了一下屏幕,你突然弹个指纹框,体验很糟糕。我一般会在界面上显示一个“继续验证”的按钮,让用户自己点。

15.6 完整生命周期管理清单

最后,我整理了一个检查清单。每次写指纹认证代码时,我都会对照着过一遍:

  • onCreate():初始化 BiometricPrompt 实例和 PromptInfo
  • onResume():重新创建 BiometricPrompt(如果之前被 cancel 了)
  • onPause():调用 cancelAuthentication(),置空引用
  • onSaveInstanceState():保存认证状态
  • onRestoreInstanceState():恢复认证状态,提示用户重新验证
  • Callback 实现:使用弱引用,避免泄漏
  • 屏幕旋转:不要自动弹框,让用户手动触发

嗯,做到这几点,指纹认证的生命周期管理基本就稳了。记住一句话:页面不可见时,认证必须取消;页面重建时,认证重新开始。别想着复用旧的会话,系统不给你这个保证。