旋转和按钮点击可能导致内存泄漏问题

3
我认为我的应用正在占用额外的内存,而垃圾回收器应该能够重新分配。我不知道它们是否会被视为内存泄漏,但我已经注意到了可能存在问题的两个地方:
从应用程序启动开始,连续将我的设备从纵向旋转到横向,然后再从横向旋转到纵向……每次旋转后,累计增加 300KB 的内存使用量。
有2个输入时,每次按钮单击,累计增加 30KB 的内存使用量。
问题在于,只要应用程序仍然在视图中,内存就不会被释放。
例如:将设备旋转10次,并单击按钮50次 -> 消耗4.5MB内存。如果我让应用程序保持打开状态并且1小时不做任何操作,则我的应用程序仍将消耗4.5MB的内存;即使很多内存应该在大约59分钟前被释放我关心的是为什么当应用程序始终处于视图中时,内存从未被释放? 我对这个工作方式理解错误吗?
注意:应用程序名为 Contrived Calculator

代码

用户界面

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);

            }

        };

    }

计算逻辑

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; }

}

强制执行GC是否会释放内存或仍有使用? - eduyayo
在调试会话进行了41分钟且没有其他使用情况时,强制进行垃圾回收将会清除内存。但是为什么在Android Studio内存工具控制台中41分钟内还没有调用垃圾回收呢? - Christopher Rucinski
不要过度使用... 如果程序一直运行,它将在内存中创建和删除东西,碎片化最终将强制进行垃圾回收。只要保持低内存使用率,就没有理由。 - eduyayo
看一下这个链接:https://dev59.com/0XI-5IYBdhLWcg3w-9wK - eduyayo
1个回答

1

跟踪内存泄漏最好使用Eclipse MAT(不用担心名称,它与从Android Studio获取的内存转储文件一样有效)。该工具具有相当陡峭的学习曲线,但对于跟踪此类内存问题,它是您能想象到的最强大的工具。

确实,在这行代码中你正在引起内存泄漏:

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

问题在于OperationClick的实例持有对Activity的引用,因为它是通过.listenerOn(this)传递的。
当您旋转设备时,您知道Activity会被重新创建。我假设您没有在onDestroy()方法中清除侦听器,因此您最终会得到一个泄漏的Activity(每次旋转4次才正确)。
顺便说一下,上周由SquareUp团队发布了Leak Canary,这是一个非常棒的Android内存泄漏检测库。这是这种类型的第一个库,我强烈建议您尝试一下!
编辑:要修复泄漏,请不要使用匿名的OperationClick对象。还要在其中添加一个“清理”方法,在该方法中摆脱listenerOn()中创建的listener

实际上,LeakCanary是我发布这篇文章的原因。我得到了一个泄漏报告,指出匿名点击监听器正在泄漏。经过查找,我发现这是由于传入的“this”参数导致的。在此之前,我有一个“onDestroy”方法,它执行了“btnAdd.setOnClickListener(null);”。通过断点验证,该方法确实被调用了,但没有释放任何内存。所以我将该方法删除了。 - Christopher Rucinski
增加了一个建议,如何修复你的内存泄漏问题……如果你还没有解决它 :) - Vesko
只为快速测试,我创建了4个OperationClick状态对象。然后我这样做... 'btnAdd.setOnClickListener((add = new OperationClick(Add)).listenerOn(this));' 然后我重写了'onDestroy'方法并调用了'add = null;',但仍未释放任何内存。所以我必须在'OperationClick'类中移除监听器?而不仅是将OperationClick引用设置为null? - Christopher Rucinski
是的,正确的想法是在OperationClick类中添加一个"cleanup()"方法。因此,在onDestroy()中有一些addListener.cleanup(),这应该有listener = null。 - Vesko
我在OperationClass中实现了cleanup方法,并在onDestroy中对每个监听器引用(addListener等)调用了cleanup方法。然而,内存使用仍然在增加。 - Christopher Rucinski

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接