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

现在许多市面上的手机根本都有指纹登陆功用。Google 也供给了调用相关功用 API,安全类的App 也根本都在运用。接下来就一同捋一捋今天的主角 BiometricPrompt

先说问题,运用BiometricPrompt 会形成内存走漏,现在该问题试了 Android 11 到 13 都发生,并且没有什么好的办法。现在想到的最好的办法是漏的少一点。当然谁有好的办法欢迎留言。

问题再现

先看动画

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

动画中操作如下

  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 是局部变量,应该没有问题才对。

内存走漏如下

我发现了 Android 指纹认证 Api  内存泄漏
能够看到每启动一次生物认证,创立的 BiometricPrompt 都不会被收回。

躲避计划:

修正计划也简略

计划一:

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

计划一存在的问题,SecondActivity 可能频频创立,所以 biometricPromp 还会存在多个实例。

计划二(现在想到的最优计划):

  1. biometricPromp 改为单例
  2. this 改为 applicationContext

修正后,App memory 中只存在一个 biometricPromp ,且没有 Activity 被走漏。

想到这儿,应该会觉得古怪,biometricPromp 为什么不会被收回?供给的 API 都看过了,没有发现什么办法能够处理这个问题。直觉告诉我这个可能是体系问题,下来分析下BiometricPrompt 吧。

BiometricPrompt 源码分析

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

App 相关信息经过 BiometricPrompt 传递到 System 进程,System 进程再告诉 SystemUI 显示认证界面。

App 信息传递到 System 进程,应该会运用 Binder。这个查找 BiometricPrompt 运用哪些 Binder。

private final IBiometricServiceReceiver mBiometricServiceReceiver =
            new IBiometricServiceReceiver.Stub() {
        ......
}

源码中发现 IBiometricServiceReceiver 比较可疑,IBiometricServiceReceiver 是匿名内部类,内部是持有 BiometricPrompt 目标的引证。

接下来看下 System Server 进程信息(注:体系是 UserDebug 的手机,才能够检查,买的手机版本是不支持的)

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

App 运用优化后(计划二)App 只存在一个 IBiometricServiceReceiver ,而 system 进程中存在三个 IBiometricServiceReceiver 的 binder proxy。 每次启动 BiometricPrompt 都会创立一个。这个就不解说为什么会呈现三个binder proxy,感爱好能够看下面推荐的文章。GC root 是 AuthSession。

再看下 AuthSession 的实例数

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

公然 AuthSession 也存在三个。

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

这儿有个知识点,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 | 目标的生命周期