史上最全,Android P指纹应用及源码解析

2023-05-16

简单使用

    • 源码分析

首先需要了解以下几点

  • 指纹识别相关api是在Android23版本添加的,所以过早版本的设备是无法使用的;
android.os.Build.VERSION.SDK_INT >= 23 // 不会真有人还在用23以前的手机吧?
  • 在Android28版本,使用BiometricPrompt替代了FingerprintManager,FingerprintManager被标记为@Deprecated,但依然可以使用它,而且BiometricPrompt与FingerprintManager并没有多大区别(Api 29 30中BiometricPrompt新增了一些方法,不在此文涉及),内部实现依然是调用的FingerprintService的相关方法;
  1. 使用BiometricPrompt(Api>=28)
    首先添加权限:
<uses-permission android:name="android.permission.USE_BIOMETRIC" />

BiometricPrompt是用于显示一个系统提供的生物识别对话框,所以我们需要新建一个构造器:

BiometricPrompt biometricPrompt = new BiometricPrompt.Builder(mContext)
                .setTitle("指纹验证")
                .setSubtitle("小标题")
                .setDescription("描述")
                .setNegativeButton("取消", mContext.getMainExecutor(), (dialog, which) -> { Log.i(TAG, "cancel fingerprint authenticate"); })
                .build();

setTitle、setSubtitle、setDescription方法就不多说了,大伙可以在下面的效果图中直接看到。这里说一下setNegativeButton方法,这个设置项是必须有的,而且里面的三个参数都不能为null,否则会抛出异常。setNegativeButton方法用于设置指纹识别对话框的取消按钮和点击事件,第一个参数为要在提示的否定按钮上显示的文本;第二个参数为该onClick事件要执行的执行器,这里可以选择主程序执行器;第三个参数为按钮点击时间,这里打印了一条log。大家可以在这里对取消按钮做一些操作,比如将其改为“密码验证”按钮等。
与setNegativeButton类似的还有一个setPositiveButton方法,该方法的使用与setNegativeButton相同,不过它是@hide的,第三方应用无法调用,大家手上有源码的可以试试看。
接下来就是指纹验证了:

biometricPrompt.authenticate(new CancellationSignal(), mContext.getMainExecutor(), authenticationCallback);

以上三个参数都是@NonNull的。
第一个参数用于取消当前指纹验证操作,可以为它设置监听器CancellationSignal#setOnCancelListener,如果没有取消,指纹感应设备会一直等待指纹输入,直到超时;
第二个参数用于分发指纹识别结果的回调事件,这里可以设为主程序执行器;
第三个参数是最重要的一个参数,用于处理指纹识别结果,它是一个内部类:

BiometricPrompt.AuthenticationCallback authenticationCallback = new BiometricPrompt.AuthenticationCallback() {
            @Override
            public void onAuthenticationError(int errorCode, CharSequence errString) {
                super.onAuthenticationError(errorCode, errString);
                Log.i(TAG, "onAuthenticationError errorCode: " + errorCode + " errString: " + errString);
            }

            @Override
            public void onAuthenticationHelp(int helpCode, CharSequence helpString) {
                super.onAuthenticationHelp(helpCode, helpString);
                Log.i(TAG, "onAuthenticationHelp helpCode:" + helpCode + "helpString: " + helpString);
            }

            @Override
            public void onAuthenticationSucceeded(BiometricPrompt.AuthenticationResult result) {
                super.onAuthenticationSucceeded(result);
                Log.i(TAG, "onAuthenticationSucceeded");
            }

            @Override
            public void onAuthenticationFailed() {
                super.onAuthenticationFailed();
                Log.i(TAG, "onAuthenticationFailed");
            }
        };

BiometricPrompt#authenticate为我们发起了一个指纹识别请求,识别结果则可以在这个callback里处理。
onAuthenticationError会在识别出现错误时调用,它通常表示一个预期之外的,不可修复的错误,比如设备不支持指纹识别功能、指纹传感器损毁、设备中不存在已录入的指纹、失败次数过多等。errorCode与errorString都是在系统内部定义好的,可以打印出来;
onAuthenticationFailed会在认证失败时调用,它与onAuthenticationError不同,这个失败通常是可预期的,可以修复的,比如输入的指纹与设备指纹库指纹不匹配,这个时候可以再次放上手指进行验证,直到超过验证次数;
onAuthenticationHelp用于指纹认证过程中给出一些帮助信息,比如手指移动过快、指纹传感器有脏污、手指移动过慢等,这些帮助信息都可以在helpString获取到;
onAuthenticationSucceeded则在输入指纹与指纹库指纹相匹配时调用,当验证成功时,将会立即结束此次指纹验证过程,再放上手指在传感器上不会有响应。
BiometricPrompt#authenticate还有一个重载方法:

