注册

我发现了 Android 指纹认证 Api 内存泄漏

我发现了 Android 指纹认证 Api 内存泄漏


目前很多市面上的手机基本都有指纹登陆功能。Google 也提供了调用相关功能 API,安全类的App 也基本都在使用。接下来就一起捋一捋今天的主角 BiometricPrompt


先说问题,使用BiometricPrompt 会造成内存泄漏,目前该问题试了 Android 11 到 13 都发生,而且没有什么好的办法。目前想到的最好的方法是漏的少一点。当然谁有好的办法欢迎留言。


问题再现


先看动画


在这里插入图片描述


动画中操作如下



  1. MainAcitivity 跳转到 SecondActivity
  2. SecondActivity 调用 BiometricPrompt 三次
  3. 从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 都不会被回收。


规避方案:


修改方案也简单


方案一:



  1. biometricPromp 改为全局变量。
  2. this 改为 applicationContext

方案一存在的问题,SecondActivity 可能频繁创建,所以 biometricPromp 还会存在多个实例。


方案二(目前想到的最优方案):



  1. biometricPromp 改为单例
  2. 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 这篇文章也是解释了的。有兴趣的可以了看下。


Binder | 对象的生命周期


一开始,我以为 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


参考资料


Binder | 对象的生命周期


作者:Jingle_zhang
来源:juejin.cn/post/7202066794299129914

0 个评论

要回复文章请先登录注册