3

I think my app is taking up extra memory that the GC should be able to reallocate. I don't know if these will be considered memory leaks, but there are 2 places that I have noticed possible issues

  • From App start, continuously rotate my device from portrait to landscape to portrait to landscape....

    • +300KB cumulative memory usage per rotate
  • With 2 inputs, every button click

    • +30KB cumulative memory usage per click

This issue is that the memory is never released as long as the app is still in view.

Example: Rotate the device 10 times, and click the buttons 50 times -> consumes 4.5MB memory. If I leave the app open and don't do anything for 1 hour, then my app will still consume 4.5MB of memory; even though a lot of the memory should have been released around 59 minutes earlier!!

My concern is why the memory is never released while the app is always in view?

Am I wrong in how this works?

NOTE: The app is called ContrivedCalculator

Code

UI

public class Calculator extends AppCompatActivity implements ICalculatorInteraction {

    private EditText txtNumber1, txtNumber2, txtResult;

    @Override
    protected void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_calculator);

        Button btnAdd = (Button) findViewById(R.id.btnAddition);
        Button btnSub = (Button) findViewById(R.id.btnSubtract);
        Button btnMul = (Button) findViewById(R.id.btnMultiple);
        Button btnDiv = (Button) findViewById(R.id.btnDivide);

        txtNumber1 = (EditText) findViewById(R.id.txtNumber1);
        txtNumber2 = (EditText) findViewById(R.id.txtNumber2);
        txtResult = (EditText) findViewById(R.id.txtResult);

        btnAdd.setOnClickListener(new OperationClick(Add).listenerOn(this));   
        btnSub.setOnClickListener(new OperationClick(Subtract).listenerOn(this));
        btnMul.setOnClickListener(new OperationClick(Multiply).listenerOn(this));
        btnDiv.setOnClickListener(new OperationClick(Divide).listenerOn(this));

    @Override
    public String getFirstNumber() { return valueOf(this.txtNumber1); }

    @Override
    public String getSecondNumber() { return valueOf(this.txtNumber2); }

    @Override
    public void updateResult(String result) { this.txtResult.setText(result); }

    private String valueOf(EditText textbox) {

        String text = textbox.getText().toString();

        if (text.isEmpty()) {

            textbox.setText("0");
            return "0";

        }

        return text;
    }

// default android activity methods

}

Listener Logic

public class OperationClick {

    private BinaryOperation operation;   // ENUM - advanced
    private View.OnClickListener listener;

    public OperationClick(final BinaryOperation operation) { this.operation = operation; }

    public View.OnClickListener listenerOn(final ICalculatorInteraction UI) {

        if (listener != null) return listener;
        return listener = new View.OnClickListener() {

            @Override
            public void onClick(View v) {

                double  num1, num2, total;
                String result, sign;

                num1 = Double.parseDouble(UI.getFirstNumber());
                num2 = Double.parseDouble(UI.getSecondNumber());
                total = operation.execute(num1, num2);
                sign = operation.getSymbol();

                result = String.format("%s %s %s = %s", num1, sign, num2, total);

                UI.updateResult(result);

            }

        };

    }

Calculation Logic

public enum BinaryOperation {

    Add      ("+") { @Override double execute(final double a, final double b) { return a + b; } },
    Subtract ("-") { @Override double execute(final double a, final double b) { return a - b; } },
    Multiply ("×") { @Override double execute(final double a, final double b) { return a * b; } },
    Divide   ("÷") { @Override double execute(final double a, final double b) { return a / b; } };

    private final String symbol;

    abstract double execute(double a, double b);

    BinaryOperation(String symbol) { this.symbol = symbol; }

    public String getSymbol() { return this.symbol; }

}
Christopher Rucinski
  • 4,737
  • 2
  • 27
  • 58
  • Forcing GC frees the memory or the usage is still there? – eduyayo May 10 '15 at 19:37
  • 41 minutes into the debug session with no other usage, forcing a GC will remove the memory. But why hasn't a GC been called in the 41 minutes in Android Studio memory tools console? – Christopher Rucinski May 10 '15 at 19:41
  • Not too much usage... If the program keeps running it will go creating and deleting stuff in memory, fragmentation will force gc eventually. As long as it keeps low memory usage, there's no reason why – eduyayo May 10 '15 at 19:44
  • Take a look at this http://stackoverflow.com/questions/1582209/java-garbage-collector-when-does-it-collect – eduyayo May 10 '15 at 19:45

1 Answers1

1

Tracking memory leaks is best done with Eclipse MAT (don't worry about the name, it works great with memory dumps from Android Studio as well). The tool has a pretty steep learning curve, but it's the most powerful thing you can imagine for tracking such memory problems.

Indeed you're causing a memory leak in this line:

btnAdd.setOnClickListener(new OperationClick(Add).listenerOn(this));

The problem is that the instance of OperationClick holds a reference to the activity, as it's passed with .listenerOn(this).

When you rotate the device you know that the Activity is recreated. I assume you're not clearing the listeners in onDestroy() method so you end up with a leaked Activity (4 times per rotate to be correct).

Btw last week Leak Canary was released by the awesome dudes from SquareUp. It's a nifty library for finding memory leaks on Android. The first library of this sort, I strongly recommend you try it!

EDIT: To fix your leak don't use anonymous OperationClick objects. Also add a "cleanup" method in them where you get rid of the listener you've created in listenerOn().

Vesko
  • 3,750
  • 2
  • 23
  • 29
  • Actually, LeakCanary is kind of why I posted this. I got one leak stating the anonymous click listener was leaking.aftrr looking I figured it was because of the passed in 'this' parameter. Before I did have an 'onDestroy' method that did this 'btnAdd.setOnClickListener(null);' It was called -verified by breakpoint - but no memory was released. So I removed the method – Christopher Rucinski May 10 '15 at 19:56
  • Added a suggestion how to fix your memory leak .. if you haven't done already :) – Vesko May 10 '15 at 20:06
  • just for quick testing, I created 4 OperationClick state objects. and did this... 'btnAdd.setOnClickListener((add = new OperationClick(Add)).listenerOn(this));' Then I overrided the 'onDestroy' method and called 'add = null;' This still didn't remove any memory. So I have to remove the listener inside 'OperationClick' class? Not just nullify the OperationClick reference? – Christopher Rucinski May 10 '15 at 20:12
  • Yes correct, my idea was to add a "cleanup()" method to OperationClick class. So in onDestroy() have something addListener.cleanup(), which in tern should have listener = null. – Vesko May 10 '15 at 20:20
  • I did that. cleanup method in OperationClass. calling the cleanup method on each listener reference (addListener, etc...) in onDestroy. However, the memory usage is still only increasing. – Christopher Rucinski May 10 '15 at 20:28