biometricPrompt.authenticate(CryptoObject crypto, CancellationSignal cancel, Executor executor, BiometricAuthenticator.AuthenticationCallback callback);

多了一个参数CryptoObject,这是一个密码对象的包装类,关于这个参数,我们会在下文进行介绍。
有关BiometricPrompt内容就是这么多了,下面给出几张效果图:


  1. 使用FingerprintManager(Api>=23)
    首先添加权限
<uses-permission android:name="android.permission.USE_FINGERPRINT" />

判断是否设备是否支持指纹功能,下面三个判断都需要为true(BiometricPrompt不需要此判断,如果不支持,BiometricPrompt会调用onAuthenticationError方法)

// 是否存在指纹功能模块
mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_FINGERPRINT);
// 设备是否有硬件支持
fingerprintManager.isHardwareDetected();
// 设备中是否存在已录入指纹
fingerprintManager.hasEnrolledFingerprints();

新建一个指纹认证结果回调:

FingerprintManager.AuthenticationCallback authenticationCallback = new FingerprintManager.AuthenticationCallback() {
            @Override
            public void onAuthenticationError(int errorCode, CharSequence errString) {
                Log.i(TAG, "onAuthenticationError errorCode: " + errorCode + " errString: " + errString);
                super.onAuthenticationError(errorCode, errString);
            }

            @Override
            public void onAuthenticationHelp(int helpCode, CharSequence helpString) {
                Log.i(TAG, "onAuthenticationError helpCode: " + helpCode + " helpString: " + helpString);
                super.onAuthenticationHelp(helpCode, helpString);
            }

            @Override
            public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) {
                Log.i(TAG, "onAuthenticationError");
                super.onAuthenticationSucceeded(result);
            }

            @Override
            public void onAuthenticationFailed() {
            	Log.i(TAG, "onAuthenticationFailed");
                super.onAuthenticationFailed();
            }
        };

作用与上文BiometricPrompt.AuthenticationCallback相同,这里就不赘述了。
接下来就是发起指纹验证:

FingerprintManager fingerprintManager = (FingerprintManager)mContext.getSystemService(Context.FINGERPRINT_SERVICE);
fingerprintManager.authenticate(null, new CancellationSignal(), 0, authenticationCallback, null);

下面看一下该方法的参数:

public void authenticate(FingerprintManager.CryptoObject crypto, CancellationSignal cancel, int flags, FingerprintManager.AuthenticationCallback callback, Handler handler)

一共有五个参数:
crypto:与BiometricPrompt相同,用于加密对象的关联,如果不需要,可以为空,不过这样会有指纹被篡改的风险;
cancel:与BiometricPrompt相同,用于取消指纹操作;
flags:此flags暂时没有用到,需要输入0;
callback:与BiometricPrompt相同,用于指纹识别结果处理;
handler:用来处理callback回调事件,类似于BiometricPrompt的Executor参数,若为空,则会使用主线程来处理;

调用fingerprintManager.authenticate后不会像BiometricPrompt那样弹出一个底部框,你可以自定义界面,然后调用fingerprintManager.authenticate方法,比如将该方法放到onCreate里,这样一进入该页面就会触发指纹认证,或者自定义一个Dialog,在Dialog弹出的时候调用指纹认证方法,认证成功或认证失败达到上限次数就会结束指纹认证过程。

  1. 指纹录制
    FingerprintManager还有一个指纹录制的方法,不过它是@hide的,第三方应用无法使用,但在系统设置里,此功能是非常重要的。
public void enroll(byte [] token, CancellationSignal cancel, int flags, int userId, EnrollmentCallback callback);

首先添加权限

<uses-permission android:name="android.permission.MANAGE_FINGERPRINT" />

然后像前面一样,新建一个录制结果回调,用来处理录制结果:

private FingerprintManager.EnrollmentCallback mEnrollmentCallback
            = new FingerprintManager.EnrollmentCallback() {

        @Override
        public void onEnrollmentProgress(int remaining) {
            // 11 to 0
            Log.i(TAG, "onEnrollmentProgress: " + remaining);
        }

        @Override
        public void onEnrollmentHelp(int helpMsgId, CharSequence helpString) {
            // 指纹已存在
            Log.i(TAG, "onEnrollmentHelp: " + helpMsgId + " " + helpString);
        }

        @Override
        public void onEnrollmentError(int errMsgId, CharSequence errString) {
            // 指纹操作已取消
            Log.i(TAG, "onEnrollmentError: " + errMsgId + " " + errString);
        }
    };

