Catalin Ghita
Catalin Ghita

Reputation: 826

How to stop Activity leaks in Android

While switching from LogInActivity to CatalogActivity twice and finally returning to LogInActivity I have noticed through the Memory Monitor that GC is not de-allocating all the memory it should, therefore a leak is present.

When the app started ( atLogInActivity) the initial allocated memory was 40MB and as I cycled through the mentioned Activities, the final allocated memory was around 60MB ( atLogInActivity).

enter image description here Therefore around 20MB of precious memory remain allocated and used without purpose, this obviously being caused by a memory leak.

With the help of this post, while investigating using the CLI command:

adb shell dumpsys meminfo my_package_name

I noticed that after the Activities were cycled through, there were 3 instances of Activities and more of Context, as you can see in this picture

enter image description here

As this answer suggested, I used

intent.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);

but the leak still occurred.

What can be causing my memory leaks? Are the Activities indeed leaking? And how can I fix it?

LogInActivity

public class LogInActivity extends AppCompatActivity {

protected EditText emailEditText;
protected EditText passwordEditText;
protected Button logInButton;
protected TextView signUpTextView;
private FirebaseAuth mFirebaseAuth;
private CallbackManager callbackManager;
public static final String TAG = "LogInActivity";
private DatabaseReference mBookRef;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_log_in);


    if (CheckConnection.isNetworkConnected(this)) {
        // Initialize FirebaseAuth
        mFirebaseAuth = FirebaseAuth.getInstance();

        mBookRef = FirebaseDatabase
                .getInstance()
                .getReference();


        signUpTextView = (TextView) findViewById(R.id.signUpText);
        emailEditText = (EditText) findViewById(R.id.emailField);
        passwordEditText = (EditText) findViewById(R.id.passwordField);
        logInButton = (Button) findViewById(R.id.loginButton);


        logInButton.setTransformationMethod(null);

        callbackManager = CallbackManager.Factory.create();
        LoginButton loginButton = (LoginButton) findViewById(R.id.facebook_login_button);
        loginButton.setReadPermissions(Arrays.asList("email", "user_hometown", "public_profile"));

        // Register your callback//
        loginButton.registerCallback(callbackManager,

                // If the login attempt is successful, then call onSuccess and pass the LoginResult//
                new FacebookCallback<LoginResult>() {
                    @Override
                    public void onSuccess(LoginResult loginResult) {
                        // Print the user’s ID and the Auth Token to Android Studio’s Logcat Monitor//
                        Log.d(TAG, "User ID: " +
                                loginResult.getAccessToken().getUserId() + "\n" +
                                "Auth Token: " + loginResult.getAccessToken().getToken());

                        signInWithFacebook(loginResult, loginResult.getAccessToken());
                    }

                    // If the user cancels the login, then call onCancel//
                    @Override
                    public void onCancel() {
                    }

                    // If an error occurs, then call onError//
                    @Override
                    public void onError(FacebookException exception) {
                    }
                });


        signUpTextView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(LogInActivity.this, SignUpActivity.class);
                startActivity(intent);
            }
        });

        logInButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                String email = emailEditText.getText().toString();
                String password = passwordEditText.getText().toString();

                email = email.trim();
                password = password.trim();

                if (email.isEmpty() || password.isEmpty()) {
                    AlertDialog.Builder builder = new AlertDialog.Builder(LogInActivity.this);
                    builder.setMessage(R.string.login_error_message)
                            .setTitle(R.string.login_error_title)
                            .setPositiveButton(android.R.string.ok, null);
                    AlertDialog dialog = builder.create();
                    dialog.show();
                } else {
                    mFirebaseAuth.signInWithEmailAndPassword(email, password)
                            .addOnCompleteListener(LogInActivity.this, new OnCompleteListener<AuthResult>() {
                                @SuppressWarnings("ThrowableResultOfMethodCallIgnored")
                                @Override
                                public void onComplete(@NonNull Task<AuthResult> task) {
                                    if (task.isSuccessful()) {
                                        Intent intent = new Intent(LogInActivity.this, CatalogActivity.class);
                                        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                                        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
                                        startActivity(intent);
                                    } else {
                                        AlertDialog.Builder builder = new AlertDialog.Builder(LogInActivity.this);
                                        if (task.getException() != null) {
                                            builder.setMessage(task.getException().getMessage())
                                                    .setTitle(R.string.login_error_title)
                                                    .setPositiveButton(android.R.string.ok, null);
                                            AlertDialog dialog = builder.create();
                                            dialog.show();
                                        }


                                    }
                                }
                            });
                }
            }
        });

    } else {
        AlertDialog.Builder builder = new AlertDialog.Builder(LogInActivity.this, R.style.MyDialogTheme);
        builder.setMessage("Please check your internet connection and retry!")
                .setTitle(R.string.login_error_title).setCancelable(false)
                .setPositiveButton("Ok", new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        Intent intent = getIntent();
                        finish();
                        startActivity(intent);
                    }
                });
        AlertDialog dialog = builder.create();
        dialog.show();
    }

}


