// Copyright 2020 The Chromium Authors // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. package org.chromium.base; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Objects; import javax.annotation.concurrent.GuardedBy; /** * Class allowing to wrap lambdas, such as {@link Callback} or {@link Runnable} with a cancelable * version of the same, and cancel them in bulk when {@link #destroy()} is called. Use an instance * of this class to wrap lambdas passed to other objects, and later use {@link #destroy()} to * prevent future invocations of these lambdas. * *
Besides helping with lifecycle management, this also prevents holding onto object references * after callbacks have been canceled. * *
Example usage: * *
{@code * public class Foo { * private CallbackController mCallbackController = new CallbackController(); * private SomeDestructibleClass mDestructible = new SomeDestructibleClass(); * * // Classic destroy, with clean up of cancelables. * public void destroy() { * // This call makes sure all tracked lambdas are destroyed. * // It is recommended to be done at the top of the destroy methods, to ensure calls from * // other threads don't use already destroyed resources. * if (mCallbackController != null) { * mCallbackController.destroy(); * mCallbackController = null; * } * * if (mDestructible != null) { * mDestructible.destroy(); * mDestructible = null; * } * } * * // Sets up Bar instance by providing it with a set of dangerous callbacks all of which could * // cause a NullPointerException if invoked after destroy(). * public void setUpBar(Bar bar) { * // Notice all callbacks below would fail post destroy, if they were not canceled. * bar.setDangerousLambda(mCallbackController.makeCancelable(() -> mDestructible.method())); * bar.setDangerousRunnable(mCallbackController.makeCancelable(this::dangerousRunnable)); * bar.setDangerousOtherCallback( * mCallbackController.makeCancelable(baz -> mDestructible.setBaz(baz))); * bar.setDangerousCallback(mCallbackController.makeCancelable(this::setBaz)); * } * * private void dangerousRunnable() { * mDestructible.method(); * } * * private void setBaz(Baz baz) { * mDestructible.setBaz(baz); * } * } * }* *
It does not matter if the lambda is intended to be invoked once or more times, as it is only * weakly referred from this class. When the lambda is no longer needed, it can be safely garbage * collected. All invocations after {@link #destroy()} will be ignored. * *
Each instance of this class in only meant for a single {@link #destroy()} call. After it is * destroyed, the owning class should create a new instance instead: * *
{@code * // Somewhere inside Foo. * mCallbackController.destroy(); // Invalidates all current callbacks. * mCallbackController = new CallbackController(); // Allows to start handing out new callbacks. * }*/ @SuppressWarnings({"NoSynchronizedThisCheck", "NoSynchronizedMethodCheck"}) public final class CallbackController { /** Interface for cancelable objects tracked by this class. */ private interface Cancelable { /** Cancels the object, preventing its execution, when triggered. */ void cancel(); } /** Class wrapping a {@link Callback} interface with a {@link Cancelable} interface. */ private class CancelableCallback
This method must not be called after {@link #destroy()}.
*
* @param This method must not be called after {@link #destroy()}.
*
* @param runnable A runnable that will be made cancelable.
* @return A cancelable instance of the runnable.
*/
public synchronized Runnable makeCancelable(@NonNull Runnable runnable) {
checkNotCanceled();
CancelableRunnable cancelable = new CancelableRunnable(runnable);
addInternal(cancelable);
return cancelable;
}
@GuardedBy("this")
private void addInternal(Cancelable cancelable) {
var cancelables = mCancelables;
cancelables.add(new WeakReference<>(cancelable));
// Flush null entries.
if ((cancelables.size() % 1024) == 0) {
// This removes null entries as a side-effect.
// Cloning the list is inefficient, but this should rarely be hit.
CollectionUtil.strengthen(cancelables);
}
}
/**
* Cancels all of the cancelables that have not been garbage collected yet.
*
* This method must only be called once and makes the instance unusable afterwards.
*/
public synchronized void destroy() {
checkNotCanceled();
for (Cancelable cancelable : CollectionUtil.strengthen(mCancelables)) {
cancelable.cancel();
}
mCancelables = null;
}
/** If the cancelation already happened, throws an {@link IllegalStateException}. */
@GuardedBy("this")
private void checkNotCanceled() {
// Use NullPointerException because it optimizes well.
Objects.requireNonNull(mCancelables);
}
}