和前面指纹识别的两个callback相识,这里有三个方法可以重写:

  • onEnrollmentProgress 因为指纹录制是一个持续的过程,需要多次放上手指到感应器上,每次成功识别到手指都会调用此方法,参数remaining是一个剩余步数,表示还要将手指放上感应器几次,这里是一共12步,也就是从11到0,可以在这里做动画更新的效果;
  • onEnrollmentHelp则是录制过程中要显示的提示信息,比如指纹已存在,这些信息是可以定制的;
  • onEnrollmentError是录制过程出现的错误信息,不会中断整个指纹录制过程,如某一步指纹感应过程中,指纹操作已取消。
    接下来就是开启指纹录制过程:
mFingerprintManager.enroll(mToken, new CancellationSignal(), 0 /* flags */, mUserId, mEnrollmentCallback);

下面是效果图:

源码分析

  1. BiometricPrompt#authenticate
    前面我们已经知道,BiometricPrompt#authenticate有两个重载方法:
// frameworks/base/core/java/android/hardware/biometrics/BiometricPrompt.java
public void authenticate(@NonNull android.hardware.biometrics.CryptoObject crypto,
            @NonNull CancellationSignal cancel,
            @NonNull @CallbackExecutor Executor executor,
            @NonNull BiometricAuthenticator.AuthenticationCallback callback)
        if (!(callback instanceof BiometricPrompt.AuthenticationCallback))
public void authenticate(@NonNull CancellationSignal cancel,
            @NonNull @CallbackExecutor Executor executor,
            @NonNull BiometricAuthenticator.AuthenticationCallback callback)

先来看看简单点的三参数方法:

    // frameworks/base/core/java/android/hardware/biometrics/BiometricPrompt.java
    public void authenticate(@NonNull CancellationSignal cancel,
            @NonNull @CallbackExecutor Executor executor,
            @NonNull AuthenticationCallback callback) {
        if (handlePreAuthenticationErrors(callback, executor)) {
            return;
        }
        mFingerprintManager.authenticate(cancel, mBundle, executor, mDialogReceiver, callback);
    }
    private boolean handlePreAuthenticationErrors(AuthenticationCallback callback,
            Executor executor) {
        // 是否存在指纹功能模块
        if (!mPackageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) {
            sendError(BiometricPrompt.BIOMETRIC_ERROR_HW_NOT_PRESENT, callback,
                      executor);
            return true;
          // 设备是否有硬件支持
        } else if (!mFingerprintManager.isHardwareDetected()) {
            sendError(BiometricPrompt.BIOMETRIC_ERROR_HW_UNAVAILABLE, callback,
                      executor);
            return true;
          // 设备内是否存在至少一个已录制好的指纹
        } else if (!mFingerprintManager.hasEnrolledFingerprints()) {
            sendError(BiometricPrompt.BIOMETRIC_ERROR_NO_BIOMETRICS, callback,
                      executor);
            return true;
        }
        return false;
    }
    private void sendError(int error, AuthenticationCallback callback, Executor executor) {
        executor.execute(() -> {
            // 给出错误信息,vendorCode是客户自定义code,当通过error找不到系统已定义的错误信息时,会通过vendorCode来查找
            callback.onAuthenticationError(error, mFingerprintManager.getErrorString(
                    error, 0 /* vendorCode */));
        });
    }

大家可以看到,在调用了BiometricPrompt#authenticate方法后,首先会进行一个判断,是否满足三个条件,这三个条件我们在前面使用FingerprintManager#authenticate时自己写了判断,而在这里就不用我们写了。如果有一个条件不满足,就会调用AuthenticationCallback的onAuthenticationError方法,交给开发者处理,返回的错误码和错误信息分别在frameworks/base/core/java/android/hardware/biometrics/BiometricConstants.java和FingerprintManager.java中定义。

