23、人脸识别与后台服务:前台服务中使用生物识别、Service与Activity通信、后台认证限制

说实话,人脸识别做到后台服务里,是很多开发者容易踩坑的地方。

我刚开始做这个功能时,天真地以为在Service里直接调BiometricManager就行。结果呢?要么闪退,要么认证结果根本回不来。后来翻源码才发现,生物识别API的设计初衷就是跟Activity生命周期绑定的。

这一章,我就把我在项目中趟过的坑,一个一个说清楚。

为什么不能在后台Service直接调生物识别?

先看一个典型的错误写法:

// 错误示例:在Service中直接调用
public class AuthService extends Service {
    public void startAuth() {
        BiometricPrompt prompt = new BiometricPrompt(this, executor, callback);
        prompt.authenticate(new BiometricPrompt.PromptInfo.Builder()
            .setTitle("验证")
            .build());
    }
}

这段代码跑起来,大概率会报错:BiometricPrompt requires a valid Activity context

为什么?因为系统需要Activity来展示那个认证对话框。Service没有UI,系统没法弹窗。

⚠️ 注意: 千万不要在后台Service中直接创建BiometricPrompt实例。这会导致IllegalArgumentException,而且不同厂商的ROM表现还不一样——有的直接崩溃,有的静默失败。

前台服务 + 生物识别的正确姿势

我个人的习惯是:用前台Service保活,但把认证逻辑委托给Activity。

说白了,Service负责后台任务,Activity负责弹认证框。两者通过Binder通信。

第一步:创建前台Service

public class FaceAuthService extends Service {
    private final IBinder binder = new LocalBinder();
    private AuthCallback callback;

    public class LocalBinder extends Binder {
        FaceAuthService getService() {
            return FaceAuthService.this;
        }
    }

    @Override
    public void onCreate() {
        super.onCreate();
        // 启动前台通知
        Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID)
            .setContentTitle("人脸认证服务运行中")
            .setSmallIcon(R.drawable.ic_face)
            .build();
        startForeground(1, notification);
    }

    @Override
    public IBinder onBind(Intent intent) {
        return binder;
    }

    public void setCallback(AuthCallback callback) {
        this.callback = callback;
    }

    public void onAuthResult(boolean success) {
        if (callback != null) {
            callback.onResult(success);
        }
    }

    public interface AuthCallback {
        void onResult(boolean success);
    }
}

第二步:Activity绑定Service并触发认证

public class MainActivity extends AppCompatActivity {
    private FaceAuthService authService;
    private boolean bound = false;

    private ServiceConnection connection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            FaceAuthService.LocalBinder binder = (FaceAuthService.LocalBinder) service;
            authService = binder.getService();
            bound = true;
            // 设置回调
            authService.setCallback(result -> {
                runOnUiThread(() -> {
                    if (result) {
                        // 认证成功,通知Service继续后台任务
                    } else {
                        // 认证失败
                    }
                });
            });
            // 触发认证
            startBiometricAuth();
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            bound = false;
        }
    };

    private void startBiometricAuth() {
        BiometricPrompt prompt = new BiometricPrompt(this, 
            ContextCompat.getMainExecutor(this), 
            new BiometricPrompt.AuthenticationCallback() {
                @Override
                public void onAuthenticationSucceeded(BiometricPrompt.AuthenticationResult result) {
                    // 把结果传给Service
                    authService.onAuthResult(true);
                }

                @Override
                public void onAuthenticationFailed() {
                    authService.onAuthResult(false);
                }
            });

        prompt.authenticate(new BiometricPrompt.PromptInfo.Builder()
            .setTitle("验证身份")
            .setSubtitle("继续操作需要验证身份")
            .setNegativeButtonText("取消")
            .build());
    }
}
💡 小技巧: 我建议把BiometricPrompt的创建放在Activity的onResume里,避免因为屏幕旋转导致认证中断。曾经有个项目,用户一旋转手机认证就失败,排查了半天才发现是生命周期问题。

Service与Activity通信的三种方式

嗯,这里我整理了一张图,把常用的通信方式画出来了:

Service ↔ Activity 通信方式 FaceAuthService MainActivity ① Binder 直连 ② BroadcastReceiver ③ LiveData / Flow 📌 Binder:性能最好,适合高频通信。我一般用它传认证结果。 📌 Broadcast:松耦合,适合跨进程。但注意不要滥用,会拖慢系统。 📌 LiveData/Flow:生命周期感知,适合UI更新。但Service里要自己管理LifecycleOwner。

后台认证限制:Android 10+ 的硬性要求

从Android 10开始,Google对后台生物识别做了严格限制。说白了,就是不让App在用户不知情的情况下偷偷扫脸。

Android版本 后台认证限制 我的建议
Android 9 (API 28) 无明确限制 但建议提前适配前台Service
Android 10 (API 29) 后台App无法启动生物识别 必须使用前台Service + Activity组合
Android 11+ (API 30+) 加强限制,后台完全禁止 强制要求用户可见的UI
核心原则: 生物识别认证必须发生在用户可见的Activity中。Service只能做"认证后的后台任务",不能做"认证本身"。

避坑指南:我踩过的三个大坑

坑一:认证成功后Service被系统杀死

我曾经遇到一个情况:人脸认证成功了,但Service因为内存不足被系统回收了。结果用户白扫了一次脸。

解决方案:在onStartCommand里返回START_STICKY,并且把Service优先级调高。

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    // 返回START_STICKY,让系统在杀死后尝试重启
    return START_STICKY;
}

坑二:认证回调在后台线程,直接更新UI崩溃

BiometricPrompt的回调默认在后台线程。如果你直接在里面更新TextView,妥妥的崩溃。

记得用runOnUiThread或者Handler切到主线程。

坑三:多个Activity绑定同一个Service

如果你的App有多个界面都需要人脸认证,别让每个Activity都单独绑定Service。我建议用一个单例的ServiceManager来管理绑定状态,避免重复创建。

⚠️ 重要提醒: 前台Service的通知是必须的。Android 13开始,用户可以在通知栏直接关闭前台Service。所以你的认证逻辑要处理好"用户主动取消"的情况。

总结一下

人脸识别跟后台服务配合,说白了就三句话:

  • 认证必须在Activity里做,Service别掺和
  • 用Binder或Broadcast把认证结果传给Service
  • 前台Service保活,但别忘了通知栏提示

我个人习惯是把认证结果存在Service的成员变量里,Activity通过Binder随时查询。这样即使Activity重建了,认证状态也不会丢。

嗯,这一章的内容就到这儿。代码不多,但都是实战中提炼出来的经验。你照着这个思路做,应该能少走不少弯路。

公众号:蓝海资料掘金营,微信deep3321