What is PropertyInvalidatedCache

PropertyInvalidatedCache is a new caching mechanism based on the LRU (Least Recently Used) cache. LRU cache that’s invalidated when an opaque value in a property changes. Self-synchronizing, synchronizing, and synchronizing but doesn’t hold a lock across data fetches on query misses. The intended use case is caching frequently-read, seldom-changed information normally retrieved across interprocess communication. Based on the above description, PropertyInvalidatedCache has the following characteristics:

  1. PropertyInvalidatedCache is updated only when the data in prop changes
  2. PropertyInvalidatedCache synchronizes itself and does not hold a lock throughout the query
  3. PropertyInvalidatedCache is recommended for data that is frequently read but rarely changed during cross-process data transfer

2. Code structure of PropertyInvalidatedCache

Why does Android 11 introduce PropertyInvalidatedCache

The fundamental reason is to minimize the unnecessary overhead in the process of IPC calls, such as Android cross-process Binder calls, we will combine the source code to see how to obtain bluetooth status before Android 11

frameworks/base/core/java/android/bluetooth/BluetoothAdapter.java 
 
    private IBluetooth mService;
    
    public int getState() {
        android.util.SeempLog.record(63);
        int state = BluetoothAdapter.STATE_OFF;

        try {
            mServiceLock.readLock().lock();
            if (mService != null) {
                state = mService.getState();
            }
        } catch (RemoteException e) {
            Log.e(TAG, "", e);
        } finally {
            mServiceLock.readLock().unlock();
        }

        ...
        
    }
