好久没写文章了,最近也比较偷懒,今天继续讨论我实际开发中遇到的需求,那就是关于APP解锁,大家都知道。现在越来越多的APP在填入账号密码后,第二次登录后,基本不会再次重复输入账号密码了。而是快捷登录,而常用的就是 指纹解锁手势解锁 二种.


好了,我们就开始我们今天的解锁之旅。

这边我只是展示我的需求的逻辑,不同项目可能逻辑不同,不影响本文主要内容。

主要步骤就分三步:

  1. 账号密码登录。登录成功后弹出一个弹框让用户选择快捷登录方式。
  2. 然后跳到相应的快捷登录的设置界面
  3. 下次登录的时候就进行快捷登录

我们一步步来看。

快捷登录方式选择

当用账号密码登录成功后,我们就在登录界面直接弹出一个弹框,然后让用户选择想要的快捷登录方式,当然如果用户二种都不想要,那就直接按取消,然后登录到主页,然后下次再打开应用就会又要重新输入账号密码。

快捷登录方式选择框

这里就会遇到我们的第一个问题:

因为Android手机有很多种类,有些有指纹,有些没有指纹, 那我们需要在有指纹的时候,跳出这个有二种选择的弹框,如果没有指纹解锁,就直接跳到手势解锁的界面。

