我发现了 Android 指纹认证 Api 内存泄漏
目前很多市面上的手机基本都有指纹登陆功能。Google 也提供了调用相关功能 API,安全类的App 也基本都在使用。接下来就一起捋一捋今天的主角 BiometricPrompt
先说问题,使用BiometricPrompt
会造成内存泄漏,目前该问题试了 Android 11 到 13 都发生,而且没有什么好的办法。目前想到的最好的方法是漏的少一点。当然谁有好的办法欢迎留言。
问题再现
先看动画
动画中操作如下
- MainAcitivity 跳转到 SecondActivity
- SecondActivity 调用
BiometricPrompt
三次 - 从SecondActivity 返回到 MainAcitivity
以下是使用 BiometricPrompt
的代码
public fun showBiometricPromptDialog() {
val keyguardManager = getSystemService(
Context.KEYGUARD_SERVICE
) as KeyguardManager;
if (keyguardManager.isKeyguardSecure) {
var biometricPromptBuild = BiometricPrompt.Builder(this).apply {// this is SecondActivity
setTitle("verify")
setAllowedAuthenticators(BiometricManager.Authenticators.DEVICE_CREDENTIAL or BiometricManager.Authenticators.BIOMETRIC_WEAK)
}
val biometricPromp = biometricPromptBuild.build()
biometricPromp.authenticate(CancellationSignal(), mExecutor, object :
BiometricPrompt.AuthenticationCallback() {
})
}
else {
Log.d("TAG", "showLockScreen: isKeyguardSecure is false");
}
}
以上逻辑 biometricPromp 是局部变量,应该没有问题才对。
内存泄漏如下
可以看到每启动一次生物认证,创建的
BiometricPrompt
都不会被回收。
规避方案:
修改方案也简单
方案一:
- biometricPromp 改为全局变量。
- this 改为 applicationContext
方案一存在的问题,SecondActivity 可能频繁创建,所以 biometricPromp 还会存在多个实例。
方案二(目前想到的最优方案):
- biometricPromp 改为单例
- this 改为 applicationContext
修改后,App memory 中只存在一个 biometricPromp ,且没有 Activity 被泄漏。
想到这里,应该会觉得奇怪,biometricPromp 为什么不会被回收?提供的 API 都看过了,没有发现什么方法可以解决这个问题。直觉告诉我这个可能是系统问题,下来分析下BiometricPrompt
吧。
BiometricPrompt 源码分析
App 相关信息通过 BiometricPrompt
传递到 System 进程,System 进程再通知 SystemUI 显示认证界面。
App 信息传递到 System 进程,应该会使用 Binder。这个查找 BiometricPrompt
使用哪些 Binder。
private final IBiometricServiceReceiver mBiometricServiceReceiver =
new IBiometricServiceReceiver.Stub() {
......
}
源码中发现 IBiometricServiceReceiver 比较可疑,IBiometricServiceReceiver 是匿名内部类,内部是持有 BiometricPrompt
对象的引用。
接下来看下 System Server 进程信息(注:系统是 UserDebug 的手机,才可以查看,买的手机版本是不支持的)
再看下 AuthSession 的实例数
果然 AuthSession 也存在三个。
这里有个知识点,binder 也是有生命周期的,三个 Proxy 这篇文章也是解释了的。有兴趣的可以了看下。
一开始,我以为 AuthSession 没有被置空,看下代码,发现 AOSP 的代码,还是比较严谨的,有置空的操作。
细心的同学发现,上图中 AuthSession 没有被任何对象引用,AuthSession 就是 GC Root,哈哈哈。
问题解密
一个实例什么情况可以作为GC Root,有兴趣的同学,可以自行百度,这里就不卖关子了,直接说问题吧。
Binder.linkToDeath()
public void linkToDeath(@NonNull DeathRecipient recipient, int flags) {
}
需要传递 IBinder.DeathRecipient ,这个 DeathRecipient 会被作为 GC root。当调用 unlinkToDeath(@NonNull DeathRecipient recipient, int flags)
,GC root 才被收回。
AuthSession 初始化的时候,会调用 IBiometricServiceReceiver .linkToDeath。
public final class AuthSession implements IBinder.DeathRecipient {
AuthSession(@NonNull Context context,
......
@NonNull IBiometricServiceReceiver clientReceiver,
......
) {
Slog.d(TAG, "Creating AuthSession with: " + preAuthInfo);
......
try {
mClientReceiver.asBinder().linkToDeath(this, 0 /* flags */);//this 变成 GC root
} catch (RemoteException e) {
Slog.w(TAG, "Unable to link to death");
}
setSensorsToStateUnknown();
}
}
Jni 中 通过 env->NewGlobalRef(object),告诉虚拟机 AuthSession 是 GC Root。
core/jni/android_util_Binder.cpp
static void android_os_BinderProxy_linkToDeath(JNIEnv* env, jobject obj,
jobject recipient, jint flags) // throws RemoteException
{
if (recipient == NULL) {
jniThrowNullPointerException(env, NULL);
return;
}
BinderProxyNativeData *nd = getBPNativeData(env, obj);
IBinder* target = nd->mObject.get();
LOGDEATH("linkToDeath: binder=%p recipient=%p\n", target, recipient);
if (!target->localBinder()) {
DeathRecipientList* list = nd->mOrgue.get();
sp<JavaDeathRecipient> jdr = new JavaDeathRecipient(env, recipient, list);//java 中 DeathRecipient 会被封装为 JavaDeathRecipient
status_t err = target->linkToDeath(jdr, NULL, flags);
if (err != NO_ERROR) {
// Failure adding the death recipient, so clear its reference
// now.
jdr->clearReference();
signalExceptionForError(env, obj, err, true /*canThrowRemoteException*/);
}
}
}
JavaDeathRecipient(JNIEnv* env, jobject object, const sp<DeathRecipientList>& list)
: mVM(jnienv_to_javavm(env)), mObject(env->NewGlobalRef(object)),// object -> DeathRecipient 变为 GC root
mObjectWeak(NULL), mList(list)
{
// These objects manage their own lifetimes so are responsible for final bookkeeping.
// The list holds a strong reference to this object.
LOGDEATH("Adding JDR %p to DRL %p", this, list.get());
list->add(this);
gNumDeathRefsCreated.fetch_add(1, std::memory_order_relaxed);
gcIfManyNewRefs(env);
}
unlinkToDeath
最终会在 Jni 中 通过 env->DeleteGlobalRef(mObject),告诉虚拟机 AuthSession 不是GC root。
virtual ~JavaDeathRecipient()
{
//ALOGI("Removing death ref: recipient=%p\n", mObject);
gNumDeathRefsDeleted.fetch_add(1, std::memory_order_relaxed);
JNIEnv* env = javavm_to_jnienv(mVM);
if (mObject != NULL) {
env->DeleteGlobalRef(mObject);// object -> DeathRecipient GC root 被撤销
} else {
env->DeleteWeakGlobalRef(mObjectWeak);
}
}
解决方式
AuthSession 置空的时候调用 IBiometricServiceReceiver 的 unlinkToDeath 方法。
总结
以上梳理的其实就是 Binder 的造成的内存泄漏。
问题严重性来看,也不算什么大问题,因为调用 BiometricPrompt
的进程被杀,system 进程相关实例也就回收释放了。一般 app 也不太可能出现,常驻进程,而且还频繁调用手机认证的。
这里主要介绍了一种容易被忽略的内存泄漏,Binder.linktoDeath()。 Google issuetracker