Copy the code
packages/apps/Bluetooth/src/com/android/bluetooth/btservice/AdapterService.java private static class AdapterServiceBinder extends IBluetooth.Stub { private AdapterService mService; . @Override public int getState() { // don't check caller, may be called from system UI AdapterService service = getService(); if (service == null) { return BluetoothAdapter.STATE_OFF; } return service.getState(); }}... public int getState() { enforceCallingOrSelfPermission(BLUETOOTH_PERM, "Need BLUETOOTH permission"); if (mAdapterProperties ! = null) { return mAdapterProperties.getState(); } return BluetoothAdapter.STATE_OFF; }Copy the code

As you can see from the above code, bluetooth state acquisition is an AIDL call, BluetoothAdapter as the interface class, through the getState of IBluetooth call the binder Server method, The logic here belongs to the app process in the internal AdapterService class AdapterServiceBinder, which inherits IBluetooth.Stub, and implements the related logic of binder Server. And then finally call AdapterProperties to get the final properties. The logic here belongs to the Bluetooth process and the code looks fine, but a closer look will show that this can be optimized. First, bluetooth state properties are the ones that don’t change very often. It may remain in the STATE_ON state for a period of time. When each app needs to query the Bluetooth state, in most cases, it will encounter a constant value. But through the above BluetoothAdapter getState interface query, APP still has to call binder over and over again, which actually carries out a lot of unnecessary overhead, so is there any way to reduce the waste of resources? Android 11 provides a solution for PropertyInvalidatedCache

How to use PropertyInvalidatedCache

Again the implementation of Bluetooth state acquisition, take a look at the logic of Android 11

frameworks/base/core/java/android/bluetooth/BluetoothAdapter.java public int getState() { android.util.SeempLog.record(63); int state = getStateInternal(); . } private int getStateInternal() { int state = BluetoothAdapter.STATE_OFF; try { mServiceLock.readLock().lock(); if (mService ! = null) { state = mBluetoothGetStateCache.query(null); } } catch (RuntimeException e) { if (e.getCause() instanceof RemoteException) { Log.e(TAG, "", e.getCause()); } else { throw e; } } finally { mServiceLock.readLock().unlock(); } return state; }Copy the code

GetState method will be called internal getStateInternal, here will pass mBluetoothGetStateCache. Query (null) to obtain the current state of the state

frameworks/base/core/java/android/bluetooth/BluetoothAdapter.java private static final String BLUETOOTH_GET_STATE_CACHE_PROPERTY = "cache_key.bluetooth.get_state"; private final PropertyInvalidatedCache<Void, Integer> mBluetoothGetStateCache = new PropertyInvalidatedCache<Void, Integer>( 8, BLUETOOTH_GET_STATE_CACHE_PROPERTY) { @Override protected Integer recompute(Void query) { try { return mService.getState(); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); }}}; /** @hide */ public void disableBluetoothGetStateCache() { mBluetoothGetStateCache.disableLocal(); } /** @hide */ public static void invalidateBluetoothGetStateCache() { PropertyInvalidatedCache.invalidateCache(BLUETOOTH_GET_STATE_CACHE_PROPERTY); }Copy the code

MBluetoothGetStateCache, new PropertyInvalidatedCache object, set maximum cache number to 8, The state cache is “cache_key.bluetooth.get_state”. In the abstract method of recompute, when the cache does not exist, Directly through the mService. GetState () to obtain the state of bluetooth And then cache provides two operation methods, disableBluetoothGetStateCache and invalidateBluetoothGetStateCache

  1. DisableBluetoothGetStateCache role is the cache in order to close the current process
  2. InvalidateBluetoothGetStateCache role is to refresh the processes and the current key match PropertyInvalidatedCache caches
packages/apps/Bluetooth/src/com/android/bluetooth/btservice/AdapterService.java public static class AdapterServiceBinder  extends IBluetooth.Stub { private AdapterService mService; AdapterServiceBinder(AdapterService svc) { mService = svc; mService.invalidateBluetoothGetStateCache(); BluetoothAdapter.getDefaultAdapter().disableBluetoothGetStateCache(); . } private void invalidateBluetoothGetStateCache() { BluetoothAdapter.invalidateBluetoothGetStateCache(); } void updateAdapterState(int prevState, int newState) { mAdapterProperties.setState(newState); invalidateBluetoothGetStateCache(); if (mCallbacks ! = null) { int n = mCallbacks.beginBroadcast(); debugLog("updateAdapterState() - Broadcasting state " + BluetoothAdapter.nameForState( newState) + " to " + n + " receivers."); for (int i = 0; i < n; i++) { try { mCallbacks.getBroadcastItem(i).onBluetoothStateChange(prevState, newState); } catch (RemoteException e) { debugLog("updateAdapterState() - Callback #" + i + " failed (" + e + ")"); } } mCallbacks.finishBroadcast(); . }Copy the code

The corresponding remote Bluetooth process is also simple to implement

  1. In an aidl constructors to initialize the server side, the first call the BluetoothAdapter invalidateBluetoothGetStateCache methods for cache refresh
  2. Then call disableBluetoothGetStateCache stop the use of the cache in the current process
  3. When the bluetooth state actually changes, the cache is flushed again in the updateAdapterState method

A few simple calls above in BluetoothAdapter and AdapterService can implement the bluetooth state cache mechanism, thus avoiding the additional binder overhead in the process of obtaining Bluetooth state multiple times

PropertyInvalidatedCache source code analysis

PropertyInvalidatedCache = PropertyInvalidatedCache = PropertyInvalidatedCache = PropertyInvalidatedCache = PropertyInvalidatedCache = PropertyInvalidatedCache

frameworks/base/core/java/android/app/PropertyInvalidatedCache.java /** * Make a new property invalidated cache. * * @param maxEntries Maximum number of entries to cache; LRU discard * @param propertyName Name of the system property holding the cache invalidation nonce */ public PropertyInvalidatedCache(int maxEntries, @NonNull String propertyName) { mPropertyName = propertyName; mMaxEntries = maxEntries; MCache = new LinkedHashMap<Query, Result>(2 /* start small */, 0.75f /* default load factor */, true /* LRU access order */) { @Override protected boolean removeEldestEntry(Map.Entry eldest) { return size() > maxEntries; }}; synchronized (sCorkLock) { sCaches.put(this, null); sInvalidates.put(propertyName, (long) 0); } } private static final WeakHashMap<PropertyInvalidatedCache, Void> sCaches = new WeakHashMap<>(); private static final HashMap<String, Long> sInvalidates = new HashMap<>();Copy the code

First look at the constructor of PropertyInvalidatedCache

  1. The two parameters indicate the maximum number of cache entries and the prop name, respectively
  2. A new LinkedHashMap is created internally and the removeEldestEntry method of the LinkedHashMap is rewritten. When the number of data stored in the map is greater than maxEntries, the earliest data entering the map needs to be removed.

The core of PropertyInvalidatedCache is the LinkedHashMap. Put the currently generated cache object into a WeakHashMap called sCaches 4. Store the prop name of the current cache as the key with value 0 in the sInvalidates HashMap

frameworks/base/core/java/android/app/PropertyInvalidatedCache.java

/**
 * Fetch a result from scratch in case it's not in the cache at all.  Called unlocked: may
 * block. If this function returns null, the result of the cache query is null. There is no
 * "negative cache" in the query: we don't cache null results at all.
 */
protected abstract Result recompute(Query query);
Copy the code

Override recompute method in the PropertyInvalidatedCache object newly created in BluetoothAdapter. In the source code of PropertyInvalidatedCache, this is an abstract method and needs to be implemented. When the cache does not exist, the actual data needs to be retrieved directly from the corresponding method

frameworks/base/core/java/android/app/PropertyInvalidatedCache.java public static void invalidateCache(@NonNull String name) { if (! sEnabled) { if (DEBUG) { Log.w(TAG, String.format( "cache invalidate %s suppressed", name)); } return; } // Take the cork lock so invalidateCache() racing against corkInvalidations() doesn't // clobber a cork-written NONCE_UNSET with a cache key we compute before the cork. // The property service is single-threaded anyway, so we don't lose any concurrency by // taking the cork lock around cache invalidations. If we see contention on this lock, // we're invalidating too often. synchronized (sCorkLock) { Integer numberCorks = sCorks.get(name); if (numberCorks ! = null && numberCorks > 0) { if (DEBUG) { Log.d(TAG, "ignoring invalidation due to cork: " + name); } return; } invalidateCacheLocked(name); }}Copy the code

Take a look at the invalidateCache method

  1. If it is in test mode, sEnabled is false and return; otherwise, continue
  2. Check whether the current cache holds a cork lock (if a cache key is corked, it will be skipped when the invalidate update is called)
  3. Call invalidateCacheLocked further
frameworks/base/core/java/android/app/PropertyInvalidatedCache.java private static final long NONCE_UNSET = 0; private static final long NONCE_DISABLED = -1; private static void invalidateCacheLocked(@NonNull String name) { // There's no race here: we don't require that values strictly increase, but instead // only that each is unique in a single runtime-restart session. final long nonce = SystemProperties.getLong(name, NONCE_UNSET); if (nonce == NONCE_DISABLED) { if (DEBUG) { Log.d(TAG, "refusing to invalidate disabled cache: " + name); } return; } long newValue; do { newValue = NoPreloadHolder.next(); } while (newValue == NONCE_UNSET || newValue == NONCE_DISABLED); final String newValueString = Long.toString(newValue); if (DEBUG) { Log.d(TAG, String.format("invalidating cache [%s]: [%s] -> [%s]", name, nonce, newValueString)); } SystemProperties.set(name, newValueString); long invalidateCount = sInvalidates.getOrDefault(name, (long) 0); sInvalidates.put(name, ++invalidateCount); } private static final class NoPreloadHolder { private static final AtomicLong sNextNonce = new AtomicLong((new Random()).nextLong()); public static long next() { return sNextNonce.getAndIncrement(); }}Copy the code

Further analysis of invalidateCacheLocked

  1. Try to get the long property with key name from SystemProperties, which defaults to NONCE_UNSET, or 0. Because the first time I call this method, I haven’t set it before, so I get 0 here
  2. Skip the nonce check for NONCE_DISABLED. When newValue is NONCE_UNSET or NONCE_DISABLED, the while loop gets the long random number of +1
  3. Convert the long random number to string and store it through System Prop
  4. Try to get a record with key name from sInvalidates’ hashMap, default to 0 if it doesn’t exist, and then +1 is applied to the random number retrieved and put back into the HashMap
frameworks/base/core/java/android/app/PropertyInvalidatedCache.java private final LinkedHashMap<Query, Result> mCache; /** * Disable the use of this cache in this process. */ public final void disableLocal() { synchronized (mLock) { mDisabled = true; mCache.clear(); }}Copy the code

The disableLocal method is simple. Set the mDisabled variable to true and clear all data in the mCache

Most importantly, when the app wants to query the state, it calls Query. Here we break query into the following scenarios to see what the code logic does

1. Call Query for the first time to query the Bluetooth status

frameworks/base/core/java/android/app/PropertyInvalidatedCache.java public Result query(Query query) { // Let access to mDisabled race: it's atomic anyway. long currentNonce = (! isDisabledLocal()) ? getCurrentNonce() : NONCE_DISABLED; for (;;) { if (currentNonce == NONCE_DISABLED || currentNonce == NONCE_UNSET) { if (DEBUG) { Log.d(TAG, String.format("cache %s %s for %s", cacheName(), currentNonce == NONCE_DISABLED ? "disabled" : "unset", queryToString(query))); } return recompute(query); }Copy the code

The current cache state is first obtained. If it is not Disable, the random value corresponding to the current prop is obtained; otherwise, it is NONCE_DISABLED

Infinite loop for (;;) Executes the associated logic, since currentNonce has a specific value, skipping the recompute execution

frameworks/base/core/java/android/app/PropertyInvalidatedCache.java private long mLastSeenNonce = NONCE_UNSET; if (currentNonce == mLastSeenNonce) { cachedResult = mCache.get(query); if (cachedResult ! = null) mHits++; } else { if (DEBUG) { Log.d(TAG, String.format("clearing cache %s because nonce changed [%s] -> [%s]", cacheName(), mLastSeenNonce, currentNonce)); } mCache.clear(); mLastSeenNonce = currentNonce; cachedResult = null; }Copy the code

MLastSeenNonce is initialized to NONCE_UNSET, so we go to else logic, assign mCache clear, currentNonce to mLastSeenNonce, and cachedResult to null

frameworks/base/core/java/android/app/PropertyInvalidatedCache.java final Result result = recompute(query); synchronized (mLock) { // If someone else invalidated the cache while we did the recomputation, don't // update the cache with a potentially stale result. if (mLastSeenNonce == currentNonce && result ! = null) { mCache.put(query, result); } mMisses++; } return maybeCheckConsistency(query, result);Copy the code

As result is null, the above logic is executed. First, the recompute method is called to actually get the status of Bluetooth, where mservice.getState () of BluetoothAdapter is called directly to get the status of Bluetooth

The current query and result are then put into the mCache. Note that the BluetoothAdapter passed query is null and maybeCheckConsistency returns the final result

frameworks/base/core/java/android/app/PropertyInvalidatedCache.java private static final boolean VERIFY = false; protected Result maybeCheckConsistency(Query query, Result proposedResult) { if (VERIFY) { Result resultToCompare = recompute(query); boolean nonceChanged = (getCurrentNonce() ! = mLastSeenNonce); if (! nonceChanged && ! debugCompareQueryResults(proposedResult, resultToCompare)) { throw new AssertionError("cache returned out of date response for " + query); } } return proposedResult; }Copy the code

If (mservice.getState ()) {if (mservice.getState ());}) {if (mservice.getState ());}

2. If the Bluetooth status does not change, run the query command to query the Bluetooth status

frameworks/base/core/java/android/app/PropertyInvalidatedCache.java public Result query(Query query) { ... final Result cachedResult; synchronized (mLock) { if (currentNonce == mLastSeenNonce) { cachedResult = mCache.get(query); if (cachedResult ! = null) mHits++;Copy the code

CurrentNonce and mLastSeenNonce are equal because mLastSeenNonce has been assigned to currentNonce on the first call and the random number has not been updated. Query mCache for result with key = query. Since query is null, cachedResult is the last bluetooth state retrieved directly from mservice.getState ()

frameworks/base/core/java/android/app/PropertyInvalidatedCache.java if (cachedResult ! = null) { final Result refreshedResult = refresh(cachedResult, query); if (refreshedResult ! = cachedResult) { if (DEBUG) { Log.d(TAG, "cache refresh for " + cacheName() + " " + queryToString(query)); } final long afterRefreshNonce = getCurrentNonce(); if (currentNonce ! = afterRefreshNonce) { currentNonce = afterRefreshNonce; if (DEBUG) { Log.d(TAG, String.format("restarting %s %s because nonce changed in refresh", cacheName(), queryToString(query))); } continue; } synchronized (mLock) { if (currentNonce ! = mLastSeenNonce) { // Do nothing: cache is already out of date. Just return the value // we already have: there's no guarantee that the contents of mCache // won't become invalid as soon as we return. } else if (refreshedResult == null) { mCache.remove(query); } else { mCache.put(query, refreshedResult); } } return maybeCheckConsistency(query, refreshedResult); } if (DEBUG) { Log.d(TAG, "cache hit for " + cacheName() + " " + queryToString(query)); } return maybeCheckConsistency(query, cachedResult); } protected Result refresh(Result oldResult, Query query) { return oldResult; }Copy the code

Perform cachedResult! Refresh Result = cachedResult, mayCheckConsistency = cachedResult, This is the result of the last query, where no actual binder calls occur, saving the resource overhead

3. After the Bluetooth status changes, run the query command to query the Bluetooth status

packages/apps/Bluetooth/src/com/android/bluetooth/btservice/AdapterService.java

void updateAdapterState(int prevState, int newState) {
     mAdapterProperties.setState(newState);
     invalidateBluetoothGetStateCache();
Copy the code

When the bluetooth state changes, the current cache is updated in the updateAdapterState method, as the previous AdapterService code shows

frameworks/base/core/java/android/app/PropertyInvalidatedCache.java private static void invalidateCacheLocked(@NonNull String name) { ... long newValue; do { newValue = NoPreloadHolder.next(); } while (newValue == NONCE_UNSET || newValue == NONCE_DISABLED); final String newValueString = Long.toString(newValue); . SystemProperties.set(name, newValueString); long invalidateCount = sInvalidates.getOrDefault(name, (long) 0); sInvalidates.put(name, ++invalidateCount); }Copy the code

InvalidateCacheLocked: When invalidateCacheLocked, a new +1 random number is generated and reset to SystemProp. SInvalidates gets a value of 1 from getOrDefault. Here we add ++ to get 2, and then we put it into the HashMap again

frameworks/base/core/java/android/app/PropertyInvalidatedCache.java

public Result query(Query query) {
    // Let access to mDisabled race: it's atomic anyway.
    long currentNonce = (!isDisabledLocal()) ? getCurrentNonce() : NONCE_DISABLED;
     
     ...
     
        final Result cachedResult;
        synchronized (mLock) {
            if (currentNonce == mLastSeenNonce) {
                cachedResult = mCache.get(query);

                if (cachedResult != null) mHits++;
            } else {
                if (DEBUG) {
                    Log.d(TAG,
                            String.format("clearing cache %s because nonce changed [%s] -> [%s]",
                                    cacheName(),
                                    mLastSeenNonce, currentNonce));
                }
                mCache.clear();
                mLastSeenNonce = currentNonce;
                cachedResult = null;
            }
        }

private long getCurrentNonce() {
    SystemProperties.Handle handle = mPropertyHandle;
    if (handle == null) {
        handle = SystemProperties.find(mPropertyName);
        if (handle == null) {
            return NONCE_UNSET;
        }
        mPropertyHandle = handle;
    }
    return handle.getLong(NONCE_UNSET);
}    
Copy the code

How does the Query logic fire in this case

CurrentNonce = currentNonce; currentNonce = currentNonce; currentNonce = currentNonce; = mLastSeenNonce, else, back to the query logic that was called the first time, recompute the bluetooth state directly via mservice.getState ()

frameworks/base/core/java/android/app/PropertyInvalidatedCache.java final Result result = recompute(query); synchronized (mLock) { // If someone else invalidated the cache while we did the recomputation, don't // update the cache with a potentially stale result. if (mLastSeenNonce == currentNonce && result ! = null) { mCache.put(query, result); } mMisses++; } return maybeCheckConsistency(query, result);Copy the code

So far, I believe Android 11 PropertyInvalidatedCache mechanism has been clear, here needs to respect Google’s big men, has been committed to optimize the performance of Android, let Android development better and better!

Finally, the flow chart of query bluetooth status based on PropertyInvalidatedCache mechanism is posted