我的判断可能比较笼统,当然还有更好的:

  1. 我直接就判断SDK是否>= 23,因为指纹解锁是SDK 23 出来的,但是很多国产手机可能是Android 5的系统,但是也有指纹解锁。这里我就直接忽略了。莫怪我心狠。
  2. 在网上看到有人用反射,就是在Application中,用反射获取FingerprintManager这个类的对象,看是否能成功获取,如果能,就存一个boolean变量为ture,说明这个手机里面有指纹相关的。如果获取失败,就说明没有指纹。

    public class MyApplication extends Application {public static final String HAS_FINGERPRINT_API = "hasFingerPrintApi";public static final String SETTINGS = "settings";@Overridepublic void onCreate() {super.onCreate();SharedPreferences sp = getSharedPreferences(SETTINGS, MODE_PRIVATE);if (sp.contains(HAS_FINGERPRINT_API)) { // 检查是否存在该值,不必每次都通过反射来检查return;}SharedPreferences.Editor editor = sp.edit();try {Class.forName("android.hardware.fingerprint.FingerprintManager"); // 通过反射判断是否存在该类editor.putBoolean(HAS_FINGERPRINT_API, true);} catch (ClassNotFoundException e) {editor.putBoolean(HAS_FINGERPRINT_API, false);e.printStackTrace();}editor.apply();}
    }复制代码

我们解决了弹出框弹出的时机后,我们就要来做这个弹出框:

我以前做弹出框都是使用Dialog系列,后来无意间看到谷歌推荐大家使用DialogFragment来做弹框,取代原来的Dialog,所以正好借着这次机会,自己写了这个DialogFragment。我下面只给出重要部分。具体的大家去百度下DialogFragment即可。

public class LockChooseFragment extends DialogFragment {@Overridepublic void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setRetainInstance(true);//设置DialogFragment 的主题及弹框的Style。setStyle(DialogFragment.STYLE_NO_TITLE, android.R.style.Theme_Material_Light_Dialog);}@Nullable@Overridepublic View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, Bundle savedInstanceState) {View view = inflater.inflate(R.layout.fragment_lock_choose, container, false);unbinder = ButterKnife.bind(this, view);return view;}@Overridepublic void onResume() {super.onResume();aty = (LoginActivity) getActivity();//让我们的弹框无法点击外面区域消失getDialog().setCanceledOnTouchOutside(false);getDialog().setCancelable(false);}}复制代码

好了。接下去弹框出来了要点击一种解锁,然后进行下一个界面。我们先从简单的手势解锁来说好了。


手势解锁

我用的是Github的开源手势解锁:PatternLockView

哈哈,是不是太简单了。。。莫怪我偷懒啊。因为github中的API写的很清楚了。我就不重复介绍怎么使用。我使用了觉得的确还不错。推荐哈。

手势解锁

指纹解锁

首先我们知道谷歌提供了fingerprint包。包下面的类具体有下面这些:

  1. FingerprintManager:主要用来协调管理和访问指纹识别硬件设备
  2. FingerprintManager.AuthenticationCallback这个一个callback接口,当指纹认证后系统会回调这个接口通知app认证的结果是什么
  3. FingerprintManager.AuthenticationResult这是一个表示认证结果的类,会在回调接口中以参数给出
  4. FingerprintManager.CryptoObject这是一个加密的对象类,用来保证认证的安全性。

在开始之前,我们需要知道使用指纹识别硬件的基本步骤:

  1. 在AndroidManifest.xml中申明如下权限:
    <uses-permission android:name="android.permission.USE_FINGERPRINT"/>

  2. 获得FingerprintManager的对象引用

  3. 在运行是检查设备指纹识别的兼容性,比如是否有指纹识别设备等。

下面我们详细说一下上面的2和3 步骤:

获得FingerprintManager的对象引用

这是app开发中获得系统服务对象的常用方式,如下:

// Using the Android Support Library v4
fingerprintManager = FingerprintManagerCompat.from(this);
// Using API level 23:
fingerprintManager = (FingerprintManager)getSystemService(Context.FINGERPRINT_SERVICE);复制代码

上面给出两种方式,第一种是通过V4支持包获得兼容的对象引用,这是google推行的做法;还有就是直接使用api 23 framework中的接口获得对象引用。


在运行是检查设备指纹识别的兼容性,比如是否有指纹识别设备等

检查运行条件要使得我们的指纹识别app能够正常运行,有一些条件是必须满足的。

  1. API level 23
    指纹识别API是在api level 23也就是android 6.0中加入的,因此我们的app必须运行在这个系统版本之上。因此google推荐使用 Android Support Library v4包来获得FingerprintManagerCompat对象,因为在获得的时候这个包会检查当前系统平台的版本。
  2. 硬件
    指纹识别肯定要求你的设备上有指纹识别的硬件,因此在运行时需要检查系统当中是不是有指纹识别的硬件:

使用 fingerprintManager.isHardwareDetected()来判断是否有该硬件支持,fingerprintManager.hasEnrolledFingerprints()判断是否手机中录有指纹。

这里我在使用我的手机做开发时候就遇到了一个大坑,上面提到了。谷歌推荐使用FingerprintManagerCompat,但是我在用FingerprintManagerCompat 来调用isHardwareDetected()和hasEnrolledFingerprints()时候,返回的都是false,但是用FingerprintManager来调用isHardwareDetected()和hasEnrolledFingerprints()时候,却是返回true,而实际上我用的是小米5,Android 6 ,API23 的手机,也的确是有指纹功能的,所以我不知道为什么反而FingerprintManagerCompat这个兼容类返回是有问题的,应该跟国内厂商的底层源码修改有关。我在Google Issue Tracker中也有很多人遇到了这个问题。但基本都什么华为,小米,三星等,都不是谷歌亲儿子。所以后来我用的是FingerprintManager这个类,这个类的使用要求在API23及以上,因为毕竟谷歌的指纹是API23才出来的,而我上面又正好直接判断API23才显示指纹解锁的选项。不谋而合。。哈哈。可能这里有点偷懒了。

判断了是否有硬件支持,和手机是否有指纹之后,要注意,谷歌还需要判断当前设备必须是处于安全保护中的,即:你的设备必须是使用屏幕锁保护的,这个屏幕锁可以是password,PIN或者图案都行。为什么是这样呢?因为google原生的逻辑就是:想要使用指纹识别的话,必须首先使能屏幕锁才行,这个和android 5.0中的smart lock逻辑是一样的,这是因为google认为目前的指纹识别技术还是有不足之处,安全性还是不能和传统的方式比较的。

KeyguardManager keyguardManager =(KeyguardManager)getSystemService(Context.KEYGUARD_SERVICE);
if (keyguardManager.isKeyguardSecure()) {
// this device is secure.
}复制代码

所以这里总结判断是:

  1. 设备是否有硬件支持
  2. 手机是否处于安全保护中(没开就提示用户开启锁屏功能)
  3. 手机中是否有指纹记录(没有就提示用户去设置应用中添加一个指纹)

好了,这些前戏都做好了,我们就要开始指纹的验证了。

验证指纹

要开始扫描用户按下的指纹是很简单的,只要调用FingerprintManager的authenticate方法即可,那么现在我们来看一下这个接口:

上图是google的api文档中的描述,现在我们挨个解释一下这些参数都是什么:

  1. crypto这是一个加密类的对象,指纹扫描器会使用这个对象来判断认证结果的合法性。这个对象可以是null,但是这样的话,就意味这app无条件信任认证的结果,虽然从理论上这个过程可能被攻击,数据可以被篡改,这是app在这种情况下必须承担的风险。因此,建议这个参数不要置为null。这个类的实例化有点麻烦,主要使用javax的security接口实现。
  2. cancel 这个是CancellationSignal类的一个对象,这个对象用来在指纹识别器扫描用户指纹的是时候取消当前的扫描操作,如果不取消的话,那么指纹扫描器会移植扫描直到超时(一般为30s,取决于具体的厂商实现),这样的话就会比较耗电。建议这个参数不要置为null。
  3. flags 标识位,根据图的文档描述,这个位暂时应该为0,这个标志位应该是保留将来使用的。
  4. callback 这个是FingerprintManager.AuthenticationCallback类的对象,这个是这个接口中除了第一个参数之外最重要的参数了。当系统完成了指纹认证过程(失败或者成功都会)后,会回调这个对象中的接口,通知app认证的结果。这个参数不能为NULL。
  5. handler 这是Handler类的对象,如果这个参数不为null的话,那么FingerprintManager将会使用这个handler中的looper来处理来自指纹识别硬件的消息。通常来讲,开发这不用提供这个参数,可以直接置为null,因为FingerprintManager会默认使用app的main looper来处理。

根据上面的参数,我们一个个来具体的分析:

创建CryptoObject类对象

上面我们分析FingerprintManager的authenticate方法的时候,看到这个方法的第一个参数就是CryptoObject类的对象,现在我们看一下这个对象怎么去实例化。
我们知道,指纹识别的结果可靠性是非常重要的,我们肯定不希望认证的过程被一个第三方以某种形式攻击,因为我们引入指纹认证的目的就是要提高安全性。但是,从理论角度来说,指纹认证的过程是可能被第三方的中间件恶意攻击的,常见的攻击的手段就是拦截和篡改指纹识别器提供的结果。这里我们可以提供CryptoObject对象给authenticate方法来避免这种形式的攻击。
FingerprintManager.CryptoObject是基于Java加密API的一个包装类,并且被FingerprintManager用来保证认证结果的完整性。通常来讲,用来加密指纹扫描结果的机制就是一个Javax.Crypto.Cipher对象。Cipher对象本身会使用由应用调用Android keystore的API产生一个key来实现上面说道的保护功能。
为了理解这些类之间是怎么协同工作的,这里我给出一个用于实例化CryptoObject对象的包装类代码,我们先看下这个代码是怎么实现的,然后再解释一下为什么是这样。

public class CryptoObjectHelper
{// This can be key name you want. Should be unique for the app.static final String KEY_NAME = "com.createchance.android.sample.fingerprint_authentication_key";// We always use this keystore on Android.static final String KEYSTORE_NAME = "AndroidKeyStore";// Should be no need to change these values.static final String KEY_ALGORITHM = KeyProperties.KEY_ALGORITHM_AES;static final String BLOCK_MODE = KeyProperties.BLOCK_MODE_CBC;static final String ENCRYPTION_PADDING = KeyProperties.ENCRYPTION_PADDING_PKCS7;static final String TRANSFORMATION = KEY_ALGORITHM + "/" +BLOCK_MODE + "/" +ENCRYPTION_PADDING;final KeyStore _keystore;public CryptoObjectHelper() throws Exception{_keystore = KeyStore.getInstance(KEYSTORE_NAME);_keystore.load(null);}public FingerprintManagerCompat.CryptoObject buildCryptoObject() throws Exception{Cipher cipher = createCipher(true);return new FingerprintManagerCompat.CryptoObject(cipher);}Cipher createCipher(boolean retry) throws Exception{Key key = GetKey();Cipher cipher = Cipher.getInstance(TRANSFORMATION);try{cipher.init(Cipher.ENCRYPT_MODE | Cipher.DECRYPT_MODE, key);} catch(KeyPermanentlyInvalidatedException e){_keystore.deleteEntry(KEY_NAME);if(retry){createCipher(false);} else{throw new Exception("Could not create the cipher for fingerprint authentication.", e);}}return cipher;}Key GetKey() throws Exception{Key secretKey;if(!_keystore.isKeyEntry(KEY_NAME)){CreateKey();}secretKey = _keystore.getKey(KEY_NAME, null);return secretKey;}void CreateKey() throws Exception{KeyGenerator keyGen = KeyGenerator.getInstance(KEY_ALGORITHM, KEYSTORE_NAME);KeyGenParameterSpec keyGenSpec =new KeyGenParameterSpec.Builder(KEY_NAME, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT).setBlockModes(BLOCK_MODE).setEncryptionPaddings(ENCRYPTION_PADDING).setUserAuthenticationRequired(true).build();keyGen.init(keyGenSpec);keyGen.generateKey();}
}复制代码

上面的类会针对每个CryptoObject对象都会新建一个Cipher对象,并且会使用由应用生成的key。这个key的名字是使用KEY_NAME变量定义的,这个名字应该是保证唯一的,建议使用域名区别。GetKey方法会尝试使用Android Keystore的API来解析一个key(名字就是上面我们定义的),如果key不存在的话,那就调用CreateKey方法新建一个key。
cipher变量的实例化是通过调用Cipher.getInstance方法获得的,这个方法接受一个transformation参数,这个参数制定了数据怎么加密和解密。然后调用Cipher.init方法就会使用应用的key来完成cipher对象的实例化工作。
这里需要强调一点,在以下情况下,android会认为当前key是无效的:

  1. 一个新的指纹image已经注册到系统中
  2. 当前设备中的曾经注册过的指纹现在不存在了,可能是被全部删除了
  3. 用户关闭了屏幕锁功能
  4. 用户改变了屏幕锁的方式
    当上面的情况发生的时候,Cipher.init方法都会抛出KeyPermanentlyInvalidatedException的异常,上面我的代码中捕获了这个异常,并且删除了当前无效的key,然后根据参数尝试再次创建。
    上面的代码中使用了android的KeyGenerator来创建一个key并且把它存储在设备中。KeyGenerator类会创建一个key,但是需要一些原始数据才能创建key,这些原始的信息是通过KeyGenParameterSpec类的对象来提供的。KeyGenerator类对象的实例化是使用它的工厂方法getInstance进行的,从上面的代码中我们可以看到这里使用的AES(Advanced Encryption Standard )加密算法的,AES会将数据分成几个组,然后针对几个组进行加密。
    接下来,KeyGenParameterSpec的实例化是使用它的Builder方法,KeyGenParameterSpec.Builder封装了以下重要的信息:
  5. key的名字
  6. key必须在加密和解密的时候是有效的
  7. 上面代码中BLOCK_MODE被设置为Cipher Block Chaining也就是KeyProperties.BLOCK_MODE_CBC,这意味着每一个被AES切分的数据块都与之前的数据块进行了异或运算了,这样的目的就是为了建立每个数据块之间的依赖关系。
  8. CryptoObjectHelper类使用了PKSC7(Public Key Cryptography Standard #7)的方式去产生用于填充AES数据块的字节,这样就是要保证每个数据块的大小是等同的(因为需要异或计算还有方面算法进行数据处理,详细可以查看AES的算法原理)。
  9. setUserAuthenticationRequired(true)调用意味着在使用key之前用户的身份需要被认证。
    每次KeyGenParameterSpec创建的时候,他都被用来初始化KeyGenerator,这个对象会产生存储在设备上的key。

怎么使用CryptoObjectHelper呢?

下面我们看一下怎么使用CryptoObjectHelper这个类,我们直接看代码就知道了:

CryptoObjectHelper cryptoObjectHelper = new CryptoObjectHelper();
fingerprintManager.authenticate(cryptoObjectHelper.buildCryptoObject(), 0,cancellationSignal, myAuthCallback, null);复制代码

使用是比较简单的,首先new一个CryptoObjectHelper对象,然后调用buildCryptoObject方法就能得到CryptoObject对象了。


取消指纹扫描

上面我们提到了取消指纹扫描的操作,这个操作是很常见的。这个时候可以使用CancellationSignal这个类的cancel方法实现:

这个方法专门用于发送一个取消的命令给特定的监听器,让其取消当前操作。
因此,app可以在需要的时候调用cancel方法来取消指纹扫描操作。


处理用户的指纹认证结果

前面我们分析authenticate接口的时候说道,调用这个接口的时候必须提供FingerprintManager.AuthenticationCallback类的对象,这个对象会在指纹认证结束之后系统回调以通知app认证的结果的。在android 6.0中,指纹的扫描和认证都是在另外一个进程中完成(指纹系统服务)的,因此底层什么时候能够完成认证我们app是不能假设的。因此,我们只能采取异步的操作方式,也就是当系统底层完成的时候主动通知我们,通知的方式就是通过回调我们自己实现的FingerprintManager.AuthenticationCallback类,这个类中定义了一些回调方法以供我们进行必要的处理:

这里写图片描述
下面我们简要介绍一下这些接口的含义:

  1. OnAuthenticationError(int errorCode, ICharSequence errString) 这个接口会再系统指纹认证出现不可恢复的错误的时候才会调用,并且参数errorCode就给出了错误码,标识了错误的原因。这个时候app能做的只能是提示用户重新尝试一遍。
  2. OnAuthenticationFailed() 这个接口会在系统指纹认证失败的情况的下才会回调。注意这里的认证失败和上面的认证错误是不一样的,虽然结果都是不能认证。认证失败是指所有的信息都采集完整,并且没有任何异常,但是这个指纹和之前注册的指纹是不相符的;但是认证错误是指在采集或者认证的过程中出现了错误,比如指纹传感器工作异常等。也就是说认证失败是一个可以预期的正常情况,而认证错误是不可预期的异常情况。
  3. OnAuthenticationHelp(int helpMsgId, ICharSequence helpString) 上面的认证失败是认证过程中的一个异常情况,我们说那种情况是因为出现了不可恢复的错误,而我们这里的OnAuthenticationHelp方法是出现了可以回复的异常才会调用的。什么是可以恢复的异常呢?一个常见的例子就是:手指移动太快,当我们把手指放到传感器上的时候,如果我们很快地将手指移走的话,那么指纹传感器可能只采集了部分的信息,因此认证会失败。但是这个错误是可以恢复的,因此只要提示用户再次按下指纹,并且不要太快移走就可以解决。
  4. OnAuthenticationSucceeded(FingerprintManagerCompati.AuthenticationResult result)这个接口会在认证成功之后回调。我们可以在这个方法中提示用户认证成功。这里需要说明一下,如果我们上面在调用authenticate的时候,我们的CryptoObject不是null的话,那么我们在这个方法中可以通过AuthenticationResult来获得Cypher对象然后调用它的doFinal方法。doFinal方法会检查结果是不是会拦截或者篡改过,如果是的话会抛出一个异常。当我们发现这些异常的时候都应该将认证当做是失败来来处理,为了安全建议大家都这么做。
    关于上面的接口还有2点需要补充一下:

    1. 上面我们说道OnAuthenticationError 和 OnAuthenticationHelp方法中会有错误或者帮助码以提示为什么认证不成功。Android系统定义了几个错误和帮助码在FingerprintManager类中,如下:

      我们的callback类实现的时候最好需要处理这些错误和帮助码。

    2. 当指纹扫描器正在工作的时候,如果我们取消本次操作的话,系统也会回调OnAuthenticationError方法的,只是这个时候的错误码是FingerprintManager.FINGERPRINT_ERROR_CANCELED(值为5),因此app需要区别对待。

比如这是我写的自定义的AuthenticationCallback类

 class FingerAuthCallback extends FingerprintManagerCompat.AuthenticationCallback {@Overridepublic void onAuthenticationError(int errMsgId, CharSequence errString) {super.onAuthenticationError(errMsgId, errString);showError(errString);}@Overridepublic void onAuthenticationHelp(int helpMsgId, CharSequence helpString) {super.onAuthenticationHelp(helpMsgId, helpString);showError(helpString);}@Overridepublic void onAuthenticationSucceeded(FingerprintManagerCompat.AuthenticationResult result) {super.onAuthenticationSucceeded(result);mIcon.setImageResource(R.drawable.ic_fingerprint_success);mErrorTextView.setTextColor(ContextCompat.getColor(context, R.color.success_color));mErrorTextView.setText(context.getResources().getString(R.string.fingerprint_success));}@Overridepublic void onAuthenticationFailed() {super.onAuthenticationFailed();showError(context.getResources().getString(R.string.fingerprint_not_recognized));}}复制代码

额外补充

指纹解锁可以用这个Github上的开源的库:FingerprintAuthHelper
我使用了。起码我测试没问题。

谷歌的指纹解锁的Demo:FingerprintDialog (进入后点击右上角的download按钮,下载demo)


参考文章:
感谢createchance的 Android 6.0指纹识别App开发demo

项目需求讨论-APP手势解锁及指纹解锁相关推荐

  1. 排版 项目 html,项目需求讨论: 文字显示排版— Html 格式

    嗨,各位,今天来个小技巧,估计很多人都知道,我也就重复提下罢了.. 比如 升级更新框 通知提示框 我们看到,我用红框框出来的地方 1.直接使用系统自带的AlertDialog的提示框,我们看到了我们更 ...

  2. 安卓系统的指纹解锁_安卓手机指纹解锁 手机指纹解锁怎么用 无良小偷的克星 好好玩...

    现在人都比较在乎空间和安全,安全就不用说了,不管什么的安全都特别在意,越安全越好.就拿手机来说,现在的人都离不开的一个物件,哪怕自己不小心把手机遗忘在什么地方,也不希望别人看到自己手机中的任何个人信息 ...

  3. 项目需求讨论-标题栏上的搜索功能

    今天讲的就是一个很简单的具体开始时候遇到的需求,在标题栏中实现搜索功能,而且美工要求需要实现下面GIF图的效果,我就实现了下,可能不是最好的,有哪里可以更方便请大家指出.正好仔细的讲解了下Search ...

  4. android 仿ios 底部弹出,项目需求讨论-仿ios底部弹框实现及分析

    hi,在项目开发中,有时候需要仿照ios的底部弹框做效果,比如我们在iPhone上面关闭定位的时候,就会弹出ios特有的底部弹框: 屏幕快照 2017-10-09 08.20.30 PM.png 弹框 ...

  5. iOS指纹解锁和手势解锁

    前言 一直想写博客来着,一来可以记录一些自己学习和研究的东西,二来也可以将自己写的一些东西分享出去,给他人参考,还可能收到他人的一些建议,从而完善自己的项目和提升自己的技术,这也是一种很好的技术交流方 ...

  6. h5调用指纹识别_如何玩转指纹解锁H5插件?

    原标题:如何玩转指纹解锁H5插件? 科技发展到今天,生活中很多东西都被附上智能二字,手机.家电.家居.汽车等等很多方面,就连指纹也变得不一般了.指纹解锁最开始仅仅是在打卡签到上使用的,渐渐的人们保险安 ...

  7. android开发指纹解锁,Android-指纹解锁技术

    什么是指纹解锁技术 指纹解锁技术原理理解 指纹解锁技术的优势和缺点 在Android中的应用开发 什么是指纹解锁技术 根据人的指纹来验证是否能够解锁的技术,类似于通过输入密码来解锁,都是通过一定的数字 ...

  8. android 手势密码功能sdk,利用ActivityLifecycleCallBack监控app前后台状态切换,实现手势密码即九宫格解锁...

    转载注明出处:http://blog..net/coderder/article/details/51063493 利用ActivityLifecycleCallbacks监控app前后台状态切换,实 ...

  9. IPAD移动端交互原型通用设计方案、ipad元件库、移动元件库、元件列表、设计元件、交互示例、界面模板、设备模板、手势图标、社交界面、音乐、电商、视图控制器、指示器、指纹解锁、手势解锁、rp元件库

    IPAD移动端交互原型通用设计方案.ipad元件库.移动元件库.元件列表.设计元件.交互示例.界面模板.设备模板.手势图标.社交界面.音乐.电商.视图控制器.指示器.指纹解锁.手势解锁.rp元件库.平 ...

最新文章

  1. ie6和ie7两个div之间有空隙
  2. S5PV210开发 -- 烧写/启动模式
  3. php 检查字符串类型,PHP之字符串类型与检验
  4. content add tpl.php,phpcms后台批量上传添加图片文章方法详解(一)
  5. Android数据存储——SQLite数据库(模板)
  6. linux rm 命令删除文件恢复_rm删除文件空间就释放了吗?天真!
  7. Tableau 自定义调色板及应用全流程讲解【保姆级】
  8. switch芯片上的QoS,VLAN介绍
  9. 操作 神通数据库_神通数据库-快速入门指南 PDF 下载
  10. Aircrack 破解无线网密码 (跑字典法)
  11. voip和rtc_VoIP语音通话研究【进阶篇(四):freeswitch+webrtc+sip.js的通话】
  12. 位图深度 PIL.image.save()保存图片后size变大
  13. 工作流系统之四十 抄送功能的实现
  14. My favorite player-Davor Suker
  15. YOLOv4论文中文翻译
  16. 由于找不到vcruntime140_1.dll无法继续执行代码,vcruntime140_1.dll丢失如何修复
  17. 什么是分布式存储系统?
  18. gvfsd-trash占用内存高时,清空回收站
  19. C++ swap用法
  20. python xlwt

热门文章

  1. 计算机仿真技术与cad第三版课后答案,《计算机仿真技术与CAD》的习题答案.doc...
  2. Mysql 中文名称(包括字母)按首字母排序
  3. 【数据融合】基于AIS和雷达的多传感器航迹融合matlab代码
  4. WPS2019教育版和EndnoteX9关联
  5. 关于 CentOS 迁移龙蜥操作系统,这里有一份详细指南,请查收~
  6. Android手机4G网络设置ipv6
  7. java 推拉流_libsrt+ffmpeg推拉流(一)
  8. 创建单元测试-编写测试用例 and执行测试用例
  9. 美颜sdk是什么?美颜技术详解
  10. worksheets 和 sheets 的不同