// /frameworks/base/core/java/android/hardware/fingerprint/FingerprintManager.java
public String getErrorString(int errMsg, int vendorCode) {
        switch (errMsg) {
            case FINGERPRINT_ERROR_UNABLE_TO_PROCESS:
                return mContext.getString(
                    com.android.internal.R.string.fingerprint_error_unable_to_process);
            case FINGERPRINT_ERROR_HW_UNAVAILABLE:
                return mContext.getString(
                    com.android.internal.R.string.fingerprint_error_hw_not_available);
            case FINGERPRINT_ERROR_NO_SPACE:
                return mContext.getString(
                    com.android.internal.R.string.fingerprint_error_no_space);
            case FINGERPRINT_ERROR_TIMEOUT:
                return mContext.getString(com.android.internal.R.string.fingerprint_error_timeout);
            case FINGERPRINT_ERROR_CANCELED:
                return mContext.getString(com.android.internal.R.string.fingerprint_error_canceled);
            case FINGERPRINT_ERROR_LOCKOUT:
                return mContext.getString(com.android.internal.R.string.fingerprint_error_lockout);
            case FINGERPRINT_ERROR_LOCKOUT_PERMANENT:
                return mContext.getString(
                        com.android.internal.R.string.fingerprint_error_lockout_permanent);
            case FINGERPRINT_ERROR_USER_CANCELED:
                return mContext.getString(
                        com.android.internal.R.string.fingerprint_error_user_canceled);
            case FINGERPRINT_ERROR_NO_FINGERPRINTS:
                return mContext.getString(
                        com.android.internal.R.string.fingerprint_error_no_fingerprints);
            case FINGERPRINT_ERROR_HW_NOT_PRESENT:
                return mContext.getString(
                        com.android.internal.R.string.fingerprint_error_hw_not_present);
            case FINGERPRINT_ERROR_VENDOR: {
            		// 客户自定义错误信息在这个数组里定义
                    String[] msgArray = mContext.getResources().getStringArray(
                            com.android.internal.R.array.fingerprint_error_vendor);
                    if (vendorCode < msgArray.length) {
                        return msgArray[vendorCode];
                    }
                }
        }
        Slog.w(TAG, "Invalid error message: " + errMsg + ", " + vendorCode);
        return null;
    }

最后一个case为客户自定义错误信息数组,所有文本资源文件定义于/frameworks/base/core/res/res/values/strings.xml
如果条件均满足,接下来就进入到了mFingerprintManager.authenticate(这里可以发现,即使在Api28版本用BiometricPrompt代替了FingerprintManager,但其内部还是使用的FingerprintManager方法):

mFingerprintManager.authenticate(cancel, mBundle, executor, mDialogReceiver, callback);

这里还有两个参数mBundle和mDialogReceiver是我们没有涉及到的:

  • mBundle携带了前文所说的Title,subTitle,Description以及按钮文本positive_text和negative_text
  • mDialogReceiver
    // frameworks/base/core/java/android/hardware/biometrics/BiometricPrompt.java
    IBiometricPromptReceiver mDialogReceiver = new IBiometricPromptReceiver.Stub() {
        @Override
        public void onDialogDismissed(int reason) {
            // Check the reason and invoke OnClickListener(s) if necessary
            // 因为点击确认按钮退出,调用确认按钮onClick方法
            if (reason == DISMISSED_REASON_POSITIVE) {
                mPositiveButtonInfo.executor.execute(() -> {
                    mPositiveButtonInfo.listener.onClick(null, DialogInterface.BUTTON_POSITIVE);
                });
              //因为点击取消按钮退出,调用取消按钮onClick方法
            } else if (reason == DISMISSED_REASON_NEGATIVE) {
                mNegativeButtonInfo.executor.execute(() -> {
                    mNegativeButtonInfo.listener.onClick(null, DialogInterface.BUTTON_NEGATIVE);
                });
            }
        }
    };

   // 实例化mNegativeButtonInfo对象
   public Builder setNegativeButton(@NonNull CharSequence text,
                @NonNull @CallbackExecutor Executor executor,
                @NonNull DialogInterface.OnClickListener listener) {
            mNegativeButtonInfo = new ButtonInfo(executor, listener);
            return this;
        }

可以看到,mDialogReceiver包含了指纹识别对话框dismiss时可能所要执行的操作,这个所要操作的onClick方法是在我们Builder对话框时,通过调用setNegativeButton方法自己定义的。
了解了各个参数的含义,我们正式进入指纹认证过程:

// /frameworks/base/core/java/android/hardware/fingerprint/FingerprintManager.java
public void authenticate(
            @NonNull CancellationSignal cancel,
            @NonNull Bundle bundle,
            @NonNull @CallbackExecutor Executor executor,
            @NonNull IBiometricPromptReceiver receiver,
            @NonNull BiometricAuthenticator.AuthenticationCallback callback) {
        // 非空判断,抛出异常
        if (cancel == null) {
            throw new IllegalArgumentException("Must supply a cancellation signal");
        }
        if (bundle == null) {
            throw new IllegalArgumentException("Must supply a bundle");
        }
        if (executor == null) {
            throw new IllegalArgumentException("Must supply an executor");
        }
        if (receiver == null) {
            throw new IllegalArgumentException("Must supply a receiver");
        }
        if (callback == null) {
            throw new IllegalArgumentException("Must supply a calback");
        }
        // null是crypto
        authenticate(mContext.getUserId(), null, cancel, bundle, executor, receiver, callback);
    }

