Memory leak with Firebase Android PhoneAuthProvider

Viewed 172

Firebase -> PhoneAuthProvider -> VerifyPhoneNumber is leaking. I believe, it might be OnVerificationStateChangedCallbacks, which we are sending on call to verifyPhoneNumber.

Steps to reproduce:

  1. Launch the app
  2. Select "PhoneAuthActivity" for phone based authentication
  3. Send Phone Number.
  4. Click back.

When clicking back, the leaked memory appear

Does someone have the same problem? any solution?

public void FirebasePhoneUser(String phoneNumber) {
            mCallback = new PhoneAuthProvider.OnVerificationStateChangedCallbacks() {
                @Override
                public void onVerificationCompleted(PhoneAuthCredential phoneAuthCredential) {
                    Log.d("Completed","");
                }
                @Override
                public void onVerificationFailed(FirebaseException e) {
                    Log.d("Error","");
                }

                @Override
                public void onCodeSent(String verificationId,
                                  PhoneAuthProvider.ForceResendingToken forceResendingToken) {
                    Log.d("onCodeSent", "");
                }
            };
            phoneAuthProvider = PhoneAuthProvider.getInstance();
            phoneAuthProvider.verifyPhoneNumber(
                    phoneNumber,
                    30,
                    TimeUnit.SECONDS,
                    TaskExecutors.MAIN_THREAD,
                    mCallback
            );
}
1 Answers

Given that API is terrible and there's no option to unsubscribe, you have several options to work this around.

  1. Proxy or Decorator. You create another OnVerificationStateChangedCallbacks which delegates method calls to another instance:
// this class must be either top-level or 'static'!
public /*static*/ final class DelegatingVerificationStateCallbacks
    extends PhoneAuthProvider.OnVerificationStateChangedCallbacks
    implements Closeable {
    @Nullable private PhoneAuthProvider.OnVerificationStateChangedCallbacks delegate;
    public DelegatingVerificationStateCallbacks(
        @NonNull PhoneAuthProvider.OnVerificationStateChangedCallbacks delegate
    ) {
        this.delegate = delegate;
    }

    @Override public void onCodeSent(
        @NonNull String verificationId,
        @NonNull PhoneAuthProvider.ForceResendingToken forceResendingToken
    ) {
        if (delegate != null) delegate.onCodeSent(verificationId, forceResendingToken);
    }
    @Override public void onCodeAutoRetrievalTimeOut(@NonNull String s) {
        if (delegate != null) delegate.onCodeAutoRetrievalTimeOut(s);
    }
    @Override public void onVerificationCompleted(@NonNull PhoneAuthCredential phoneAuthCredential) {
        if (delegate != null) delegate.onVerificationCompleted(phoneAuthCredential);
    }
    @Override public void onVerificationFailed(@NonNull FirebaseException e) {
        if (delegate != null) delegate.onVerificationFailed(e);
    }

    @Override public void close() {
        delegate = null;
    }
}

I've implemented Closeable for cleanup but you could implement RxJava's Disposable or whatever instead.

The usage pattern here is obvious and well-known:

public final class SomeScreen extends ActivityOrFragmentOrControllerOrWhatever {
    private final ArrayList<Closeable> disposeBag = new ArrayList<>();
    private void performAuth() {
        DelegatingVerificationStateCallbacks callbacks =
            new DelegatingVerificationStateCallbacks(
                new OnVerificationStateChangedCallbacks() { … }
            );
        disposeBag.add(callbacks);
        phoneAuthProvider.verifyPhoneNumber(…, callbacks);
    }
    @Override protected void onDestroy() {
        for (Closeable c : disposeBag) {
            try { c.close(); }
            catch (IOException ignored) { }
        }
        disposeBag.clear();
    }
}

Result: Firebase leaks a reference to empty and cheap DelegatingVerificationStateCallbacks, not to Activity.

  1. Nulling references out yourself. You can take approach presented above to clear your own references to Activity. This implies these reference must be explicit, i. e. class must not be anonymous or inner to your activity. You must take full control of class constructor and fields, top-level class or nested static class is a good fit.

  2. Weak reference. This is less explicit and involves some indirection but still works: you instantiate top-level or nested static class, pass Activity to constructor, wrap it in a WeakReference, and assign to a field. That's all, after some time WeakReference#get will start returning null.

  3. Reflection. Very bad and unstable option which could help in some other situations. Sometimes your Activity could be leaked by Android SDK or vendor-specific code, and options from above don't apply. Then you can null out some private fields yourself. Don't do this for Firebase.

Related