Android 11 的PropertyInvalidatedCache机制

2,823 阅读9分钟

一. 什么是 PropertyInvalidatedCache

PropertyInvalidatedCache通俗上来说,是android11上的新推出的一种基于LRU (Least Recently Used) cache的缓存机制,先看下android官方是怎么定义的 LRU cache that's invalidated when an opaque value in a property changes. Self-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. 通过以上的描述,可以归纳下PropertyInvalidatedCache有以下几个特点:

  1. PropertyInvalidatedCache只在prop中的数据发生变化时进行更新
  2. PropertyInvalidatedCache能够进行自我同步,同时并不会在数据查询的整个过程中持有锁
  3. 当在跨进程的数据传输过程中需要获频繁被读取但是很少改变的数据时,推荐用PropertyInvalidatedCache来做存储

二. PropertyInvalidatedCache 的代码结构

propertyInvalidatedCache.png

propertyInvalidatedCache_03.png

三. Android 11 为什么要引入 PropertyInvalidatedCache

根本原因就是为了最大程度的较少IPC调用过程中很多不必要的开销,例如android的跨进程binder调用,我们来结合蓝牙的源码看下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();
        }

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

从上面的代码可以看到,蓝牙的状态获取是一个aidl调用,BluetoothAdapter作为接口类,通过IBluetooth的getState调用binder server的方法,这里的逻辑属于app进程 在AdapterService的内部类AdapterServiceBinder,继承了IBluetooth.Stub,实现了binder server的相关的逻辑,然后最终调用AdapterProperties获取最终的属性,这里的逻辑属于bluetooth进程 看上去代码好像没什么问题,但是仔细研究一下就会发现,这块是可以优化的 首先蓝牙的状态属性是属于那种不会经常改变的,可能一段时间一直保持在STATE_ON状态,当各个app需要查询蓝牙状态的时候,大多数情况下,可遇见的都是一个不变的value。但是通过上面的BluetoothAdapter的getState接口查询,app还是得一遍遍的进行binder调用,这其实进行了很多不必要的开销,那么有没有办法减少这种资源的浪费呢,Android 11上给出了PropertyInvalidatedCache的解决方案

四. PropertyInvalidatedCache该如何使用

还是蓝牙状态获取的实现,看看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;
}

getState方法会调用内部方法getStateInternal,这里会通过mBluetoothGetStateCache.query(null)去获取当前的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);
}

看下mBluetoothGetStateCache的实现,new 了一个PropertyInvalidatedCache对象,这里设置最大的缓存数量为8,state cache为"cache_key.bluetooth.get_state",在实现的recompute的抽象方法里,当cache不存在时,直接通过mService.getState()直接去获取蓝牙的状态 然后提供了两个操作cache的方法,disableBluetoothGetStateCache和invalidateBluetoothGetStateCache

  1. disableBluetoothGetStateCache的作用是为了关闭当前进程的cache使用
  2. invalidateBluetoothGetStateCache的作用是为了刷新和当前key匹配的所有进程的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();
         
     ...
     }

对应的远端Bluetooth 进程的实现也很简单

  1. 在aidl server端的构造函数初始化时,首先调用BluetoothAdapter的invalidateBluetoothGetStateCache方法进行cache的刷新
  2. 然后再调用 disableBluetoothGetStateCache在当前进程中停止cache的使用
  3. 在蓝牙的状态发生实际变化时,在updateAdapterState方法中再次刷新cache

以上BluetoothAdapter和AdapterService中的简单的几步调用,就可以实现蓝牙状态的cache缓存机制,从而避免了蓝牙状态多次获取的过程中带来的额外的binder开销

五. PropertyInvalidatedCache源码分析

下面我们从源码角度分析一下,为什么上面简单的几步操作就可以实现这个cache机制,带着问题,我们撸下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<>();        

首先看下PropertyInvalidatedCache的构造函数

  1. 两个参数分别表示最大的cache entries数量和prop name
  2. 内部新建了一个LinkedHashMap,并重写了LinkedHashMap的removeEldestEntry方法,当map存储的数据数量大于maxEntries时,需要移除掉最早进入map的数据。 通过这个其实我们大概了解到了,PropertyInvalidatedCache的核心其实是通过LinkedHashMap来实现的
  3. 将当前生成的cache对象put到名为sCaches的WeakHashMap中
  4. 将当前cache的 prop name作为key,value为0,存储到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);