前面是各个参数的非空判断,看到最后一行的null参数大伙应该明白了吧,BiometricPrompt#authenticate的四参数方法正是用crypto替代了null,所以这两个重载方法其实最后都是调用了FingerprintManager内部的authenticate方法,接下来我们一起看一下这个方法:

// /frameworks/base/core/java/android/hardware/fingerprint/FingerprintManager.java
private void authenticate(int userId,
            @Nullable android.hardware.biometrics.CryptoObject crypto,
            @NonNull CancellationSignal cancel,
            @NonNull Bundle bundle,
            @NonNull @CallbackExecutor Executor executor,
            @NonNull IBiometricPromptReceiver receiver,
            @NonNull BiometricAuthenticator.AuthenticationCallback callback) {
        // mCryptoObject会在AuthenticationCallback#onAuthenticationSucceeded作为结果返回
        mCryptoObject = crypto;
        // 指纹操作被取消
        if (cancel.isCanceled()) {
            Slog.w(TAG, "authentication already canceled");
            return;
        } else {
            cancel.setOnCancelListener(new OnAuthenticationCancelListener(crypto));
        }

        if (mService != null) {
            try {
                mExecutor = executor;
                // 验证结果回调赋值到本地变量,待会会用到这个
                mAuthenticationCallback = callback;
                final long sessionId = crypto != null ? crypto.getOpId() : 0;
                // 主要看这里
                mService.authenticate(mToken, sessionId, userId, mServiceReceiver,
                        0 /* flags */, mContext.getOpPackageName(), bundle, receiver);
            } catch (RemoteException e) {
                Slog.w(TAG, "Remote exception while authenticating", e);
                mExecutor.execute(() -> {
                    // 调用AuthenticationCallback#onAuthenticationError发送硬件不可用错误信息
                    callback.onAuthenticationError(FINGERPRINT_ERROR_HW_UNAVAILABLE,
                            getErrorString(FINGERPRINT_ERROR_HW_UNAVAILABLE, 0 /* vendorCode */));
                });
            }
        }
    }

大家看一下mService.authenticate注释处的mServiceReceiver,还记得我们在一开始的创建的mDialogReceiver吗,对应的就是这里的最后一个参数receiver,用来处理取消对话框点击事件的,这里需要与mServiceReceiver区分开。那么mServiceReceiver是干嘛的呢:

// /frameworks/base/core/java/android/hardware/fingerprint/FingerprintManager.java
private IFingerprintServiceReceiver mServiceReceiver = new IFingerprintServiceReceiver.Stub() {

        @Override // binder call
        public void onEnrollResult(long deviceId, int fingerId, int groupId, int remaining) {
            mHandler.obtainMessage(MSG_ENROLL_RESULT, remaining, 0,
                    new Fingerprint(null, groupId, fingerId, deviceId)).sendToTarget();
        }

        @Override // binder call
        public void onAcquired(long deviceId, int acquireInfo, int vendorCode) {
            if (mExecutor != null) {
                mExecutor.execute(() -> {
                    sendAcquiredResult(deviceId, acquireInfo, vendorCode);
                });
            } else {
                mHandler.obtainMessage(MSG_ACQUIRED, acquireInfo, vendorCode,
                        deviceId).sendToTarget();
            }
        }

        @Override // binder call
        public void onAuthenticationSucceeded(long deviceId, Fingerprint fp, int userId) {
            if (mExecutor != null) {
                mExecutor.execute(() -> {
                    sendAuthenticatedSucceeded(fp, userId);
                });
            } else {
                mHandler.obtainMessage(MSG_AUTHENTICATION_SUCCEEDED, userId, 0, fp).sendToTarget();
            }
        }

        @Override // binder call
        public void onAuthenticationFailed(long deviceId) {
            if (mExecutor != null) {
                mExecutor.execute(() -> {
                    sendAuthenticatedFailed();
                });
            } else {
                mHandler.obtainMessage(MSG_AUTHENTICATION_FAILED).sendToTarget();
            }
        }

        @Override // binder call
        public void onError(long deviceId, int error, int vendorCode) {
            if (mExecutor != null) {
                // BiometricPrompt case
                if (error == FingerprintManager.FINGERPRINT_ERROR_USER_CANCELED
                        || error == FingerprintManager.FINGERPRINT_ERROR_CANCELED) {
                    // User tapped somewhere to cancel, or authentication was cancelled by the app
                    // or got kicked out. The prompt is already gone, so send the error immediately.
                    mExecutor.execute(() -> {
                        sendErrorResult(deviceId, error, vendorCode);
                    });
                } else {
                    // User got an error that needs to be displayed on the dialog, post a delayed
                    // runnable on the FingerprintManager handler that sends the error message after
                    // FingerprintDialog.HIDE_DIALOG_DELAY to send the error to the application.
                    mHandler.postDelayed(() -> {
                        mExecutor.execute(() -> {
                            sendErrorResult(deviceId, error, vendorCode);
                        });
                    }, BiometricPrompt.HIDE_DIALOG_DELAY);
                }
            } else {
                mHandler.obtainMessage(MSG_ERROR, error, vendorCode, deviceId).sendToTarget();
            }
        }

        @Override // binder call
        public void onRemoved(long deviceId, int fingerId, int groupId, int remaining) {
            mHandler.obtainMessage(MSG_REMOVED, remaining, 0,
                    new Fingerprint(null, groupId, fingerId, deviceId)).sendToTarget();
        }

        @Override // binder call
        public void onEnumerated(long deviceId, int fingerId, int groupId, int remaining) {
            // TODO: propagate remaining
            mHandler.obtainMessage(MSG_ENUMERATED, fingerId, groupId, deviceId).sendToTarget();
        }
    };

