#android #memory-leaks #leakcanary
#Android #утечки памяти #утечка
Вопрос:
Я мог видеть, что leakcanary жалуется на утечку памяти в FingerprintManager.mContext в приведенном ниже коде. Я пробовал статический внутренний класс, слабые ссылки в AsyncTask. Не удалось найти код, создающий утечку памяти.
class BiometricHelper(var localListener: BiometricHelperListener, val fragmentActivity: FragmentActivity) : BiometricPrompt.AuthenticationCallback() {
private val mListener: WeakReference<BiometricHelper.BiometricHelperListener> = WeakReference(localListener)
private val mFragmentActivity: WeakReference<FragmentActivity> = WeakReference(fragmentActivity)
val mEditor: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(TransfastApplication.getContext())
private val TAG = BiometricHelper::class.java!!.getName()
private var mkeyStore: KeyStore? = null
private var mgenerator: KeyGenerator? = null
private var mcipher: Cipher? = null
private val KEYSTORE = "somevalue"
private val PREFERENCES_KEY_IV = "somevalue"
private val PREFERENCES_KEY_PASS = "somevalue"
private var mcryptoObject: BiometricPrompt.CryptoObject? = null
private var encrypting: Boolean = false
private var mEmail: String? = null
private var mPassword: String? = null
private var mAuthTask: BiometricHelper.UserLoginTask? = null
private lateinit var myBiometricPrompt: BiometricPrompt
interface BiometricHelperListener {
fun onAuthenticationSuccessful(result: BiometricPrompt.AuthenticationResult)
fun onSavedUserAuthenticationSuccessful(password: String)
fun onAuthenticationFailed(error: String)
fun onFingerPrintRegistrationDenied()
fun onNoBiometricSupport()
}
public fun registerAuthentication(email: String, password: String) {
this.mEmail = email;
this.mPassword = password;
mAuthTask = UserLoginTask(this, mEditor, email, password)
mAuthTask?.execute(null as Void?)
}
public fun authenticateExistingUser(email: String) {
this.mEmail = email
mAuthTask = UserLoginTask(this, mEditor);
mAuthTask?.execute(null as Void?)
}
private fun startAuth(isRegister: Boolean) {
val executor = Executors.newSingleThreadExecutor()
myBiometricPrompt = BiometricPrompt(mFragmentActivity.get()!!, executor, this)
if (mcryptoObject != null) {
if (isRegister) {
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("Enable Fingerprint")
.setSubtitle(mFragmentActivity.get()!!.resources.getString(R.string.fingerprint_confirm_message))
//.setDescription(mFragmentActivity.get()!!.resources.getString(R.string.fingerprint_confirm_description))
.setNegativeButtonText(mFragmentActivity.get()!!.resources.getString(R.string.text_cancel))
.build()
myBiometricPrompt.authenticate(promptInfo, mcryptoObject!!)
} else {
val promptInfo = BiometricPrompt.PromptInfo.Builder()
.setTitle("Login")
.setSubtitle(mEmail)
.setNegativeButtonText(mFragmentActivity.get()!!.resources.getString(R.string.text_cancel))
.build()
myBiometricPrompt.authenticate(promptInfo, mcryptoObject!!)
}
}
}
public fun isUserRegistered(): Boolean {
if (mEditor.getString(PREFERENCES_KEY_PASS, null) != null) {
return true;
}
return false;
}
public fun checkBiometricSupport(): Boolean {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) //currently support library does not support Android M
return false;
val keyguardManager = mFragmentActivity.get()?.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
if (!keyguardManager.isKeyguardSecure) {
false
}
val fingerprintManagerCompat = FingerprintManagerCompat.from(mFragmentActivity.get()!!)
if (!fingerprintManagerCompat.hasEnrolledFingerprints()) {
return false;
}
if (ActivityCompat.checkSelfPermission(mFragmentActivity.get()!!,
Manifest.permission.USE_BIOMETRIC) != PackageManager.PERMISSION_GRANTED) {
false
}
return if (mFragmentActivity.get()!!.packageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) {
true
} else true
}
private fun getKeyStore(): Boolean {
try {
mkeyStore = KeyStore.getInstance(KEYSTORE)
mkeyStore?.load(null) // Create empty keystore
return true
} catch (e: KeyStoreException) {
} catch (e: CertificateException) {
} catch (e: NoSuchAlgorithmException) {
} catch (e: IOException) {
}
return false
}
@TargetApi(Build.VERSION_CODES.M)
fun createNewKey(forceCreate: Boolean): Boolean {
try {
if (forceCreate)
mkeyStore?.deleteEntry(PrefManager.getInstance().keyAlias)
if (!mkeyStore?.containsAlias(PrefManager.getInstance().keyAlias)!!) {
mgenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, KEYSTORE)
mgenerator?.init(KeyGenParameterSpec.Builder(PrefManager.getInstance().keyAlias,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
.setUserAuthenticationRequired(true)
.build()
)
mgenerator?.generateKey()
}
return true
} catch (e: Exception) {
}
return false
}
private fun getCipher(): Boolean {
try {
mcipher = Cipher.getInstance(
KeyProperties.KEY_ALGORITHM_AES "/"
KeyProperties.BLOCK_MODE_CBC "/"
KeyProperties.ENCRYPTION_PADDING_PKCS7)
return true
} catch (e: NoSuchAlgorithmException) {
} catch (e: NoSuchPaddingException) {
}
return false
}
public fun isNewFingerprintEnrolled(): Boolean {
getKeyStore()
getCipher()
return !initCipher(Cipher.DECRYPT_MODE);
}
@TargetApi(Build.VERSION_CODES.M)
public fun initCipher(mode: Int): Boolean {
try {
mkeyStore?.load(null)
val keyspec = mkeyStore?.getKey(PrefManager.getInstance().keyAlias, null) as SecretKey
if (mode == Cipher.ENCRYPT_MODE) {
mcipher?.init(mode, keyspec)
var localEditor = mEditor.edit()
localEditor.putString(PREFERENCES_KEY_IV, Base64.encodeToString(mcipher?.getIV(), Base64.NO_WRAP)).commit()
} else {
val iv = Base64.decode(mEditor.getString(PREFERENCES_KEY_IV, ""), Base64.NO_WRAP)
val ivspec = IvParameterSpec(iv)
mcipher?.init(mode, keyspec, ivspec)
}
return true
} catch (e: Exception) {
clearSavedData()
}
return false
}
@TargetApi(Build.VERSION_CODES.M)
private fun initCryptObject(): Boolean {
try {
mcryptoObject = mcipher?.let { BiometricPrompt.CryptoObject(it) }
return true
} catch (ex: Exception) {
}
return false
}
fun encryptString(password: String) {
try {
val bytes = mcipher?.doFinal(password.toByteArray())
val encryptedText = Base64.encodeToString(bytes, Base64.NO_WRAP)
var localEditor = mEditor.edit()
localEditor.putString(PREFERENCES_KEY_PASS, encryptedText).commit()
} catch (e: Exception) {
clearSavedData()
}
}
fun decryptString(cipherText: String): String? {
try {
val bytes = Base64.decode(cipherText, Base64.NO_WRAP)
val finalText = String(mcipher!!.doFinal(bytes))
return finalText;
} catch (e: Exception) {
clearSavedData()
}
return null;
}
class UserLoginTask : AsyncTask<Void, Void, Boolean> {
private val mEmail: String?
private val mPassword: String?
private val mRegister: Boolean? // if false, authenticate instead
private var biometricHelper: WeakReference<BiometricHelper>
private var editor: WeakReference<SharedPreferences>
internal constructor(biometricHelper: BiometricHelper,
editor: SharedPreferences,
email: String, password: String) {
this.biometricHelper = WeakReference<BiometricHelper>(biometricHelper)
this.editor = WeakReference<SharedPreferences>(editor)
mEmail = email
mPassword = password
mRegister = true
}
internal constructor(biometricHelper: BiometricHelper,
editor: SharedPreferences) {
this.biometricHelper = WeakReference(biometricHelper)
this.editor = WeakReference(editor)
mRegister = false
mEmail = null
mPassword = null
}
override fun doInBackground(vararg params: Void): Boolean? {
if (!biometricHelper.get()!!.getKeyStore())
return false
if (!biometricHelper.get()!!.createNewKey(false))
return false
if (!biometricHelper.get()!!.getCipher())
return false
// Inside doInBackground
if (mRegister!!) {
biometricHelper.get()!!.encrypting = true
if (!biometricHelper.get()!!.initCipher(Cipher.ENCRYPT_MODE))
return false
} else {
biometricHelper.get()!!.encrypting = false
if (!biometricHelper.get()!!.initCipher(Cipher.DECRYPT_MODE))
return false
}
return if (!biometricHelper.get()!!.initCryptObject()) false else true
}
override fun onPostExecute(success: Boolean) {
if (success amp;amp; !isCancelled) {
mRegister?.let { biometricHelper.get()!!.startAuth(it) }
} else {
//something not right proceed with normal login
biometricHelper.get()!!.onFingerprintRegistrationDenied()
}
}
}
fun onFingerprintRegistrationDenied() {
mListener.get()?.onFingerPrintRegistrationDenied()
}
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
// super.onAuthenticationError(errorCode, errString)
Logger.d("onAuthenticationError **** " errorCode "*** " errString)
if ((errorCode == BiometricPrompt.ERROR_NEGATIVE_BUTTON || errorCode == BiometricPrompt.ERROR_USER_CANCELED) amp;amp; encrypting) {
mListener.get()?.onFingerPrintRegistrationDenied()
} else if (errorCode == BiometricPrompt.ERROR_CANCELED || errorCode == BiometricPrompt.ERROR_LOCKOUT || errorCode == BiometricPrompt.ERROR_LOCKOUT_PERMANENT) {
myBiometricPrompt.cancelAuthentication()
mListener.get()?.onAuthenticationFailed(errString.toString())
} else {
super.onAuthenticationError(errorCode, errString)
// listener.onAuthenticationFailed(errString.toString())
}
}
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
if (encrypting) {
mPassword?.let { encryptString(it) }
mListener.get()?.onAuthenticationSuccessful(result)
} else {
var encryptedPassword = mEditor.getString(PREFERENCES_KEY_PASS, "");
val decryptedPassword = decryptString(encryptedPassword)
if (decryptedPassword != null) {
mListener.get()?.onSavedUserAuthenticationSuccessful(decryptedPassword)
} else {
mListener.get()?.onAuthenticationFailed(SignInFragment.FINGERPRINT_ERROR)
}
}
}
fun clearSavedData() {
var localEditor = mEditor.edit()
localEditor.remove(PREFERENCES_KEY_IV).commit();
localEditor.remove(PREFERENCES_KEY_PASS).commit();
try {
mkeyStore?.deleteEntry(PrefManager.getInstance().keyAlias);
} catch (ex: java.lang.Exception) {
} finally {
PrefManager.getInstance().remove(PrefManager.Key.PREF_KEY_ALIAS)
}
}
fun onDestroy() {
if (mAuthTask != null) {
mAuthTask?.cancel(true);
mAuthTask = null
}
}
}
Прилагаю скриншот сообщения от leakcanary.
Класс UserLoginTask используется для регистрации, а также для входа пользователя. Класс BiometricHelper используется для аутентификации по отпечаткам пальцев.
Комментарии:
1. Сохранение действий / контекстов в виде полей в классе обычно является для меня тревожным сигналом. В вашем случае вы передаете действие классу BiometricHelper, который он сохраняет как поле. Затем экземпляр этого класса сохраняется внутри запущенной AsyncTask. Утечка здесь может заключаться в том, что AsyncTask сохраняет ссылку на BiometricHelper, которая будет сохранять ссылку на действие до тех пор, пока выполняется задача. Это приводит к тому, что активность выходит за рамки обычного жизненного цикла, так что это может квалифицироваться как утечка памяти.