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

1,377 阅读4分钟

我发现了 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 | 对象的生命周期