这个IFingerprintServiceReceiver mServiceReceiver可是非常重要的,刚才不是说了指纹验证的结果会交给BiometricPrompt.AuthenticationCallback来处理吗?包括success、error、failed、help,那么AuthenticationCallback的这些方法是怎么被调用的呢?没错,大部分都是由IFingerprintServiceReceive来调用的(前面有些地方我们已经看到了发生错误时也调用过onAuthenticationError)。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

史上最全,Android P指纹应用及源码解析 的相关文章

  • Testng学习笔记(一)

    目录 TestNg简介1 TestNG使用流程1 1TestNG安装及使用1 2创建maven项目 xff0c 进行POM配置1 3 创建Test类1 4添加测试方法 2 TestNG常用注解及生命周期2 1常用注解2 2TestNG注解的
  • (史上最详细的解释看过来)深入理解函数栈帧

    函数的调用过程 xff08 栈帧 xff09 话说 xff0c 什么是函数栈帧 xff1f 我之前也是一脸懵逼的 xff08 xff09 xff0c 举个栗子 xff0c 先看一段简单的代码 xff1a span class token m
  • main函数参数及可变参数列表解析

    main函数的参数解析 平时写main函数大多都是int main xff0c 然后误以为main函数是没有参数的 xff0c 但在有些书上又看到main括号里有三个不怎么看的懂的参数 xff0c 其实仔细研究就会发现 xff0c main
  • 给定一棵二叉树,判断它是否是镜像对称的

    给定一个二叉树 xff0c 检查它是否是镜像对称的 在这里先解释一下镜像对称的概念 xff0c 顾名思义 xff0c 就像人站在镜子前面面对自己一样 xff0c 看到的一切都是对称的 镜中的反射与现实中的人具有相同的头部 xff0c 但反射
  • 判断一棵二叉树是否是平衡二叉树

    我们先来整理一下什么是平衡二叉树 xff1f 满足以下两点的就是平衡二叉树 xff1a 1 左右子树的高度差不能超过1 2 左右子树也是平衡二叉树 需要注意的是空树也是平衡二叉树 例如下面这棵树就不是平衡二叉树 因为对于B来说左右子树高度超
  • Fiddler实现手机抓包入门配置

    emmm 在此之前我也只是听过抓包抓包 xff0c 但是没有真正的接触到 xff0c Fiddler是一款很强大的抓包工具了 xff0c 但是一开始很多人可能会想我一样走上歧途 xff0c 所以在此总结一下 xff0c 希望看到的朋友少走弯
  • Idea使用Maven连接MySQL数据库

    连接MySQL数据库的步骤 xff1a 1 添加数据库 2 填写连接信息如图 3 填写连接信息完之后 xff0c 需要记一下jar包的位置 xff0c 用于添加jar包 4 显示jar包信息 5 添加文件夹命名java xff0c 并将其设
  • mybatis generator 生成中文字段注释

    首先 xff0c 试了好几次 xff0c 重写 DefaultCommentGenerator xff0c 创建MyCommentGenerator类实现CommentGenerator接口 xff0c 都是按照网上的方案来的 xff0c
  • org/apache/velocity/context/Context

    mybatisplus generator AutoGenerator 生成文件时报错如下 xff1a 请输入表名 xff1a alarm 14 38 28 569 main DEBUG com baomidou mybatisplus g
  • NVIDIA开源NeMo,CUDA,pytorch安装使用过程踩坑总结

    说明 Nemo cpu版本直接看文章最后一部分 xff08 linux版本的 xff09 xff0c windows下安装的话要先安装CUDA来支持GPU xff0c 在安装pytorch 在安装nemo 但是还是出问题了 xff0c 不建
  • 网络url转文件

    首先是获取springboot resource下面的文件 String fileUrl 61 ResourceUtils getURL 34 classpath 34 getPath 43 34 test txt 34 File file
  • ByteBuffer和String互转

    Charset charset 61 StandardCharsets UTF 8 String str 61 34 kj你好呀 kjlkjlkjlk 34 ByteBuffer buffer 61 ByteBuffer allocate
  • @Test单元测试注入bean 3种方式

    1 64 ContextConfiguration 64 ContextConfiguration Spring整合JUnit4测试时 xff0c 使用注解引入多个配置文件 64 RunWith SpringJUnit4ClassRunne
  • java 导出word

    目前来看 xff0c java导出word大致有6种解决方案 xff1a 1 xff1a Jacob是Java COM Bridge的缩写 xff0c 使用Jacob自带的DLL动态链接库 xff0c 并通过JNI的方式实现了在Java平台
  • JAVA visualVM 查看堆栈分配

    由于在做nlp xff0c 用到standFord nlp xff0c 堆栈分配总是一处 xff0c 所以需要这个 xff0c 正好学习一下 我用的是jdk8 5 xff0c 位于 JDK 根目录的 bin 文件夹下 xff0c 无需安装
  • Android Studio Gradle失败 Could not reolve play-services-vision-17.0.2.aar 等类似问题解决办法

    Android Studio Gradle失败 Could not reolve play services vision 17 0 2 aar 等类似问题解决办法 网上一些常规解决办法 如果上述常规解决办法尝试后 xff0c 都无法解决你
  • 利用fsl进行配准

    利用fsl进行配准 配准概念 配准就是将两个不同空间 体素 xff0c 扫描的位置不一致的nii xff0c 配准到同一个空间上 xff0c 使得两者在大脑上的相应位置就可以一一对应上了 通常MRI数据处理的步骤 xff1a 先配准到tem
  • 1.计算机概论

    学习linux前先来了解一下计算机概念 xff0c 如果了解相关内容 xff0c 可跳过本章节 1 1 电脑 电脑是一种计算机 xff0c 计算机实际是 xff1a 接受用户输入的命令与数据 xff0c 经由中央处理器的算术和逻辑单元运算后
  • SpringDataJPA——使用EntityManager利用原生SQL自定义复杂查询

    使用EntityManager 原生SQL查询方法记录以下学习过程中找到的其他文章地址 原生SQL查询方法 在这里进行记录以下使用过程 xff0c 注释已经很清晰 span class token annotation punctuatio
  • 操作系统(二十三)生产者消费者问题

    2 3 6 生产者消费者问题 生产者消费者问题 The proceducer consumer problem 是一个经典的进程同步的问题 xff0c 问题是这样描述的 xff1a 在操作系统中有一组生产者进程一组消费者进程 xff0c 生

