284 lines
12 KiB
Java
284 lines
12 KiB
Java
// 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 android.os.Handler;
|
|
import android.os.Looper;
|
|
|
|
import androidx.annotation.NonNull;
|
|
import androidx.annotation.Nullable;
|
|
import androidx.annotation.VisibleForTesting;
|
|
|
|
import org.chromium.base.lifetime.DestroyChecker;
|
|
|
|
import java.lang.ref.WeakReference;
|
|
import java.util.HashMap;
|
|
import java.util.HashSet;
|
|
import java.util.Set;
|
|
|
|
/**
|
|
* UnownedUserDataHost is a type-safe and heterogeneous container that does not own the objects that
|
|
* are stored within. It has the ability to associate a key of type {@code UnownedUserDataKey<T>},
|
|
* where {@code T extends UnownedUserData}, with an instance of {@code T}.
|
|
* <p>
|
|
* Mismatch of types between key and value type information can be checked at compile time, which
|
|
* ensures it is not possible to insert or retrieve data where the types do not match. Neither the
|
|
* key nor the object is allowed to be {@code null}.
|
|
* <p>
|
|
* Value objects are held using {@link WeakReference} in the container, which means that they can be
|
|
* garbage collected once the last strong reference has been removed. The {@link UnownedUserDataKey}
|
|
* is still a strong reference, so it is important that it does not have a reference to the object
|
|
* it is used as a key for. When trying to retrieve a garbage collected item for which a key is
|
|
* still held, the entry in the map is removed during the invocation.
|
|
* <p>
|
|
* Invoking {@link #destroy()} clears out the map, including both keys and the {@link
|
|
* WeakReference}s to the {@link UnownedUserData}s, making them available for garbage collection.
|
|
* This enables the garbage collector to not be blocked on this class for continuing the garbage
|
|
* collection cycle. During this process, all {@link UnownedUserData} objects are informed that they
|
|
* have been detached.
|
|
* <p>
|
|
* All interaction with the UnownedUserDataHost must be performed on the same thread.
|
|
* <p>
|
|
* {@link UnownedUserData} is somewhat similar to {@link org.chromium.base.UserData}, except that it
|
|
* is not owned by the host. The structure is also a bit different since the instances are retrieved
|
|
* through a {@link UnownedUserDataKey} instead of the class type itself. The reason for this is to
|
|
* ensure that we protect against accidental incorrect usage where something has been made
|
|
* accessible through misconfigured GN visibility rules, incorrect package visibility or
|
|
* misconfigured DEPS rules. In addition, it enforces clients to go through the from-method to
|
|
* retrieve the object, ensuring that control stays with the object itself.
|
|
* <p>
|
|
* All methods on UnownedUserDataHost except {@link #destroy()} is package protected to ensure all
|
|
* interaction with the host goes through the {@link UnownedUserDataKey}.
|
|
* <p>
|
|
* Example usage:
|
|
* <pre>{@code
|
|
* public class HolderClass {
|
|
* // Defines the container.
|
|
* private final UnownedUserDataHost mUnownedUserDataHost = new UnownedUserDataHost();
|
|
*
|
|
* public UnownedUserDataHost getUnownedUserDataHost() {
|
|
* return mUnownedUserDataHost;
|
|
* }
|
|
* }
|
|
*
|
|
* public class Foo implements UnownedUserData {
|
|
* // Keeping KEY private enforces acquisition by calling #from(), therefore Foo is in control
|
|
* // of getting the instance.
|
|
* private static final UnownedUserDataKey<Foo> KEY = new UnownedUserDataKey<>(Foo.class);
|
|
*
|
|
* // The UnownedUserData framework enables this method in particular.
|
|
* public static Foo from(HolderClass holder) {
|
|
* return KEY.retrieveDataFromHost(holderClass.getUnownedUserDataHost());
|
|
* }
|
|
*
|
|
* public void initialize(HolderClass holderClass) {
|
|
* // This could also be in the constructor or somewhere else that is reasonable for a
|
|
* // particular object.
|
|
* KEY.attachToHost(holderClass.getUnownedUserDataHost(), this);
|
|
* }
|
|
*
|
|
* public void destroy() {
|
|
* // This ensures that the UnownedUserData can not be resurrected through any
|
|
* // UnownedUserDataHost after this.
|
|
* // For detaching from a particular host, use KEY.detachFromHost(host) instead.
|
|
* KEY.detachFromAllHosts(this);
|
|
* }
|
|
* }
|
|
*
|
|
*
|
|
* // After construction, `foo` needs to attach itself to the HolderClass instance of the
|
|
* // UnownedUserDataHost.
|
|
* // Depending on who owns Foo, this could be its factory, or some other ownership model. Foo
|
|
* // does not need to hold on to the HolderClass, as that is taken care of by the key during
|
|
* // attachment. It is up to the implementor to decide whether this is in the constructor, or
|
|
* // in a separate initialize step.
|
|
* Foo foo = new Foo();
|
|
* foo.initialize(holderClass);
|
|
*
|
|
* ...
|
|
*
|
|
* // Now that the instance of Foo is attached to the particular instance of Holder, it
|
|
* // can be retrieved using just the HolderClass instance.
|
|
* Foo sameFoo = Foo.from(holderClass);
|
|
*
|
|
* ...
|
|
*
|
|
* // During destruction of `foo`, it must remove itself from the instance of HolderClass to
|
|
* // ensure that it can not be retrieved using that path any longer.
|
|
* foo.destroy();
|
|
* }
|
|
* </pre>
|
|
* <p>
|
|
* The code snippet above uses a {@code static} key to be able to facilitate the method {@code
|
|
* public static Foo from(HolderClass holderClass)}, since it would not be possible to retrieve the
|
|
* private key from that method if it was an instance member.
|
|
* <p>
|
|
* The code snippet above also assumes that {@code Foo} has knowledge about the {@code HolderClass},
|
|
* instead of taking in a {@link UnownedUserDataHost} in the {@code from} method, since that
|
|
* typically provides a more pleasant experience for users.
|
|
* <p>
|
|
* There is also another common pattern for retrieving an attached object, and that is to do it
|
|
* lazily:
|
|
* <pre>{@code
|
|
* public static Foo from(HolderClass holderClass) {
|
|
* Foo foo = KEY.retrieveDataFromHost(holderClass.getUnownedUserDataHost());
|
|
* if (foo == null) {
|
|
* foo = new Foo();
|
|
* KEY.attachToHost(holderClass.getUnownedUserDataHost(), foo);
|
|
* }
|
|
* return foo;
|
|
* }
|
|
* }
|
|
* </pre>
|
|
* <p>
|
|
* However, it is important to note that in this scenario, as soon as the code that invokes
|
|
* from(...) drops the reference, Foo will be eligible for garbage collection since the host only
|
|
* holds a {@link WeakReference}. This means that Foo could end up being constructed and garbage
|
|
* collected often, depending on whether the caller holds on to a strong reference or not.
|
|
*
|
|
* @see UnownedUserDataKey for information about the type of key that is required.
|
|
* @see UnownedUserData for the marker interface used for this type of data.
|
|
*/
|
|
public final class UnownedUserDataHost {
|
|
private static Looper retrieveNonNullLooperOrThrow() {
|
|
Looper looper = Looper.myLooper();
|
|
if (looper == null) throw new IllegalStateException();
|
|
return looper;
|
|
}
|
|
|
|
private final ThreadUtils.ThreadChecker mThreadChecker = new ThreadUtils.ThreadChecker();
|
|
private final DestroyChecker mDestroyChecker = new DestroyChecker();
|
|
|
|
/**
|
|
* Handler to use to post {@link UnownedUserData#onDetachedFromHost(UnownedUserDataHost)}
|
|
* invocations to.
|
|
*/
|
|
private Handler mHandler;
|
|
|
|
/** The core data structure within this host. */
|
|
private HashMap<UnownedUserDataKey<?>, WeakReference<? extends UnownedUserData>>
|
|
mUnownedUserDataMap = new HashMap<>();
|
|
|
|
public UnownedUserDataHost() {
|
|
this(new Handler(retrieveNonNullLooperOrThrow()));
|
|
}
|
|
|
|
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
|
|
/* package */ UnownedUserDataHost(Handler handler) {
|
|
mHandler = handler;
|
|
}
|
|
|
|
/**
|
|
* Stores a {@link WeakReference} to {@code object} using the given {@code key}.
|
|
*
|
|
* <p>If the key is already attached to a different host, it is detached from that host.
|
|
*
|
|
* @param key the key to use for the object.
|
|
* @param newValue the object to store.
|
|
* @param <T> the type of {@link UnownedUserData}.
|
|
*/
|
|
/* package */ <T extends UnownedUserData> void set(
|
|
@NonNull UnownedUserDataKey<T> key, @NonNull T newValue) {
|
|
checkState();
|
|
|
|
// If we already have data, we might want to detach that first.
|
|
if (mUnownedUserDataMap.containsKey(key)) {
|
|
T currentValue = get(key);
|
|
// If we are swapping objects, inform the previous object of detachment.
|
|
if (!newValue.equals(currentValue)) key.detachFromHost(this);
|
|
}
|
|
|
|
mUnownedUserDataMap.put(key, new WeakReference<>(newValue));
|
|
}
|
|
|
|
/**
|
|
* Retrieves the {@link UnownedUserData} object stored under the given key.
|
|
*
|
|
* @param key the key to use for the object.
|
|
* @param <T> the type of {@link UnownedUserData}.
|
|
* @return the stored version or {@code null} if it is not stored or has been garbage collected.
|
|
*/
|
|
@Nullable
|
|
/* package */ <T extends UnownedUserData> T get(@NonNull UnownedUserDataKey<T> key) {
|
|
checkState();
|
|
|
|
WeakReference<? extends UnownedUserData> valueWeakRef = mUnownedUserDataMap.get(key);
|
|
if (valueWeakRef == null) return null;
|
|
UnownedUserData value = valueWeakRef.get();
|
|
if (value == null) {
|
|
// The object the entry referenced has now been GCed, so remove the entry.
|
|
key.detachFromHost(this);
|
|
return null;
|
|
}
|
|
return key.getValueClass().cast(value);
|
|
}
|
|
|
|
/**
|
|
* Removes the {@link UnownedUserData} object stored under the given key, if any.
|
|
*
|
|
* @param key the key to use for the object.
|
|
* @param <T> the type of {@link UnownedUserData}.
|
|
*/
|
|
/* package */ <T extends UnownedUserData> void remove(@NonNull UnownedUserDataKey<T> key) {
|
|
checkState();
|
|
|
|
WeakReference<? extends UnownedUserData> valueWeakRef = mUnownedUserDataMap.remove(key);
|
|
if (valueWeakRef == null) return;
|
|
|
|
UnownedUserData value = valueWeakRef.get();
|
|
// Invoking anything on `value` might be re-entrant for the caller so responses should be
|
|
// posted. However, the informOnDetachmentFromHost() method contains a documented warning
|
|
// that it might be re-entrant, so it is OK to use that to guard the call to
|
|
// `onDetachedFromHost(...)`.
|
|
if (value != null && value.informOnDetachmentFromHost()) {
|
|
mHandler.post(() -> value.onDetachedFromHost(this));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Destroys the UnownedUserDataHost by clearing out the map, making the objects stored within
|
|
* available for garbage collection as early as possible, in case the object owning the
|
|
* UnownedUserDataHost stays alive for a while after destroy() has been invoked.
|
|
* <p>
|
|
* Objects stored within the UnownedUserDataHost are informed of this destroy() call through
|
|
* {@link UnownedUserData#onDetachedFromHost(UnownedUserDataHost)}, and the {@link
|
|
* UnownedUserDataKey} instances are updated to not refer to this host anymore.
|
|
*/
|
|
public void destroy() {
|
|
mThreadChecker.assertOnValidThread();
|
|
|
|
// Protect against potential races.
|
|
if (mDestroyChecker.isDestroyed()) return;
|
|
|
|
// Create a shallow copy of all keys to ensure each held object can safely remove itself
|
|
// from the map while iterating over their keys.
|
|
Set<UnownedUserDataKey<?>> keys = new HashSet<>(mUnownedUserDataMap.keySet());
|
|
for (UnownedUserDataKey<?> key : keys) key.detachFromHost(this);
|
|
|
|
mUnownedUserDataMap = null;
|
|
mHandler = null;
|
|
|
|
// Need to wait until the end to destroy the ThreadChecker to ensure that the
|
|
// detachFromHost(...) invocations above are allowed to invoke remove(...).
|
|
mDestroyChecker.destroy();
|
|
}
|
|
|
|
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
|
|
/* package */ int getMapSize() {
|
|
checkState();
|
|
|
|
return mUnownedUserDataMap.size();
|
|
}
|
|
|
|
/* package */ boolean isDestroyed() {
|
|
return mDestroyChecker.isDestroyed();
|
|
}
|
|
|
|
private void checkState() {
|
|
mThreadChecker.assertOnValidThread();
|
|
mDestroyChecker.checkNotDestroyed();
|
|
}
|
|
}
|