分析下BluetoothAdapter中新建的PropertyInvalidatedCache对象中override的recompute方法,在PropertyInvalidatedCache的源码中,这是一个抽象方法,需要被实现,当cache中不存在时,需要直接从对应的方法中拿到实际的数据,好了,我们到此暂且跳过,看后面这个方法在什么时候会被调用

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

看下invalidateCache方法

  1. 首先会判断当前是否处于test mode,如果是test mode,sEnabled为false,直接return,否则的话,继续向下执行
  2. 判断当前的cache是否持有cork lock(如果一个cache key被corked了,当调用invalidate更新的时候会直接被跳过)如果持有的话,直接忽略,方法反馈
  3. 进一步调用 invalidateCacheLocked
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();
    }
}

进一步分析 invalidateCacheLocked

  1. 尝试从SystemProperties中获取key为name的long属性,默认值为NONCE_UNSET,即0。因为第一次调用该方法,之前没有设置过,所以这里拿到的是0
  2. 跳过nonce检查为NONCE_DISABLED阶段,当newValue为NONCE_UNSET或者NONCE_DISABLED时,while循环获取+1的long随机数
  3. 将long随机数转换为string,通过system prop存储下来
  4. 从sInvalidates的hashmap中尝试获取key为name的记录,如果不存在,默认值为0,然后对获取的随机数进行+1,重新put到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();
    }
}

disableLocal方法比较简单,将mDisabled的变量置为true,然后clear掉mCache中所有数据

最重要的来了,当app想要查询状态时,会调用query,这里我们将query分为以下几个场景,看代码逻辑都分别干了啥

1. 第一次调用query查询蓝牙状态

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

首先会获取当前cache的state,如果不为disable,则获取当前prop对应的随机数值,否则为NONCE_DISABLED

死循环for(;;)执行相关的逻辑,因为currentNonce有具体的数值,跳过recompute的执行

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;
}

mLastSeenNonce初始化为NONCE_UNSET,所以这里走到else的逻辑,将mCache clear,然后currentNonce赋值给mLastSeenNonce,cachedResult置为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);

因为 result为null,所以执行上面的逻辑,首先回调recompute方法,实际去获取蓝牙的状态,这里就会调用BluetoothAdapter中的mService.getState() 直接去获取蓝牙的状态

然后将当前的查询和结果put到mCache中,注意BluetoothAdapter中传的query为null 调用maybeCheckConsistency返回最终的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;
}

VERIFY默认为false,所以上面的if语句执行不到,返回recompute获得的result,也就是直接通过mService.getState()拿到的蓝牙状态

2. 蓝牙状态没有发生变化的情况下,调用query查询蓝牙状态

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++;

开始的执行逻辑和1一样,我们这里直接跳过,来到currentNonce和mLastSeenNonce的判定,因为在第一次调用时,mLastSeenNonce已经被赋值为currentNonce,且随机数没有更新,所以这里两个相等,我们从mCache中查询key为query的result是否存在,因为query为null,所以cachedResult为上一次通过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;
}

执行cachedResult != null的 if 代码块, 这里refresh之后,返回的还是oldResult,refreshedResult等于cachedResult,直接调到mayCheckConsistency方法,也就是返回cachedResult,即为上一次的查询结果,这里并没有发生实际的binder调用,省去了资源的开销

3. 蓝牙状态发生改变后,调用query查询蓝牙状态

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

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

当蓝牙状态发生变化时,由之前AdapterService的代码可知,在updateAdapterState方法中,会更新当前的cache

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

直接看invalidateCacheLocked,当cache被invalidate时,会生成一个新的+1随机数,然后重新设置到SystemProp中,同时sInvalidates通过getOrDefault拿到的值为1,这里进行++之后为2,然后再次put到 HashMap中

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

看看这种情况下,query逻辑该如何触发

首先还是去getCurrentNonce,因为在invalidate过程中,system prop中存储的key为prop name的long随机数值被更新,所以currentNonce != mLastSeenNonce,执行else操作,看到这里是不是很熟悉,又回到了之前第一次调用的query的逻辑中,执行recompute逻辑执行通过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);

至此,相信Android 11的 PropertyInvalidatedCache 机制已经清晰明了了,这里需要respect一下google的大牛们,一直致力于优化android的性能,让android 发展的越来越好!

最后贴一下基于PropertyInvalidatedCache机制查询蓝牙状态的流程图

propertyInvalidatedCache_02.png