随机推荐

  • Powershell脚本:一键优化windows 10(原版)

    本套Powershell脚本出自github开源项目 xff0c 包含原版WIN10系统大概300个一键优化 组件精简方案 例如彻底关闭Windows defender xff0c 关闭共享 打印机 xff0c 保留Windows upda
  • 安装ubuntu与windows双系统

    ubuntu程序的安装 开机进bios xff0c 在Security页面 xff0c 关掉secure boot xff1a 存储系统文件 xff0c 建议10GB 15GB xff1b swap xff1a 交换分区 xff0c 即Li
  • Windows编程经典书籍

    本人是刚刚开始学习windows编程的 感觉看雪学院的大牛很NB 想找一些书籍来看学习学习 可是不知道看哪些书好 驱动 对菜鸟们来说真是一个很深奥的话题 所以 我找来了这篇文章供大家分享 以后大家发现什么好书就在楼下跟贴吧 作者 xff1a
  • 经典Windows编程书单

    说好的这次写一个图形编程书单 但是看起来不是很好整理 xff0c 这类书散落的家里到处都是 先把经典Windows编程的书整理一下吧 xff0c 不过Windows的也到处都是很多都找不到了 xff0c 只能把找到的拍个照 xff0c 可能
  • ubuntu18.04开机循环输入密码无法进入桌面

    问题 xff1a 在profile和environment文件里配置了java环境变量后 xff0c 重启电脑后即使输入正确的用户名和密码 xff0c 也会重新跳到登录界面 xff0c 无法进入系统 xff0c 一直循环登录 原因 xff1
  • ubuntu 安装VS

    Table of Contents 一 前言 二 安装过程 1 下载VS Code 2 安装过程 3 下载C 43 43 模块 4 汉化 5 常用快捷键 一 前言 因为要用到在ubuntu系统中使用VS Code 来编写C 43 43 代码
  • Windows系统FTP服务器设置

    设置操作步骤 步骤一 xff1a 确认电脑是否开通联网共享服务 依次点击 控制面板 程序 启用或关闭Windows功能 按钮 xff0c 进入到 Windows功能 页面 xff0c 查看 Internet Information Serv
  • springboot thymeleaf 配置

    Springboot默认是不支持JSP的 xff0c 默认使用thymeleaf模板引擎 1 在application properties文件中增加Thymeleaf模板的配置 thymelea模板配置 spring thymeleaf
  • 【ubuntu】fatal: detected dubious ownership in repository at ...

    在ubuntu使用git的时候遇到了以下错误 xff1a fatal detected dubious ownership in repository at 39 home xxx 39 To add an exception for th
  • 有意思的Windows脚本(1)

    有意思的Windows脚本 1 因为不知道今天的博客写什么啦 xff0c 就放几个好玩的Windows脚本的源码吧 xff0c 大家千万不要干坏事情哦 xff0c 嘿嘿 1 vbs循环 xff08 桌面上建一个记事本 xff0c 输入下面代
  • 程序员3年5年10年三个阶段

    第一阶段 三年 三年对于程序员来说是第一个门槛 xff0c 这个阶段将会淘汰掉一批不适合写代码的人 这一阶段 xff0c 我们走出校园 xff0c 迈入社会 xff0c 成为一名程序员 xff0c 正式从书本上的内容迈向真正的企业级开发 我
  • 使用 matplotlib 轻松制作动画

    https www codenong com e264872efa062c7d6955 该链接讲了如何使用 matplotlib 轻松制作动画 xff0c 很好用
  • C#中使用IMemoryCache实现内存缓存

    1 缓存基础知识 缓存是实际工作中非常常用的一种提高性能的方法 缓存可以减少生成内容所需的工作 xff0c 从而显著提高应用程序的性能和可伸缩性 缓存最适用于不经常更改的数据 通过缓存 xff0c 可以比从原始数据源返回的数据的副本速度快得
  • 2021-09-13使用@Slf4j报错 程序包org.slf4j不存在

    导入两个maven依赖 然后就OK了 span class token tag span class token tag span class token punctuation lt span dependency span span c
  • PowerShell7.X的安装与美化

    参考链接1 xff1a https blog csdn net qq 39537898 article details 117411132参考链接2 xff1a https sspai com post 59380 很有参考价值 xff0c
  • Lab2 p3 围棋吃子的算法实现

    简单介绍下框架 xff1a 1 xff0e 声明一维数组block 作为一个临时变量记录一个块的大小 xff0c 声明一个整型blockLength记录这个块的长度 2 xff0e kill 为吃子的主函数 recersion int i
  • Python爬取皮皮虾视频

    背景 xff1a 今天闲着没事做 xff0c 然后想着刷刷视频 xff0c 然后发现前段时间学习了一下网络爬虫的一些基本应用 xff0c 就想着利用爬虫到网上去爬取一点视频来模拟人为的点击 下载操作 因为皮皮虾是手机端的app xff0c
  • 解决Result Maps collection already contains value for...BaseResultMap问题

    使用generatorSqlmapCustom逆向工程生成代码报错 假如使用generatorSqlmapCustom逆向工程生成代码 xff0c 即生成dao文件和mapper xml文件 xff0c 复制粘贴至工程中运行报错 Resul
  • IDEA2022.1的一些不常见问题解决方案

    文章目录 IDEA2022 1小问题解决方案 学习的时候尝鲜用了最新版本的IDEA 出现过以下老版本不会遇见的问题 Spring Initializer 创建的项目 无法新建module 显示Directory is already tak
  • 史上最全,Android P指纹应用及源码解析

    简单使用 源码分析 首先需要了解以下几点 指纹识别相关api是在Android23版本添加的 xff0c 所以过早版本的设备是无法使用的 xff1b android span class token punctuation span os