// 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}, * where {@code T extends UnownedUserData}, with an instance of {@code T}. *

* 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}. *

* 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. *

* 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. *

* All interaction with the UnownedUserDataHost must be performed on the same thread. *

* {@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. *

* All methods on UnownedUserDataHost except {@link #destroy()} is package protected to ensure all * interaction with the host goes through the {@link UnownedUserDataKey}. *

* Example usage: *

{@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 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();
 * }
 * 
*

* 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. *

* 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. *

* There is also another common pattern for retrieving an attached object, and that is to do it * lazily: *

{@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;
 * }
 * }
 * 
*

* 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, WeakReference> 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}. * *

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 the type of {@link UnownedUserData}. */ /* package */ void set( @NonNull UnownedUserDataKey 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 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 get(@NonNull UnownedUserDataKey key) { checkState(); WeakReference 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 the type of {@link UnownedUserData}. */ /* package */ void remove(@NonNull UnownedUserDataKey key) { checkState(); WeakReference 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. *

* 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> 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(); } }