@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    callbackManager.onActivityResult(requestCode, resultCode, data);
}

private void signInWithFacebook(final LoginResult loginResulted, AccessToken token) {
    Log.d(TAG, "signInWithFacebook:" + token);

    AuthCredential credential = FacebookAuthProvider.getCredential(token.getToken());
    mFirebaseAuth.signInWithCredential(credential)
            .addOnCompleteListener(this, new OnCompleteListener<AuthResult>() {
                @Override
                public void onComplete(@NonNull Task<AuthResult> task) {
                    Log.d(TAG, "signInWithCredential:onComplete:" + task.isSuccessful());

                    // If sign in fails, display a message to the user. If sign in succeeds
                    // the auth state listener will be notified and logic to handle the
                    // signed in user can be handled in the listener.
                    if (!task.isSuccessful()) {
                        Log.w(TAG, "signInWithCredential", task.getException());
                        Toast.makeText(LogInActivity.this, "Authentication failed.",
                                Toast.LENGTH_SHORT).show();
                    } else {
                        final String uid = task.getResult().getUser().getUid();
                        String userName = task.getResult().getUser().getDisplayName();

                        GraphRequest request = GraphRequest.newMeRequest(
                                loginResulted.getAccessToken(),
                                new GraphRequest.GraphJSONObjectCallback() {
                                    @Override
                                    public void onCompleted(JSONObject object, GraphResponse response) {
                                        Log.v("LoginActivity", response.toString());

                                        // GET TOWN AND WRITE ON FIREBASE DATABASE
                                        try {
                                            JSONObject hometown = object.getJSONObject("hometown");
                                            String town = hometown.getString("name");

                                            mBookRef.child("user-books").child(uid).child("userLocation").setValue(town);

                                        } catch (JSONException e) {
                                            Log.e("BOOK_TRADE", "UNEXPECTED JSON EXCEPTION", e);
                                        }

                                    }
                                });
                        Bundle parameters = new Bundle();
                        parameters.putString("fields", "name,hometown");
                        request.setParameters(parameters);
                        request.executeAsync();


                        mBookRef.child("user-books").child(uid).child("userName").setValue(userName);

                        Intent intent = new Intent(LogInActivity.this, CatalogActivity.class);
                        intent.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
                        startActivity(intent);
                        finish();
                    }
                }
            });
}

@Override
protected void onDestroy() {
    super.onDestroy();

    unbindDrawables(findViewById(R.id.activity_log_in_root_view));
    System.gc();
}

private void unbindDrawables(View view) {
    if (view.getBackground() != null)
        view.getBackground().setCallback(null);

    if (view instanceof ImageView) {
        ImageView imageView = (ImageView) view;
        imageView.setImageBitmap(null);
    } else if (view instanceof ViewGroup) {
        ViewGroup viewGroup = (ViewGroup) view;
        for (int i = 0; i < viewGroup.getChildCount(); i++)
            unbindDrawables(viewGroup.getChildAt(i));

        if (!(view instanceof AdapterView))
            viewGroup.removeAllViews();
    }
   }
 }

CatalogActivity

public class CatalogActivity extends AppCompatActivity {
private ViewPager mViewPager;
private SectionsPageAdapter mSectionsPageAdapter;
private FirebaseAuth mFirebaseAuth;
private Context context;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    context = this;
    overridePendingTransition(R.anim.slidein, R.anim.slideout);

    setContentView(R.layout.activity_catalog);

    mFirebaseAuth = FirebaseAuth.getInstance();
    mSectionsPageAdapter = new SectionsPageAdapter(getSupportFragmentManager());
    mViewPager = (ViewPager) findViewById(R.id.container);
    setupViewPager(mViewPager);

    TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs);
    tabLayout.setupWithViewPager(mViewPager);
    int tabIcon1 = R.drawable.ic_collections_24dp;
    int tabIcon2 = R.drawable.ic_book_black_24dp;
    int tabIcon3 = R.drawable.ic_chat__24dp;

    if (tabLayout.getTabAt(0) != null) {
        tabLayout.getTabAt(0).setIcon(tabIcon1);
    }
    if (tabLayout.getTabAt(1) != null) {
        tabLayout.getTabAt(1).setIcon(tabIcon2);
        tabLayout.getTabAt(1).getIcon().setAlpha(128);
    }
    if (tabLayout.getTabAt(2) != null) {
        tabLayout.getTabAt(2).setIcon(tabIcon3);
        tabLayout.getTabAt(2).getIcon().setAlpha(128);
    }

    tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
        @Override
        public void onTabSelected(TabLayout.Tab tab) {
            int tabIconColor = ContextCompat.getColor(context, R.color.colorAccent);
            if (tab.getIcon() != null) {
                tab.getIcon().setColorFilter(tabIconColor, PorterDuff.Mode.SRC_IN);
                tab.getIcon().setAlpha(255);
            }

        }

        @Override
        public void onTabUnselected(TabLayout.Tab tab) {
            int tabIconColor = ContextCompat.getColor(context, R.color.grey);
            if (tab.getIcon() != null) {
                tab.getIcon().setColorFilter(tabIconColor, PorterDuff.Mode.SRC_IN);
                tab.getIcon().setAlpha(128);
            }
        }

        @Override
        public void onTabReselected(TabLayout.Tab tab) {

        }
    });

    ViewServer.get(this).addWindow(this);
}

@Override
protected void onDestroy() {
    super.onDestroy();
    ViewServer.get(this).removeWindow(this);
}


private void setupViewPager(ViewPager viewPager) {
    SectionsPageAdapter adapter = new SectionsPageAdapter(getSupportFragmentManager());
    adapter.addFragment(new CatalogFragment());
    adapter.addFragment(new MyBooksfragment());
    adapter.addFragment(new MyChatsFragment());
    viewPager.setAdapter(adapter);
}


@Override
public boolean onCreateOptionsMenu(Menu menu) {
    MenuInflater inflater = getMenuInflater();
    inflater.inflate(R.menu.chat_menu, menu);
    return true;
}

@Override
public boolean onOptionsItemSelected(MenuItem item) {
    int id = item.getItemId();

    //noinspection SimplifiableIfStatement
    if (id == R.id.sign_out_menu) {
        LoginManager.getInstance().logOut();
        mFirebaseAuth.signOut();
        loadLogInView();
    }


    return super.onOptionsItemSelected(item);
}

private void loadLogInView() {
    Intent intent = new Intent(this, LogInActivity.class);
       //        intent.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK);
    startActivity(intent);
    }
}

Note: I use this (unorthodox) method to clean any bitmap-related memory leaks in my fragments:

private void unbindDrawables(View view) {
    if (view.getBackground() != null)
        view.getBackground().setCallback(null);

    if (view instanceof ImageView) {
        ImageView imageView = (ImageView) view;
        imageView.setImageBitmap(null);
    } else if (view instanceof ViewGroup) {
        ViewGroup viewGroup = (ViewGroup) view;
        for (int i = 0; i < viewGroup.getChildCount(); i++)
            unbindDrawables(viewGroup.getChildAt(i));

        if (!(view instanceof AdapterView))
            viewGroup.removeAllViews();
    }
}

Upvotes: 0

Views: 2031

Answers (1)

Crispert
Crispert

Reputation: 1167

It seems that in most cases you are not finishing the running activities before starting other ones. Just setting FLAG_ACTIVITY_NEW_TASK, FLAG_ACTIVITY_CLEAR_TASK will cause the new activity to be open in a new task, while leaving the old activity running in the existing task. Eventually you'll end up with one instance of each activity class (Login, Signup, Catalog) running in its own task, as dumpsys is showing. You can check whether that's the case by looking at the list of running activities on your device.

The simple solution - if you don't want activities in background - would be to finish the current activity after starting a new one. For example when going from LoginActivity to SignupActivity you would finish LoginActivity and start SignupActivity. If the user navigates back you finish SignupActivity and start LoginActivity once again. Note however, that you need not do this because Android will automatically close background activities if low on resources and reopen them if you go back in the activity stack.

Upvotes: 1

Related Questions