刷新消息队列以防止基于消息的泄漏。但是要刷新哪个HandlerThread?

时间:2019-04-12 21:49:34

标签: android memory-leaks leakcanary

从本质上讲,我发现here中的内存泄漏由LeakCanary检测到。

LC stacktrace

我正在尝试通过使用以上文章中的“管道工修复”来解决此问题,该文章用空消息刷新消息队列。每次空闲时,提供的代码样本刷新消息队列。在我的对话框被关闭后,我只需要刷新一次队列:

public class ExampleDialogFragment extends AppCompatDialogFragment {

    public static ExampleDialogFragment newInstance() {
        return new ExampleDialogFragment();
    }

    @NonNull
    @Override
    public Dialog onCreateDialog(final Bundle savedInstanceState) {
        return new AlertDialog.Builder(getContext())
                .setPositiveButton(android.R.string.ok, (dialog, which) -> onClicked())
                .create();
    }

    private void onClicked() {
        if (getTargetFragment() instanceof Callbacks) {
            ((Callbacks) getTargetFragment()).onButtonClicked();
            flushStackLocalLeaks();
        }
    }

    private static void flushStackLocalLeaks() {
        final Handler handler = new Handler(Looper.myLooper());
        handler.post(() -> Looper.myQueue().addIdleHandler(() -> {
            Timber.d("Flushing on thread %s", Thread.currentThread().getName());
            handler.sendMessageDelayed(handler.obtainMessage(), 1000);
            return false; // we only want to flush once, not *every* 1000 mSec
        }));
    }

    public interface Callbacks {
        void onButtonClicked();
    }
}

我面临的问题是,在LeakCanary报告中,泄漏根源处的HandlerThread从来不是我期望的线程。有时是ConnectivityThread,有时是HandlerThread,其中mName = "queued-work-looper",其他是我的分析库使用的HandlerThread。在所有这些情况下,它都不是主线程。我希望消息从主线程泄漏。所以

  1. 为什么这种泄漏发生在主线程以外的HandlerThread / ConnectivityThread上?
  2. 我如何知道哪个HandlerThread需要冲洗?

1 个答案:

答案 0 :(得分:2)

我在12月在这里提交了此问题:https://issuetracker.google.com/issues/146144484

核心问题是每个闲置的HandlerThread都保留其最后回收的消息。

我们当前的解决方法是随着时间的推移寻找新的处理程序线程并设置重复刷新:

import android.os.Handler
import android.os.HandlerThread
import android.os.Looper
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit.SECONDS

object AndroidLeaks {

  /**
   * HandlerThread instances keep local reference to their last handled message after recycling it.
   * That message is obtained by a dialog which sets on an OnClickListener on it and then never
   * recycles it, expecting it to be garbage collected but it ends up being held by the HandlerThread.
   */
  fun flushHandlerThreads() {
    val executor = Executors.newSingleThreadScheduledExecutor()

    val flushedThreadIds = mutableSetOf<Int>()
    // Wait 2 seconds then look for handler threads every 3 seconds.
    executor.scheduleWithFixedDelay({
      val newHandlerThreadsById = findAllHandlerThreads()
          .mapNotNull { thread ->
            val threadId = thread.threadId
            if (threadId == -1 || threadId in flushedThreadIds) {
              null
            } else {
              threadId to thread
            }
          }
      flushedThreadIds += newHandlerThreadsById.map { it.first }
      newHandlerThreadsById
          .map { it.second }
          .forEach { handlerThread ->
            var scheduleFlush = true
            val flushHandler = Handler(handlerThread.looper)
            flushHandler.onEachIdle {
              if (scheduleFlush) {
                scheduleFlush = false
                // When the Handler thread becomes idle, we post a message to force it to move.
                // Source: https://developer.squareup.com/blog/a-small-leak-will-sink-a-great-ship/
                try {
                  flushHandler.postDelayed({
                    // Right after this postDelayed executes, the idle handler will likely be called
                    // again (if the queue is otherwise empty), so we'll need to schedule a flush
                    // again.
                    scheduleFlush = true
                  }, 1000)
                } catch (ignored: RuntimeException) {
                  // If the thread is quitting, posting to it will throw. There is no safe and atomic way
                  // to check if a thread is quitting first then post it it.
                }
              }
            }
          }
    }, 2, 3, SECONDS)
  }

  private fun Handler.onEachIdle(onIdle: () -> Unit) {
    try {
      // Unfortunately Looper.getQueue() is API 23. Looper.myQueue() is API 1.
      // So we have to post to the handler thread to be able to obtain the queue for that
      // thread from within that thread.
      post {
        Looper
            .myQueue()
            .addIdleHandler {
              onIdle()
              true
            }
      }
    } catch (ignored: RuntimeException) {
      // If the thread is quitting, posting to it will throw. There is no safe and atomic way
      // to check if a thread is quitting first then post it it.
    }
  }

  private fun findAllHandlerThreads(): List<HandlerThread> {
    // Based on https://stackoverflow.com/a/1323480
    var rootGroup = Thread.currentThread().threadGroup!!
    while (rootGroup.parent != null) rootGroup = rootGroup.parent
    var threads = arrayOfNulls<Thread>(rootGroup.activeCount())
    while (rootGroup.enumerate(threads, true) == threads.size) {
      threads = arrayOfNulls(threads.size * 2)
    }
    return threads.mapNotNull { if (it is HandlerThread) it else null }
  }
}