现代智能手机,基本上都有人脸解锁功能,那他是怎么实现的哦?下面从代码角度来分析下他。

先上流程图

人脸解锁,都要先录入(这部分后面会出其他博客),再注册,再人脸解锁,响应,下面从代码角度来分析他。

先从锁屏部分的类KeyguardUpdateMonitor入手,下面是人脸服务注册方法。

private void startListeningForFace() {final int userId = getCurrentUser();final boolean unlockPossible = isUnlockWithFacePossible(userId);if (mFaceCancelSignal != null) {Log.e(TAG, "Cancellation signal is not null, high chance of bug in face auth lifecycle"+ " management. Face state: " + mFaceRunningState+ ", unlockPossible: " + unlockPossible);}if (mFaceRunningState == BIOMETRIC_STATE_CANCELLING) {setFaceRunningState(BIOMETRIC_STATE_CANCELLING_RESTARTING);return;} else if (mFaceRunningState == BIOMETRIC_STATE_CANCELLING_RESTARTING) {// Waiting for ERROR_CANCELED before requesting auth againreturn;}if (DEBUG) Log.v(TAG, "startListeningForFace(): " + mFaceRunningState);if (unlockPossible) {mFaceCancelSignal = new CancellationSignal();// This would need to be updated for multi-sensor devicesfinal boolean supportsFaceDetection = !mFaceSensorProperties.isEmpty()&& mFaceSensorProperties.get(0).supportsFaceDetection;mFaceAuthUserId = userId;if (isEncryptedOrLockdown(userId) && supportsFaceDetection) {mFaceManager.detectFace(mFaceCancelSignal, mFaceDetectionCallback, userId);} else {final boolean isBypassEnabled = mKeyguardBypassController != null&& mKeyguardBypassController.isBypassEnabled();mFaceManager.authenticate(null /* crypto */, mFaceCancelSignal,mFaceAuthenticationCallback, null /* handler */, userId, isBypassEnabled);}setFaceRunningState(BIOMETRIC_STATE_RUNNING);}}

第31行,人脸注册,这里要注意下变量mFaceAuthenticationCallback,这是回调接口对象,底层设别结果的回传信息,会通过这个变量对象告知用户人脸解锁成功或失败或错误,等等。

    @VisibleForTestingfinal FaceManager.AuthenticationCallback mFaceAuthenticationCallback= new FaceManager.AuthenticationCallback() {@Overridepublic void onAuthenticationFailed() {handleFaceAuthFailed();if (mKeyguardBypassController != null) {mKeyguardBypassController.setUserHasDeviceEntryIntent(false);}}@Overridepublic void onAuthenticationSucceeded(FaceManager.AuthenticationResult result) {Trace.beginSection("KeyguardUpdateMonitor#onAuthenticationSucceeded");handleFaceAuthenticated(result.getUserId(), result.isStrongBiometric());Trace.endSection();if (mKeyguardBypassController != null) {mKeyguardBypassController.setUserHasDeviceEntryIntent(false);}}@Overridepublic void onAuthenticationHelp(int helpMsgId, CharSequence helpString) {handleFaceHelp(helpMsgId, helpString.toString());}@Overridepublic void onAuthenticationError(int errMsgId, CharSequence errString) {handleFaceError(errMsgId, errString.toString());if (mKeyguardBypassController != null) {mKeyguardBypassController.setUserHasDeviceEntryIntent(false);}}@Overridepublic void onAuthenticationAcquired(int acquireInfo) {handleFaceAcquired(acquireInfo);}};

后面在设别结果回传的时候,再讨论。

回到代码mFaceManager#authenticate 部分,讨论下变量mFaceManager是如何定义的。

在KeyguardUpdateMonitor类的构造函数中定义的,如下,显然这是一个系统服务。

if (mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_FACE)) {mFaceManager = (FaceManager) context.getSystemService(Context.FACE_SERVICE);mFaceSensorProperties = mFaceManager.getSensorPropertiesInternal();}

搜索关键字Context.FACE_SERVICE,在SystemServiceRegistry类中实现了系统服务注册。

registerService(Context.FACE_SERVICE, FaceManager.class,new CachedServiceFetcher<FaceManager>() {@Overridepublic FaceManager createService(ContextImpl ctx)throws ServiceNotFoundException {final IBinder binder;if (ctx.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.O) {binder = ServiceManager.getServiceOrThrow(Context.FACE_SERVICE);} else {binder = ServiceManager.getService(Context.FACE_SERVICE);}IFaceService service = IFaceService.Stub.asInterface(binder);return new FaceManager(ctx.getOuterContext(), service);}});

注意第12,13行,会获得跨进程对象FaceService对象实例,然后绑定到FaceManager系统对象实例中,然后在mFaceManager#authenticate方法中,使用mService 变量,调用到FaceService对象实例下的authenticate方法。

 @RequiresPermission(USE_BIOMETRIC_INTERNAL)public void authenticate(@Nullable CryptoObject crypto, @Nullable CancellationSignal cancel,@NonNull AuthenticationCallback callback, @Nullable Handler handler, int userId,boolean isKeyguardBypassEnabled) {if (callback == null) {throw new IllegalArgumentException("Must supply an authentication callback");}if (cancel != null && cancel.isCanceled()) {Slog.w(TAG, "authentication already canceled");return;}if (mService != null) {try {useHandler(handler);mAuthenticationCallback = callback;mCryptoObject = crypto;final long operationId = crypto != null ? crypto.getOpId() : 0;Trace.beginSection("FaceManager#authenticate");final long authId = mService.authenticate(mToken, operationId, userId,mServiceReceiver, mContext.getOpPackageName(), isKeyguardBypassEnabled);if (cancel != null) {cancel.setOnCancelListener(new OnAuthenticationCancelListener(authId));}} catch (RemoteException e) {Slog.w(TAG, "Remote exception while authenticating: ", e);// Though this may not be a hardware issue, it will cause apps to give up or// try again later.callback.onAuthenticationError(FACE_ERROR_HW_UNAVAILABLE,getErrorString(mContext, FACE_ERROR_HW_UNAVAILABLE,0 /* vendorCode */));} finally {Trace.endSection();}}}

搜索IFaceService service = IFaceService.Stub.asInterface(binder);

可以判断出mService 就是跨进程对象实例FaceService

第22行,注意mServiceReceiver,这是一个回调接口对象,应该是跨进程的,后面会讲到。

来到FaceService#authenticate

@Override // Binder callpublic long authenticate(final IBinder token, final long operationId, int userId,final IFaceServiceReceiver receiver, final String opPackageName,boolean isKeyguardBypassEnabled) {Utils.checkPermission(getContext(), USE_BIOMETRIC_INTERNAL);// TODO(b/152413782): If the sensor supports face detect and the device is encrypted or//  lockdown, something wrong happened. See similar path in FingerprintService.final boolean restricted = false; // Face APIs are privatefinal int statsClient = Utils.isKeyguard(getContext(), opPackageName)? BiometricsProtoEnums.CLIENT_KEYGUARD: BiometricsProtoEnums.CLIENT_UNKNOWN;// Keyguard check must be done on the caller's binder identity, since it also checks// permission.final boolean isKeyguard = Utils.isKeyguard(getContext(), opPackageName);final Pair<Integer, ServiceProvider> provider = getSingleProvider();if (provider == null) {Slog.w(TAG, "Null provider for authenticate");return -1;}return provider.second.scheduleAuthenticate(provider.first, token, operationId, userId,0 /* cookie */,new ClientMonitorCallbackConverter(receiver), opPackageName, restricted,statsClient, isKeyguard, isKeyguardBypassEnabled);}

第25行,provider.second,看看是什么对象。

先从第19行开始看起

final Pair<Integer, ServiceProvider> provider = getSingleProvider();

@Nullableprivate Pair<Integer, ServiceProvider> getSingleProvider() {final List<FaceSensorPropertiesInternal> properties = getSensorProperties();if (properties.size() != 1) {Slog.e(TAG, "Multiple sensors found: " + properties.size());return null;}// Theoretically we can just return the first provider, but maybe this is easier to// understand.final int sensorId = properties.get(0).sensorId;for (ServiceProvider provider : mServiceProviders) {if (provider.containsSensor(sensorId)) {return new Pair<>(sensorId, provider);}}Slog.e(TAG, "Single sensor, but provider not found");return null;}

看看第12行的mServiceProviders集合变量在哪儿赋值的。

private void addHidlProviders(@NonNull List<FaceSensorPropertiesInternal> hidlSensors) {for (FaceSensorPropertiesInternal hidlSensor : hidlSensors) {mServiceProviders.add(new Face10(getContext(), hidlSensor, mLockoutResetDispatcher));}}

从第4行代码,我们可以判断出provider.second 就是Face10对象实例。

provider.second.scheduleAuthenticate就是Face10对象实例下的scheduleAuthenticate方法。

@Overridepublic void scheduleAuthenticate(int sensorId, @NonNull IBinder token, long operationId,int userId, int cookie, @NonNull ClientMonitorCallbackConverter receiver,@NonNull String opPackageName, long requestId, boolean restricted, int statsClient,boolean allowBackgroundAuthentication, boolean isKeyguardBypassEnabled) {mHandler.post(() -> {scheduleUpdateActiveUserWithoutHandler(userId);final boolean isStrongBiometric = Utils.isStrongBiometric(mSensorId);final FaceAuthenticationClient client = new FaceAuthenticationClient(mContext,mLazyDaemon, token, requestId, receiver, userId, operationId, restricted,opPackageName, cookie, false /* requireConfirmation */, mSensorId,isStrongBiometric, statsClient, mLockoutTracker, mUsageStats,allowBackgroundAuthentication, isKeyguardBypassEnabled);mScheduler.scheduleClientMonitor(client);});}

第15行,看看变量mScheduler在哪儿定义的。

@VisibleForTestingFace10(@NonNull Context context,@NonNull FaceSensorPropertiesInternal sensorProps,@NonNull LockoutResetDispatcher lockoutResetDispatcher,@NonNull BiometricScheduler scheduler) {mSensorProperties = sensorProps;mContext = context;mSensorId = sensorProps.sensorId;mScheduler = scheduler;mHandler = new Handler(Looper.getMainLooper());mUsageStats = new UsageStats(context);mAuthenticatorIds = new HashMap<>();mLazyDaemon = Face10.this::getDaemon;mLockoutTracker = new LockoutHalImpl();mHalResultController = new HalResultController(sensorProps.sensorId, context, mHandler,mScheduler, mLockoutTracker, lockoutResetDispatcher);mHalResultController.setCallback(() -> {mDaemon = null;mCurrentUserId = UserHandle.USER_NULL;});try {ActivityManager.getService().registerUserSwitchObserver(mUserSwitchObserver, TAG);} catch (RemoteException e) {Slog.e(TAG, "Unable to register user switch observer");}}

第9行,在Face10构造函数中赋值的,需要继续追踪,Face10对象实例在哪儿实现的,在

FaceService#addHidlProviders,会调用的,

 private void addHidlProviders(@NonNull List<FaceSensorPropertiesInternal> hidlSensors) {for (FaceSensorPropertiesInternal hidlSensor : hidlSensors) {mServiceProviders.add(new Face10(getContext(), hidlSensor, mLockoutResetDispatcher));}}
public Face10(@NonNull Context context, @NonNull FaceSensorPropertiesInternal sensorProps,@NonNull LockoutResetDispatcher lockoutResetDispatcher) {this(context, sensorProps, lockoutResetDispatcher,new BiometricScheduler(TAG, BiometricScheduler.SENSOR_TYPE_FACE,null /* gestureAvailabilityTracker */));}

搜索前面的代码mScheduler.scheduleClientMonitor(client)

可以确定mScheduler 就是第4行的new BiometricScheduler对象实例。

那继续往下看mScheduler.scheduleClientMonitor(client),实际上就是

public void scheduleClientMonitor(@NonNull BaseClientMonitor clientMonitor) {scheduleClientMonitor(clientMonitor, null /* clientFinishCallback */);}
  public void scheduleClientMonitor(@NonNull BaseClientMonitor clientMonitor,@Nullable BaseClientMonitor.Callback clientCallback) {// If the incoming operation should interrupt preceding clients, mark any interruptable// pending clients as canceling. Once they reach the head of the queue, the scheduler will// send ERROR_CANCELED and skip the operation.if (clientMonitor.interruptsPrecedingClients()) {for (Operation operation : mPendingOperations) {if (operation.mClientMonitor instanceof Interruptable&& operation.mState != Operation.STATE_WAITING_IN_QUEUE_CANCELING) {Slog.d(getTag(), "New client incoming, marking pending client as canceling: "+ operation.mClientMonitor);operation.mState = Operation.STATE_WAITING_IN_QUEUE_CANCELING;}}}mPendingOperations.add(new Operation(clientMonitor, clientCallback));Slog.d(getTag(), "[Added] " + clientMonitor+ ", new queue size: " + mPendingOperations.size());// If the new operation should interrupt preceding clients, and if the current operation is// cancellable, start the cancellation process.if (clientMonitor.interruptsPrecedingClients()&& mCurrentOperation != null&& mCurrentOperation.mClientMonitor instanceof Interruptable&& mCurrentOperation.mState == Operation.STATE_STARTED) {Slog.d(getTag(), "[Cancelling Interruptable]: " + mCurrentOperation);cancelInternal(mCurrentOperation);}startNextOperationIfIdle();}

第31行,继续往下看

{if (mCurrentOperation != null) {Slog.v(getTag(), "Not idle, current operation: " + mCurrentOperation);return;}if (mPendingOperations.isEmpty()) {Slog.d(getTag(), "No operations, returning to idle");return;}mCurrentOperation = mPendingOperations.poll();final BaseClientMonitor currentClient = mCurrentOperation.mClientMonitor;Slog.d(getTag(), "[Polled] " + mCurrentOperation);// If the operation at the front of the queue has been marked for cancellation, send// ERROR_CANCELED. No need to start this client.if (mCurrentOperation.mState == Operation.STATE_WAITING_IN_QUEUE_CANCELING) {Slog.d(getTag(), "[Now Cancelling] " + mCurrentOperation);if (!(currentClient instanceof Interruptable)) {throw new IllegalStateException("Mis-implemented client or scheduler, "+ "trying to cancel non-interruptable operation: " + mCurrentOperation);}final Interruptable interruptable = (Interruptable) currentClient;interruptable.cancelWithoutStarting(getInternalCallback());// Now we wait for the client to send its FinishCallback, which kicks off the next// operation.return;}if (mGestureAvailabilityDispatcher != null&& mCurrentOperation.mClientMonitor instanceof AcquisitionClient) {mGestureAvailabilityDispatcher.markSensorActive(mCurrentOperation.mClientMonitor.getSensorId(),true /* active */);}// Not all operations start immediately. BiometricPrompt waits for its operation// to arrive at the head of the queue, before pinging it to start.final boolean shouldStartNow = currentClient.getCookie() == 0;if (shouldStartNow) {if (mCurrentOperation.isUnstartableHalOperation()) {final HalClientMonitor<?> halClientMonitor =(HalClientMonitor<?>) mCurrentOperation.mClientMonitor;// Note down current length of queuefinal int pendingOperationsLength = mPendingOperations.size();final Operation lastOperation = mPendingOperations.peekLast();Slog.e(getTag(), "[Unable To Start] " + mCurrentOperation+ ". Last pending operation: " + lastOperation);// For current operations, 1) unableToStart, which notifies the caller-side, then// 2) notify operation's callback, to notify applicable system service that the// operation failed.halClientMonitor.unableToStart();if (mCurrentOperation.mClientCallback != null) {mCurrentOperation.mClientCallback.onClientFinished(mCurrentOperation.mClientMonitor, false /* success */);}// Then for each operation currently in the pending queue at the time of this// failure, do the same as above. Otherwise, it's possible that something like// setActiveUser fails, but then authenticate (for the wrong user) is invoked.for (int i = 0; i < pendingOperationsLength; i++) {final Operation operation = mPendingOperations.pollFirst();if (operation == null) {Slog.e(getTag(), "Null operation, index: " + i+ ", expected length: " + pendingOperationsLength);break;}if (operation.isHalOperation()) {((HalClientMonitor<?>) operation.mClientMonitor).unableToStart();}if (operation.mClientCallback != null) {operation.mClientCallback.onClientFinished(operation.mClientMonitor,false /* success */);}Slog.w(getTag(), "[Aborted Operation] " + operation);}// It's possible that during cleanup a new set of operations came in. We can try to// run these. A single request from the manager layer to the service layer may// actually be multiple operations (i.e. updateActiveUser + authenticate).mCurrentOperation = null;startNextOperationIfIdle();} else {Slog.d(getTag(), "[Starting] " + mCurrentOperation);currentClient.start(getInternalCallback());mCurrentOperation.mState = Operation.STATE_STARTED;}} else {try {mBiometricService.onReadyForAuthentication(currentClient.getCookie());} catch (RemoteException e) {Slog.e(getTag(), "Remote exception when contacting BiometricService", e);}Slog.d(getTag(), "Waiting for cookie before starting: " + mCurrentOperation);mCurrentOperation.mState = Operation.STATE_WAITING_FOR_COOKIE;}}

第87行,变量currentClient是哪个对象

搜索前面的代码,发现下面2行代码

mCurrentOperation = mPendingOperations.poll();

final BaseClientMonitor currentClient = mCurrentOperation.mClientMonitor

继续搜索

mPendingOperations.add(new Operation(clientMonitor, clientCallback));

可以确定clientMonitor 我们需要的。

scheduleClientMonitor方法中,有变量clientMonitor,可以确定是传递过来的

就是FaceAuthenticationClient 对象实例。因此currentClient.start就是调用FaceAuthenticationClient下的start方法,另外注意下第87行的start下的参数,应该后面在分析回调的时候,会讨论到。现在来看FaceAuthenticationClient下的start方法。

 @Overridepublic void start(@NonNull Callback callback) {super.start(callback);mState = STATE_STARTED;}

第3行,继续追踪父类AuthenticationClient

 /*** Start authentication*/@Overridepublic void start(@NonNull Callback callback) {super.start(callback);final @LockoutTracker.LockoutMode int lockoutMode =mLockoutTracker.getLockoutModeForUser(getTargetUserId());if (lockoutMode != LockoutTracker.LOCKOUT_NONE) {Slog.v(TAG, "In lockout mode(" + lockoutMode + ") ; disallowing authentication");int errorCode = lockoutMode == LockoutTracker.LOCKOUT_TIMED? BiometricConstants.BIOMETRIC_ERROR_LOCKOUT: BiometricConstants.BIOMETRIC_ERROR_LOCKOUT_PERMANENT;onError(errorCode, 0 /* vendorCode */);return;}if (mTaskStackListener != null) {mActivityTaskManager.registerTaskStackListener(mTaskStackListener);}Slog.d(TAG, "Requesting auth for " + getOwnerString());mStartTimeMs = System.currentTimeMillis();mAuthAttempted = true;startHalOperation();}

第27行,startHalOperation()最终调用的是FaceAuthenticationClient#startHalOperation()

@Overrideprotected void startHalOperation() {try {if (mSensorPrivacyManager != null&& mSensorPrivacyManager.isSensorPrivacyEnabled(SensorPrivacyManager.Sensors.CAMERA,getTargetUserId())) {onError(BiometricConstants.BIOMETRIC_ERROR_HW_UNAVAILABLE,0 /* vendorCode */);mCallback.onClientFinished(this, false /* success */);} else {mCancellationSignal = getFreshDaemon().authenticate(mOperationId);}} catch (RemoteException e) {Slog.e(TAG, "Remote exception when requesting auth", e);onError(BiometricFaceConstants.FACE_ERROR_HW_UNAVAILABLE, 0 /* vendorCode */);mCallback.onClientFinished(this, false /* success */);}}

第12行getFreshDaemon().authenticate(mOperationId);

getFreshDaemon()实际上是调用Face10#getFreshDaemon

    private synchronized IBiometricsFace getDaemon() {if (mTestHalEnabled) {final TestHal testHal = new TestHal(mContext, mSensorId);testHal.setCallback(mHalResultController);return testHal;}if (mDaemon != null) {return mDaemon;}Slog.d(TAG, "Daemon was null, reconnecting, current operation: "+ mScheduler.getCurrentClient());try {mDaemon = IBiometricsFace.getService();} catch (java.util.NoSuchElementException e) {// Service doesn't exist or cannot be opened.Slog.w(TAG, "NoSuchElementException", e);} catch (RemoteException e) {Slog.e(TAG, "Failed to get face HAL", e);}if (mDaemon == null) {Slog.w(TAG, "Face HAL not available");return null;}mDaemon.asBinder().linkToDeath(this, 0 /* flags */);// HAL ID for these HIDL versions are only used to determine if callbacks have been// successfully set.long halId = 0;try {halId = mDaemon.setCallback(mHalResultController).value;} catch (RemoteException e) {Slog.e(TAG, "Failed to set callback for face HAL", e);mDaemon = null;}Slog.d(TAG, "Face HAL ready, HAL ID: " + halId);if (halId != 0) {scheduleLoadAuthenticatorIds();scheduleInternalCleanup(ActivityManager.getCurrentUser(), null /* callback */);scheduleGetFeature(mSensorId, new Binder(),ActivityManager.getCurrentUser(),BiometricFaceConstants.FEATURE_REQUIRE_ATTENTION, null,mContext.getOpPackageName());} else {Slog.e(TAG, "Unable to set callback");mDaemon = null;}return mDaemon;}

第35行后面,是涉及到人脸设别后的结果回调相关。后面会介绍,现在继续第16行。是跨进程获取BiometricsFace.cpp对象实例。然后继续其BiometricsFace#authenticate方法。

Return<Status> BiometricsFace::authenticate(uint64_t /* operationId */) {mClientCallback->onError(kDeviceId, mUserId, FaceError::HW_UNAVAILABLE, 0 /* vendorCode */);return Status::OK;
}

现在,基本上人脸注册部分讲完了,继续往下说下设别结果如何回传的

搜索下,前面的代码mDaemon.setCallback(mHalResultController)

mHalResultController这是一个回调接口对象,设别结果从他往上回传。

mHalResultController = new HalResultController(sensorProps.sensorId, context, mHandler,mScheduler, mLockoutTracker, lockoutResetDispatcher);

继续往下看这个类,实际上是个跨进程的回调接口对象

public static class HalResultController extends IBiometricsFaceClientCallback.Stub {/*** Interface to sends results to the HalResultController's owner.*/public interface Callback {/*** Invoked when the HAL sends ERROR_HW_UNAVAILABLE.*/void onHardwareUnavailable();}private final int mSensorId;@NonNull private final Context mContext;@NonNull private final Handler mHandler;@NonNull private final BiometricScheduler mScheduler;@Nullable private Callback mCallback;@NonNull private final LockoutHalImpl mLockoutTracker;@NonNull private final LockoutResetDispatcher mLockoutResetDispatcher;HalResultController(int sensorId, @NonNull Context context, @NonNull Handler handler,@NonNull BiometricScheduler scheduler, @NonNull LockoutHalImpl lockoutTracker,@NonNull LockoutResetDispatcher lockoutResetDispatcher) {mSensorId = sensorId;mContext = context;mHandler = handler;mScheduler = scheduler;mLockoutTracker = lockoutTracker;mLockoutResetDispatcher = lockoutResetDispatcher;}public void setCallback(@Nullable Callback callback) {mCallback = callback;}@Overridepublic void onEnrollResult(long deviceId, int faceId, int userId, int remaining) {mHandler.post(() -> {final CharSequence name = FaceUtils.getLegacyInstance(mSensorId).getUniqueName(mContext, userId);final Face face = new Face(name, faceId, deviceId);final BaseClientMonitor client = mScheduler.getCurrentClient();if (!(client instanceof FaceEnrollClient)) {Slog.e(TAG, "onEnrollResult for non-enroll client: "+ Utils.getClientName(client));return;}final FaceEnrollClient enrollClient = (FaceEnrollClient) client;enrollClient.onEnrollResult(face, remaining);});}@Overridepublic void onAuthenticated(long deviceId, int faceId, int userId,ArrayList<Byte> token) {mHandler.post(() -> {final BaseClientMonitor client = mScheduler.getCurrentClient();if (!(client instanceof AuthenticationConsumer)) {Slog.e(TAG, "onAuthenticated for non-authentication consumer: "+ Utils.getClientName(client));return;}final AuthenticationConsumer authenticationConsumer =(AuthenticationConsumer) client;final boolean authenticated = faceId != 0;final Face face = new Face("", faceId, deviceId);authenticationConsumer.onAuthenticated(face, authenticated, token);});}@Overridepublic void onAcquired(long deviceId, int userId, int acquiredInfo,int vendorCode) {mHandler.post(() -> {final BaseClientMonitor client = mScheduler.getCurrentClient();if (!(client instanceof AcquisitionClient)) {Slog.e(TAG, "onAcquired for non-acquire client: "+ Utils.getClientName(client));return;}final AcquisitionClient<?> acquisitionClient =(AcquisitionClient<?>) client;acquisitionClient.onAcquired(acquiredInfo, vendorCode);});}@Overridepublic void onError(long deviceId, int userId, int error, int vendorCode) {mHandler.post(() -> {final BaseClientMonitor client = mScheduler.getCurrentClient();Slog.d(TAG, "handleError"+ ", client: " + (client != null ? client.getOwnerString() : null)+ ", error: " + error+ ", vendorCode: " + vendorCode);if (!(client instanceof ErrorConsumer)) {Slog.e(TAG, "onError for non-error consumer: " + Utils.getClientName(client));return;}final ErrorConsumer errorConsumer = (ErrorConsumer) client;errorConsumer.onError(error, vendorCode);if (error == BiometricConstants.BIOMETRIC_ERROR_HW_UNAVAILABLE) {Slog.e(TAG, "Got ERROR_HW_UNAVAILABLE");if (mCallback != null) {mCallback.onHardwareUnavailable();}}});}@Overridepublic void onRemoved(long deviceId, ArrayList<Integer> removed, int userId) {mHandler.post(() -> {final BaseClientMonitor client = mScheduler.getCurrentClient();if (!(client instanceof RemovalConsumer)) {Slog.e(TAG, "onRemoved for non-removal consumer: "+ Utils.getClientName(client));return;}final RemovalConsumer removalConsumer = (RemovalConsumer) client;if (!removed.isEmpty()) {// Convert to old fingerprint-like behavior, where remove() receives// one removal at a time. This way, remove can share some more common code.for (int i = 0; i < removed.size(); i++) {final int id = removed.get(i);final Face face = new Face("", id, deviceId);final int remaining = removed.size() - i - 1;Slog.d(TAG, "Removed, faceId: " + id + ", remaining: " + remaining);removalConsumer.onRemoved(face, remaining);}} else {removalConsumer.onRemoved(null, 0 /* remaining */);}Settings.Secure.putIntForUser(mContext.getContentResolver(),Settings.Secure.FACE_UNLOCK_RE_ENROLL, 0, UserHandle.USER_CURRENT);});}@Overridepublic void onEnumerate(long deviceId, ArrayList<Integer> faceIds, int userId) {mHandler.post(() -> {final BaseClientMonitor client = mScheduler.getCurrentClient();if (!(client instanceof EnumerateConsumer)) {Slog.e(TAG, "onEnumerate for non-enumerate consumer: "+ Utils.getClientName(client));return;}final EnumerateConsumer enumerateConsumer = (EnumerateConsumer) client;if (!faceIds.isEmpty()) {// Convert to old fingerprint-like behavior, where enumerate() receives one// template at a time. This way, enumerate can share some more common code.for (int i = 0; i < faceIds.size(); i++) {final Face face = new Face("", faceIds.get(i), deviceId);enumerateConsumer.onEnumerationResult(face, faceIds.size() - i - 1);}} else {// For face, the HIDL contract is to receive an empty list when there are no// templates enrolled. Send a null identifier since we don't consume them// anywhere, and send remaining == 0 so this code can be shared with Face@1.1enumerateConsumer.onEnumerationResult(null /* identifier */, 0);}});}@Overridepublic void onLockoutChanged(long duration) {mHandler.post(() -> {Slog.d(TAG, "onLockoutChanged: " + duration);final @LockoutTracker.LockoutMode int lockoutMode;if (duration == 0) {lockoutMode = LockoutTracker.LOCKOUT_NONE;} else if (duration == -1 || duration == Long.MAX_VALUE) {lockoutMode = LockoutTracker.LOCKOUT_PERMANENT;} else {lockoutMode = LockoutTracker.LOCKOUT_TIMED;}mLockoutTracker.setCurrentUserLockoutMode(lockoutMode);if (duration == 0) {mLockoutResetDispatcher.notifyLockoutResetCallbacks(mSensorId);}});}}

第55和69行,设别结果回传到这里。

搜索mScheduler.scheduleClientMonitor(client)。

最终能确定第69行的client就是FaceAuthenticationClient对象实例

然后调用其下的onAuthenticated方法,回传到这里

@Overridepublic void onAuthenticated(BiometricAuthenticator.Identifier identifier,boolean authenticated, ArrayList<Byte> token) {super.onAuthenticated(identifier, authenticated, token);mState = STATE_STOPPED;mUsageStats.addEvent(new UsageStats.AuthenticationEvent(getStartTimeMs(),System.currentTimeMillis() - getStartTimeMs() /* latency */,authenticated,0 /* error */,0 /* vendorError */,getTargetUserId()));}

第4行,看其父类的方法

@Overridepublic void onAuthenticated(BiometricAuthenticator.Identifier identifier,boolean authenticated, ArrayList<Byte> hardwareAuthToken) {super.logOnAuthenticated(getContext(), authenticated, mRequireConfirmation,getTargetUserId(), isBiometricPrompt());final ClientMonitorCallbackConverter listener = getListener();if (DEBUG) Slog.v(TAG, "onAuthenticated(" + authenticated + ")"+ ", ID:" + identifier.getBiometricId()+ ", Owner: " + getOwnerString()+ ", isBP: " + isBiometricPrompt()+ ", listener: " + listener+ ", requireConfirmation: " + mRequireConfirmation+ ", user: " + getTargetUserId()+ ", clientMonitor: " + toString());final PerformanceTracker pm = PerformanceTracker.getInstanceForSensorId(getSensorId());if (isCryptoOperation()) {pm.incrementCryptoAuthForUser(getTargetUserId(), authenticated);} else {pm.incrementAuthForUser(getTargetUserId(), authenticated);}if (mAllowBackgroundAuthentication) {Slog.w(TAG, "Allowing background authentication,"+ " this is allowed only for platform or test invocations");}// Ensure authentication only succeeds if the client activity is on top.boolean isBackgroundAuth = false;if (!mAllowBackgroundAuthentication && authenticated&& !Utils.isKeyguard(getContext(), getOwnerString())&& !Utils.isSystem(getContext(), getOwnerString())) {final List<ActivityManager.RunningTaskInfo> tasks =mActivityTaskManager.getTasks(1);if (tasks == null || tasks.isEmpty()) {Slog.e(TAG, "No running tasks reported");isBackgroundAuth = true;} else {final ComponentName topActivity = tasks.get(0).topActivity;if (topActivity == null) {Slog.e(TAG, "Unable to get top activity");isBackgroundAuth = true;} else {final String topPackage = topActivity.getPackageName();if (!topPackage.contentEquals(getOwnerString())) {Slog.e(TAG, "Background authentication detected, top: " + topPackage+ ", client: " + getOwnerString());isBackgroundAuth = true;}}}}// Fail authentication if we can't confirm the client activity is on top.if (isBackgroundAuth) {Slog.e(TAG, "Failing possible background authentication");authenticated = false;// SafetyNet logging for exploitation attempts of b/159249069.final ApplicationInfo appInfo = getContext().getApplicationInfo();EventLog.writeEvent(0x534e4554, "159249069", appInfo != null ? appInfo.uid : -1,"Attempted background authentication");}if (authenticated) {// SafetyNet logging for b/159249069 if constraint is violated.if (isBackgroundAuth) {final ApplicationInfo appInfo = getContext().getApplicationInfo();EventLog.writeEvent(0x534e4554, "159249069", appInfo != null ? appInfo.uid : -1,"Successful background authentication!");}markAlreadyDone();if (mTaskStackListener != null) {mActivityTaskManager.unregisterTaskStackListener(mTaskStackListener);}final byte[] byteToken = new byte[hardwareAuthToken.size()];for (int i = 0; i < hardwareAuthToken.size(); i++) {byteToken[i] = hardwareAuthToken.get(i);}if (mIsStrongBiometric) {mBiometricManager.resetLockoutTimeBound(getToken(),getContext().getOpPackageName(),getSensorId(), getTargetUserId(), byteToken);}final CoexCoordinator coordinator = CoexCoordinator.getInstance();coordinator.onAuthenticationSucceeded(SystemClock.uptimeMillis(), this,new CoexCoordinator.Callback() {@Overridepublic void sendAuthenticationResult(boolean addAuthTokenIfStrong) {if (addAuthTokenIfStrong && mIsStrongBiometric) {final int result = KeyStore.getInstance().addAuthToken(byteToken);Slog.d(TAG, "addAuthToken: " + result);} else {Slog.d(TAG, "Skipping addAuthToken");}if (listener != null) {try {// Explicitly have if/else here to make it super obvious in case the// code is touched in the future.if (!mIsRestricted) {listener.onAuthenticationSucceeded(getSensorId(),identifier,byteToken,getTargetUserId(),mIsStrongBiometric);} else {listener.onAuthenticationSucceeded(getSensorId(),null /* identifier */,byteToken,getTargetUserId(),mIsStrongBiometric);}} catch (RemoteException e) {Slog.e(TAG, "Unable to notify listener", e);}} else {Slog.w(TAG, "Client not listening");}}@Overridepublic void sendHapticFeedback() {if (listener != null && mShouldVibrate) {vibrateSuccess();}}@Overridepublic void handleLifecycleAfterAuth() {AuthenticationClient.this.handleLifecycleAfterAuth(true /* authenticated */);}@Overridepublic void sendAuthenticationCanceled() {sendCancelOnly(listener);}});} else {// Allow system-defined limit of number of attempts before giving upfinal @LockoutTracker.LockoutMode int lockoutMode =handleFailedAttempt(getTargetUserId());if (lockoutMode != LockoutTracker.LOCKOUT_NONE) {markAlreadyDone();}final CoexCoordinator coordinator = CoexCoordinator.getInstance();coordinator.onAuthenticationRejected(SystemClock.uptimeMillis(), this, lockoutMode,new CoexCoordinator.Callback() {@Overridepublic void sendAuthenticationResult(boolean addAuthTokenIfStrong) {if (listener != null) {try {listener.onAuthenticationFailed(getSensorId());} catch (RemoteException e) {Slog.e(TAG, "Unable to notify listener", e);}}}@Overridepublic void sendHapticFeedback() {if (listener != null && mShouldVibrate) {vibrateError();}}@Overridepublic void handleLifecycleAfterAuth() {AuthenticationClient.this.handleLifecycleAfterAuth(false /* authenticated */);}@Overridepublic void sendAuthenticationCanceled() {sendCancelOnly(listener);}});}}

然后回传到第109行,listener.onAuthenticationSucceeded

看看listener对象实例是什么

FaceService#authenticate方法中,即new ClientMonitorCallbackConverter(receiver)对象实例

所以,listener.onAuthenticationSucceeded调用的是ClientMonitorCallbackConverter#onAuthenticationSucceeded,代码如下

public class ClientMonitorCallbackConverter {private IBiometricSensorReceiver mSensorReceiver; // BiometricServiceprivate IFaceServiceReceiver mFaceServiceReceiver; // FaceManagerprivate IFingerprintServiceReceiver mFingerprintServiceReceiver; // FingerprintManagerpublic ClientMonitorCallbackConverter(IBiometricSensorReceiver sensorReceiver) {mSensorReceiver = sensorReceiver;}public ClientMonitorCallbackConverter(IFaceServiceReceiver faceServiceReceiver) {mFaceServiceReceiver = faceServiceReceiver;}public ClientMonitorCallbackConverter(IFingerprintServiceReceiver fingerprintServiceReceiver) {mFingerprintServiceReceiver = fingerprintServiceReceiver;}// The following apply to all clientsvoid onAcquired(int sensorId, int acquiredInfo, int vendorCode) throws RemoteException {if (mSensorReceiver != null) {mSensorReceiver.onAcquired(sensorId, acquiredInfo, vendorCode);} else if (mFaceServiceReceiver != null) {mFaceServiceReceiver.onAcquired(acquiredInfo, vendorCode);} else if (mFingerprintServiceReceiver != null) {mFingerprintServiceReceiver.onAcquired(acquiredInfo, vendorCode);}}void onAuthenticationSucceeded(int sensorId, BiometricAuthenticator.Identifier identifier,byte[] token, int userId, boolean isStrongBiometric) throws RemoteException {if (mSensorReceiver != null) {mSensorReceiver.onAuthenticationSucceeded(sensorId, token);} else if (mFaceServiceReceiver != null) {mFaceServiceReceiver.onAuthenticationSucceeded((Face) identifier, userId,isStrongBiometric);} else if (mFingerprintServiceReceiver != null) {mFingerprintServiceReceiver.onAuthenticationSucceeded((Fingerprint) identifier, userId,isStrongBiometric);}}void onAuthenticationFailed(int sensorId) throws RemoteException {if (mSensorReceiver != null) {mSensorReceiver.onAuthenticationFailed(sensorId);} else if (mFaceServiceReceiver != null) {mFaceServiceReceiver.onAuthenticationFailed();} else if (mFingerprintServiceReceiver != null) {mFingerprintServiceReceiver.onAuthenticationFailed();}}public void onError(int sensorId, int cookie, int error, int vendorCode)throws RemoteException {if (mSensorReceiver != null) {mSensorReceiver.onError(sensorId, cookie, error, vendorCode);} else if (mFaceServiceReceiver != null) {mFaceServiceReceiver.onError(error, vendorCode);} else if (mFingerprintServiceReceiver != null) {mFingerprintServiceReceiver.onError(error, vendorCode);}}// The following only apply to IFingerprintServiceReceiver and IFaceServiceReceiverpublic void onDetected(int sensorId, int userId, boolean isStrongBiometric)throws RemoteException {if (mFaceServiceReceiver != null) {mFaceServiceReceiver.onFaceDetected(sensorId, userId, isStrongBiometric);} else if (mFingerprintServiceReceiver != null) {mFingerprintServiceReceiver.onFingerprintDetected(sensorId, userId, isStrongBiometric);}}void onEnrollResult(BiometricAuthenticator.Identifier identifier, int remaining)throws RemoteException {if (mFaceServiceReceiver != null) {mFaceServiceReceiver.onEnrollResult((Face) identifier, remaining);} else if (mFingerprintServiceReceiver != null) {mFingerprintServiceReceiver.onEnrollResult((Fingerprint) identifier, remaining);}}void onRemoved(BiometricAuthenticator.Identifier identifier, int remaining)throws RemoteException {if (mFaceServiceReceiver != null) {mFaceServiceReceiver.onRemoved((Face) identifier, remaining);} else if (mFingerprintServiceReceiver != null) {mFingerprintServiceReceiver.onRemoved((Fingerprint) identifier, remaining);}}/** Called when a challenged has been generated. */public void onChallengeGenerated(int sensorId, int userId, long challenge)throws RemoteException {if (mFaceServiceReceiver != null) {mFaceServiceReceiver.onChallengeGenerated(sensorId, userId, challenge);} else if (mFingerprintServiceReceiver != null) {mFingerprintServiceReceiver.onChallengeGenerated(sensorId, userId, challenge);}}public void onFeatureSet(boolean success, int feature) throws RemoteException {if (mFaceServiceReceiver != null) {mFaceServiceReceiver.onFeatureSet(success, feature);}}public void onFeatureGet(boolean success, int[] features, boolean[] featureState)throws RemoteException {if (mFaceServiceReceiver != null) {mFaceServiceReceiver.onFeatureGet(success, features, featureState);}}// Fingerprint-specific callbacks for FingerprintManager onlypublic void onUdfpsPointerDown(int sensorId) throws RemoteException {if (mFingerprintServiceReceiver != null) {mFingerprintServiceReceiver.onUdfpsPointerDown(sensorId);}}public void onUdfpsPointerUp(int sensorId) throws RemoteException {if (mFingerprintServiceReceiver != null) {mFingerprintServiceReceiver.onUdfpsPointerUp(sensorId);}}// Face-specific callbacks for FaceManager only/*** Called each time a new frame is received during face authentication.** @param frame Information about the current frame.** @throws RemoteException If the binder call to {@link IFaceServiceReceiver} fails.*/public void onAuthenticationFrame(@NonNull FaceAuthenticationFrame frame)throws RemoteException {if (mFaceServiceReceiver != null) {mFaceServiceReceiver.onAuthenticationFrame(frame);}}/*** Called each time a new frame is received during face enrollment.** @param frame Information about the current frame.** @throws RemoteException If the binder call to {@link IFaceServiceReceiver} fails.*/public void onEnrollmentFrame(@NonNull FaceEnrollFrame frame) throws RemoteException {if (mFaceServiceReceiver != null) {mFaceServiceReceiver.onEnrollmentFrame(frame);}}
}

第35行的mFaceServiceReceiver 就是reveiever对象实例。其对应的源码在FaceManager.java下。

程序回调到第14行的onAuthenticationSucceeded

private final IFaceServiceReceiver mServiceReceiver = new IFaceServiceReceiver.Stub() {@Override // binder callpublic void onEnrollResult(Face face, int remaining) {mHandler.obtainMessage(MSG_ENROLL_RESULT, remaining, 0, face).sendToTarget();}@Override // binder callpublic void onAcquired(int acquireInfo, int vendorCode) {mHandler.obtainMessage(MSG_ACQUIRED, acquireInfo, vendorCode).sendToTarget();}@Override // binder callpublic void onAuthenticationSucceeded(Face face, int userId, boolean isStrongBiometric) {mHandler.obtainMessage(MSG_AUTHENTICATION_SUCCEEDED, userId,isStrongBiometric ? 1 : 0, face).sendToTarget();}@Override // binder callpublic void onFaceDetected(int sensorId, int userId, boolean isStrongBiometric) {mHandler.obtainMessage(MSG_FACE_DETECTED, sensorId, userId, isStrongBiometric).sendToTarget();}@Override // binder callpublic void onAuthenticationFailed() {mHandler.obtainMessage(MSG_AUTHENTICATION_FAILED).sendToTarget();}@Override // binder callpublic void onError(int error, int vendorCode) {mHandler.obtainMessage(MSG_ERROR, error, vendorCode).sendToTarget();}@Override // binder callpublic void onRemoved(Face face, int remaining) {mHandler.obtainMessage(MSG_REMOVED, remaining, 0, face).sendToTarget();}@Overridepublic void onFeatureSet(boolean success, int feature) {mHandler.obtainMessage(MSG_SET_FEATURE_COMPLETED, feature, 0, success).sendToTarget();}@Overridepublic void onFeatureGet(boolean success, int[] features, boolean[] featureState) {SomeArgs args = SomeArgs.obtain();args.arg1 = success;args.arg2 = features;args.arg3 = featureState;mHandler.obtainMessage(MSG_GET_FEATURE_COMPLETED, args).sendToTarget();}@Overridepublic void onChallengeGenerated(int sensorId, int userId, long challenge) {mHandler.obtainMessage(MSG_CHALLENGE_GENERATED, sensorId, userId, challenge).sendToTarget();}@Overridepublic void onAuthenticationFrame(FaceAuthenticationFrame frame) {mHandler.obtainMessage(MSG_AUTHENTICATION_FRAME, frame).sendToTarget();}@Overridepublic void onEnrollmentFrame(FaceEnrollFrame frame) {mHandler.obtainMessage(MSG_ENROLLMENT_FRAME, frame).sendToTarget();}}

第15行,通过mHandler跳转到

 case MSG_AUTHENTICATION_SUCCEEDED:sendAuthenticatedSucceeded((Face) msg.obj, msg.arg1 /* userId */,msg.arg2 == 1 /* isStrongBiometric */);break;
private void sendAuthenticatedSucceeded(Face face, int userId, boolean isStrongBiometric) {if (mAuthenticationCallback != null) {final AuthenticationResult result =new AuthenticationResult(mCryptoObject, face, userId, isStrongBiometric);mAuthenticationCallback.onAuthenticationSucceeded(result);}}

这里的mAuthenticationCallback就是文章开头部分有介绍,搜索

mAuthenticationCallback = callback

我们继续追踪callback变量,他实际上就是KeyguardUpdateMonitor#startListeningForFace方法中传入了这个对象。其对应的变量是mFaceAuthenticationCallback。

然后找到其对应的方法onAuthenticationSucceeded,即设别成功告知了上层用户。

人脸注册,解锁,响应,一网打尽相关推荐

  1. 人脸注册源码faceregiste

    人脸注册: using System; using System.Collections.Generic; using System.ComponentModel; using System.Data ...

  2. java 百度账号注册界面_基于百度AI使用H5实现调用摄像头进行人脸注册、人脸搜索功能(Java)...

    人脸注册.人脸搜索使用百度AI接口.不支持H5活体检测(需要活体检测请参考百度AI-H5活体检测) 只是为了演示.所以是IP.最好用火狐浏览器访问.谷歌提示异常作者就不专门修改了.大家可以直接下载源码 ...

  3. 人脸识别解锁能用照片绕过?华为、三星、小米、HTC等均上榜

    荷兰消费者协会(Consumentenbond)于近日通过一篇题为<Gezichtsherkenning op smartphone niet altijd veilig(智能手机上的人脸识别并 ...

  4. 人脸识别系统_人脸注册

    基于上次的人脸检测后,一直纠结人脸注册,照片存放方式,我想到了两种方式,1.数据库存照片存放的路径,2.数据库存放照片的二进制码.但是针对我的毕业设计我想要是存路径的话,万一一不小心图片删除了,岂不是 ...

  5. 华为畅享max有没有人脸识别_华为畅享Z有指纹识别吗?支持人脸识别解锁吗

    指纹识别可以说是近几年来智能手机的标配,而且也有很多机型采用了人脸识识别和指纹识别的双重解锁方案.那么华为畅享Z有指纹识别吗?支持人脸识别解锁吗.下面小编就来为大家详细解答一下,一起来看看吧! 华为畅 ...

  6. “人脸识别“解锁,到底安全不安全?

    有句话叫做"没有人两眼之间的距离是一样的". 在武侠小说中,高手可以通过两眼之间的距离,识破他人的易容术. 在现实生活中,手机可以通过检索你的面部,识别手机前的人是不是你.人的面部 ...

  7. 使用虹软SDK实现离线人脸注册,人脸登录(H5-JS前端,java后台)

    前言: 一开始找人脸识别的第三方接口,选择了百度,就是发请求给百度的接口,解析人家返回的数据. 但是这样的话,如果没有网络,或者没有外网.程序在局域网中使用的时候,自然就gg了. 人脸识别嘛,大家了解 ...

  8. 微信h5页面实现人脸注册和登陆

    工作中总会遇到一些麻烦的问题,有问题不要怕解决就好了,前段时间要实现微信h5页面人脸注册/登录,本以为很简单的一个东西,没想到居然这么麻烦,写个文章记录下过程和遇到的问题及解决办法: 需求:①人脸注册 ...

  9. 【python】BaiDuAI-人脸检测、人脸搜索、人脸注册

    文章目录 一.打开摄像头 二.获取摄像头捕捉的一帧图片,保存下来并进行人脸检测 三.建立人脸库 四.人脸搜索 五.人脸注册 一.打开摄像头 1.初始化打开摄像头按钮 # 初始uidef init_ui ...

最新文章

  1. 一台25万公里卡罗拉的返老还童记
  2. 完成OSS.Http底层HttpClient重构封装 支持标准库
  3. oem是代工还是贴牌_食用油OEM贴牌代工业务要注意哪些问题?
  4. Struts2教程9:实现自已的拦截器
  5. 揭秘Product Hunt怎样运用邮件崛起
  6. Tesseract-OCR图片识别为文字
  7. java cpu利用率上不去_Java 面试突击之 Java 并发知识基础 amp; 进阶考点全解析
  8. 涂威威:第四范式经验与思考分享
  9. mysql计算经纬度亮点之间的距离
  10. 高校科研管理系统源代码_高校科研信息管理系统
  11. 柯尼卡美能达一体机 扫描文件,不是全彩的,就首页和尾页是彩色,中间黑白
  12. TPYBoard MircoPython 外接SPI AD7705
  13. python删除图片文字_ps去掉图片上的文字的6种方法
  14. 【Python】与或非的符号表示
  15. c# 3D图形处理库
  16. 广告代码(弹窗和富媒体)
  17. JAVA——实现求s=a+aa+aaa+aaaa+aa...a的值
  18. 哈勃(Hubble)太空望远镜:人类的大眼睛
  19. IE浏览器连不上网电脑无法联网
  20. 入行10年后,我总结了这份FPGA学习路线

热门文章

  1. python扩展库xlwt支持对excel_Python扩展库xlwt支持对Excel2003或更低版本的Excel文件进行写操作。...
  2. 舆情监测系统工作流程大致是怎样的?
  3. 【个人网站搭建】GitHub pages+hexo框架下为next主题添加菜单分类页面
  4. 超适合3D建模小白的技巧!学习一项技能,方向方法对了事半功倍
  5. 数据分析报告1:某电商店铺印度销售情况分析
  6. 下行控制信息 - 上行DCI
  7. 记录刚上大学的我是如何攻破教务系统(教务系统漏洞)
  8. Android开发——Android手机屏幕适配方案总结
  9. 哈里斯鹰优化算法(HHO)附matlab code链接
  10. 监控系统-3.1自定义告警