这段代码可以避免 Android 处理程序内存泄漏吗?

Can this code avoid the Android handler memory leak?

handler1 是泄漏。

我想将 handler1 代码转换为 handler2 代码。可以吗?

这两个代码有什么区别?

public class MainActivity extends AppCompatActivity {

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

        // leaks!
        Handler handler1 = new Handler()
        {
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);
                Log.e("LOG", "Hello~1");
            }
        };

        Handler handler2 = new Handler(new Handler.Callback() {
            @Override
            public boolean handleMessage(Message msg) {
                Log.e("LOG", "Hello~2");
                return false;
            }
        });

        handler1.postDelayed(new Runnable() {
            @Override
            public void run() { }
        }, 60000);
        handler2.postDelayed(new Runnable() {
            @Override
            public void run() { }
        }, 60000);

        finish();
    }
}

为什么 handler1 出现泄漏警告?

关于leak warning的原因,this article解释的很好。

引自文章

In Java, non-static inner and anonymous classes hold an implicit reference to their outer class. Static inner classes, on the other hand, do not.

因此,当您通过匿名 class 创建 handler1 时,它将持有对 MainActivity 实例的引用,并且 MainActiviy 无法被垃圾收集。

解决方案

再次引用文章

To fix the problem, subclass the Handler in a new file or use a static inner class instead. Static inner classes do not hold an implicit reference to their outer class, so the activity will not be leaked. If you need to invoke the outer activity’s methods from within the Handler, have the Handler hold a WeakReference to the activity so you don’t accidentally leak a context. To fix the memory leak that occurs when we instantiate the anonymous Runnable class, we make the variable a static field of the class (since static instances of anonymous classes do not hold an implicit reference to their outer class):

根据文章,更新您的代码如下:

public class MainActivity extends AppCompatActivity {

    private static class MyHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
             Log.e("LOG", "Hello~1");
        }
    }

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

        Handler handler1 = new MyHandler();

        handler1.postDelayed(new Runnable() {
            @Override
            public void run() { }
        }, 60000);

        finish();
    }
}

handler2能解决问题吗?

来自@Michael的回答This Handler class should be static or leaks might occur: IncomingHandler 提供解决方案。

引用@Michael 的回答

As I understand it, this will not avoid the potential memory leak. Message objects hold a reference to the mIncomingHandler object which holds a reference the Handler.Callback object which holds a reference to the Service object. As long as there are messages in the Looper message queue, the Service will not be GC. However, it won't be a serious issue unless you have long delay messages in the message queue.

在你的例子中,handler2 将包含对 Handler.Callback object.And 的引用,因为 Handler.Callback 是由匿名 class 创建的,因此它将包含一个也参考 MainActiviy 实例。所以 MainActiviy 实例也不能被垃圾回收。

我在模拟器 (android 28) 上尝试此代码并转储内存,探查器显示没有泄漏,我也尝试 Leakcanary 并显示相同的结果。这让我怀疑网上文章的准确性。然后我注意到了区别,如果我使用 Kotlin 写这个逻辑,内存不会泄漏,但是使用 Java 代码会泄漏。

后来我发现了一个有趣的东西。在java中,无论是否使用外部class方法,匿名内部class都会持有构造函数引用的外部对象。在kotlin中,如果内部逻辑不使用外部class方法,匿名内部class不持有外部对象引用。

我在 Kotlin 代码中使用了处理程序来捕获传入的蓝牙传入消息。此代码没有 lint 泄漏警告:

private val incomingMsgHandler: Handler = Handler { msg ->
    msg.obj?.let {
        if (it is ByteArray) {
            val msgStr = String(it)
            setIncomingMessage(msgStr)
        }
    }
    true
}

简而言之,这使用带有 lambda 回调的 Handler 构造函数。