我发现了 Android 指纹认证 Api 内存泄漏
我发现了 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 的手机,才可以查看,买的手机版本是不支持的)
😂 App 使用优化后(方案二)App 只存在一个 IBiometricServiceReceiver ,而 system 进程中存在三个 IBiometricServiceReceiver 的 binder proxy。 每次启动 BiometricPrompt 都会创建一个。这个就不解释为什么会出现三个binder proxy,感兴趣可以看下面推荐的文章。GC root 是 AuthSession。
再看下 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
参考资料
来源:juejin.cn/post/7202066794299129914