PropertyInvalidatedCache类说明
该类是为了优化改动不频繁的数据在进程间交互使用的。Android大多进程间交互,使用了Binder机制。每次交互数据都需要Binder调用,但是对于那些很少改动的数据,每次都使用Binder调用,开销是不小的。该类会在首次Binder调用之后,会将结果缓存到本地,之后如果Server端数据一直都没有改动,那Client就会一直使用本地缓存。如果数据发生了改变,那么下次客户端再获取数据的时候,又会通过Binder调用获取最新的数据,再缓存,后续又会使用缓存数据。数据如果发生了改变,Client端是怎么发现数据改变了呢?这个是通过全局属性SystemProperties来实现的。在数据发生改变的时候,会去更改系统属性的值,Client端去获取数据的时候,会去比较系统属性值是否发生了变化,如果发生了变化,就会使用Binder交互,重新获取新的数据。
并且该类是标记了hide,普通的应用APP开发是没法使用的。
初始化
/**
* 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
* @param cacheName Name of this cache in debug and dumpsys
*/
public PropertyInvalidatedCache(int maxEntries, @NonNull String propertyName,
@NonNull String cacheName) {
mPropertyName = propertyName;
mCacheName = cacheName;
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) {
final int size = size();
if (size > mHighWaterMark) {
mHighWaterMark = size;
}
if (size > maxEntries) {
mMissOverflow++;
return true;
}
return false;
}
};
synchronized (sCorkLock) {
sCaches.put(this, null);
if (sDisabledKeys.contains(mCacheName)) {
disableInstance();
}
}
}
初始化成员变量mPropertyName 、mCacheName 、mMaxEntries 、mCache 。
mPropertyName 是属性名称,这个就是全局属性变量的key,是通过构造函数的参数传递进来。Client端发现Server端的数据发生变化,就是通过读取该属性的值发生变化知道的。该名字通过类注释可以发现按照惯例是以“cache_key.”前缀。
mCacheName 是缓存名,进程中应该唯一。进程中通过禁止该类缓存功能就是通过它的名字去查找的。
mMaxEntries 是当前缓存的最大数量,是由参数maxEntries传递进来的。如果超过了该值,会将最后访问到的缓存,给删除。
mCache 是用LinkedHashMap实现的缓存。并且该缓存超过了总量之后,再删除是按照获取的最后访问顺序进行删除的。
查询
/**
* Get a value from the cache or recompute it.
*/
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
|| currentNonce == NONCE_CORKED || bypass(query)) {
if (!mDisabled) {
// Do not bother collecting statistics if the cache is
// locally disabled.
synchronized (mLock) {
mSkips[(int) currentNonce]++;
}
}
if (DEBUG) {
if (!mDisabled) {
Log.d(TAG, String.format(
"cache %s %s for %s",
cacheName(), sNonceName[(int) currentNonce], queryToString(query)));
}
}
return recompute(query);
}
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 of %d entries because nonce changed [%s] -> [%s]",
cacheName(), mCache.size(),
mLastSeenNonce, currentNonce));
}
clear();
mLastSeenNonce = currentNonce;
cachedResult = null;
}
}
// Cache hit --- but we're not quite done yet. A value in the cache might need to
// be augmented in a "refresh" operation. The refresh operation can combine the
// old and the new nonce values. In order to make sure the new parts of the value
// are consistent with the old, possibly-reused parts, we check the property value
// again after the refresh and do the whole fetch again if the property invalidated
// us while we were refreshing.
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);
}
// Cache miss: make the value from scratch.
if (DEBUG) {
Log.d(TAG, "cache miss for " + cacheName() + " " + queryToString(query));
}
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);
}
}
这个方法是理解PropertyInvalidatedCache类的关键,里面描述了什么时候从本地缓存取出数据,什么时候去Server端获取数据,怎么缓存的。
开始就先获取当前currentNonce,获取的时候,会去检查当前是不是被禁止的状态,如果是,就不能使用本地缓存,每次都需要去Server端获取数据。禁止状态是通过isDisabledLocal()方法判断的。这个方法后面再说,接着向下看。如果是禁止的状态,currentNonce的值为NONCE_DISABLED,在该状态下,就会调用recompute(query)方法,来返回结果。这个方法,是留给实现子类,一般都是向Server端请求数据。
如果不是禁止状态,就会调用getCurrentNonce()方法返回currentNonce的值。该方法就是用来返回全局属性变量的值。如果没有设置该变量的值,会返回NONCE_UNSET。在该状态下,也是不会使用本地缓存的。
从代码里还可以看到一个状态NONCE_CORKED,在该状态下也不使用缓存。后续再说这个。
不适用缓存的情况,还有一种,就是bypass(query)返回true。这个也是留给实现子类改写的。默认值返回false,是需要使用本地缓存的。
如果上面的情况都不符合,会继续向下走,去获取缓存。在代码29行,对currentNonce与mLastSeenNonce判断是否相等,如果相等,就会去缓存mCache中取值。mLastSeenNonce代表上一次获取属性的值,如果本地和上次获取的属性值一样,代表值没有发生改变,可以使用缓存中的值。如果不等,那说明值已经发生改变,本地缓存没法使用,后续需要重新获取新值。同时,还会清除缓存,并且更新mLastSeenNonce,并将cachedResult赋值为null,以便后续重新获取新值。
接着往下看,代码51行,cachedResult不为null的情况,这种情况是缓存命中的情况。不过接着就走到了refresh(cachedResult, query)方法,这个是"refresh" 操作。
/**
* Make result up-to-date on a cache hit. Called unlocked;
* may block.
*
* Return either 1) oldResult itself (the same object, by reference equality), in which
* case we just return oldResult as the result of the cache query, 2) a new object, which
* replaces oldResult in the cache and which we return as the result of the cache query
* after performing another property read to make sure that the result hasn't changed in
* the meantime (if the nonce has changed in the meantime, we drop the cache and try the
* whole query again), or 3) null, which causes the old value to be removed from the cache
* and null to be returned as the result of the cache query.
*/
protected Result refresh(Result oldResult, Query query) {
return oldResult;
}
看到这个方法的注释,可能返回三个结果。
1)原来的对象,这个对象就会作为这个query()方法的返回结果
2)一个新的对象,后续会在判断在这个"refresh" 操作完成之后,判断属性是否发生了变化,如果属性发生变化,会丢弃这个缓存,并且再试一遍整个query。如果属性没有发生变化,会将这个新对象替换原来的旧缓存。并且将该新对象作为结果返回。
3)null,后续会在判断在这个"refresh" 操作完成之后,判断属性是否发生了变化,如果属性发生变化,会丢弃这个缓存,并且再试一遍整个query。如果属性没有发生变化,会删除原来的旧缓存。并且将null作为结果返回。
但是看这个方法的具体实现,就直接将原缓存对象oldResult返回了。可能就是为了在具体子类中再实现该方法。
再返回query方法,53行到83行,就是处理了上面三种返回情况,所说的操作。
接着往下看89行,就是处理缓存没有命中的情况,就是调用recompute(query)得到返回结果。之前也说了,这个方法一般都是向Server端请求数据。
得到结果以后,判断mLastSeenNonce和currentNonce相等并且结果不为null的情况下,将结果放入缓存中。然后再将结果返回。
禁止状态
看下这个方法
/**
* Return whether the cache is disabled in this process.
*/
public final boolean isDisabledLocal() {
return mDisabled || !sEnabled;
}
其中,mDisabled是该类的成员变量,而sEnabled是一个静态变量。所以mDisabled是控制局部禁止,而sEnabled是用来控制进程全局的。
mDisabled是用disableInstance()来设置的。如下,并且该方法会调用clear()将缓存清理掉。
/**
* Disable the use of this cache in this process.
*/
public final void disableInstance() {
synchronized (mLock) {
mDisabled = true;
clear();
}
}
sEnabled是一个静态私有变量,设置该变量是以下方法,不过要说明一下的是,如果设置了全局禁止,是没法再次打开,因为没有提供具体实现方法。
/**
* Disable all caches in the local process. Once disabled it is not
* possible to re-enable caching in the current process.
*/
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
public static void disableForTestMode() {
Log.d(TAG, "disabling all caches in the process");
sEnabled = false;
}
CORKED NONCE
临时的将缓存放入未初始化状态,并且阻止无效数据移出那个状态。这个是为了避免在短时间进行大量缓存数据无效的开销。在该状态下,Client端是直接与Server端进行数据交互的。
关于它的两个方法是corkInvalidations(@NonNull String name)和uncorkInvalidations(@NonNull String name)。一个是设置CORKED NONCE,一个是取消CORKED NONCE。并且这两应该配对使用。
上面这个是类注释里面的大致意思,不过还是需要解释一下到底是什么意思。
这个状态的设置应该是在Server端。在数据量很大的情况下,Server端更新数据是需要一段时间的,并且是在更新数据完毕才设置数据无效全局属性。在这个更新数据期间之内,如果不设置CORKED NONCE,Client端是不知道数据是正在更新的,也就是Client端的原来的缓存已经是无效的状态了。如果这个时候客户端通过query()方法调用获取数据,这个时候就是无效缓存数据。如果加上CORKED NONCE,Server端则在开始更新数据前会先调用corkInvalidations(@NonNull String name)设置CORKED NONCE,等到更新结束,再调用uncorkInvalidations(@NonNull String name)去除CORKED NONCE。这样Client在Server更新数据的时候,通过query()方法就能知道,现在是CORKED NONCE,需要去Server端重新获取数据。并且这个状态和NONCE_UNSET和NONCE_DISABLED是一样都不会去更新本地的缓存的。可以对照query()方法的代码看一下这个逻辑。
再看下corkInvalidations(@NonNull String name):
/**
* Temporarily put the cache in the uninitialized state and prevent invalidations from
* moving it out of that state: useful in cases where we want to avoid the overhead of a
* large number of cache invalidations in a short time. While the cache is corked, clients
* bypass the cache and talk to backing services directly. This property makes corking
* correctness-preserving even if corked outside the lock that controls access to the
* cache's backing service.
*
* corkInvalidations() and uncorkInvalidations() must be called in pairs.
*
* @param name Name of the cache-key property to cork
*/
public static void corkInvalidations(@NonNull String name) {
if (!sEnabled) {
if (DEBUG) {
Log.w(TAG, String.format(
"cache cork %s suppressed", name));
}
return;
}
synchronized (sCorkLock) {
int numberCorks = sCorks.getOrDefault(name, 0);
if (DEBUG) {
Log.d(TAG, String.format("corking %s: numberCorks=%s", name, numberCorks));
}
// If we're the first ones to cork this cache, set the cache to the corked state so
// existing caches talk directly to their services while we've corked updates.
// Make sure we don't clobber a disabled cache value.
// TODO(dancol): we can skip this property write and leave the cache enabled if the
// caller promises not to make observable changes to the cache backing state before
// uncorking the cache, e.g., by holding a read lock across the cork-uncork pair.
// Implement this more dangerous mode of operation if necessary.
if (numberCorks == 0) {
final long nonce = SystemProperties.getLong(name, NONCE_UNSET);
if (nonce != NONCE_UNSET && nonce != NONCE_DISABLED) {
SystemProperties.set(name, Long.toString(NONCE_CORKED));
}
} else {
final long count = sCorkedInvalidates.getOrDefault(name, (long) 0);
sCorkedInvalidates.put(name, count + 1);
}
sCorks.put(name, numberCorks + 1);
if (DEBUG) {
Log.d(TAG, "corked: " + name);
}
}
}
可见,在首次设置的时候,如果当前不是NONCE_UNSET 和NONCE_DISABLED,则会将全局属性值设置为NONCE_CORKED。如果在还没有取消CORKED的时候,再次接着调用该方法,会记录设置次数。sCorks就是用来记录当前设置的次数。
还有个sCorkedInvalidates这个是用来记录缓存已经是corked,再次无效或cork缓存的空操作的次数。
再看下uncorkInvalidations(@NonNull String name):
/**
* Undo the effect of a cork, allowing cache invalidations to proceed normally.
* Removing the last cork on a cache name invalidates the cache by side effect,
* transitioning it to normal operation (unless explicitly disabled system-wide).
*
* @param name Name of the cache-key property to uncork
*/
public static void uncorkInvalidations(@NonNull String name) {
if (!sEnabled) {
if (DEBUG) {
Log.w(TAG, String.format(
"cache uncork %s suppressed", name));
}
return;
}
synchronized (sCorkLock) {
int numberCorks = sCorks.getOrDefault(name, 0);
if (DEBUG) {
Log.d(TAG, String.format("uncorking %s: numberCorks=%s", name, numberCorks));
}
if (numberCorks < 1) {
throw new AssertionError("cork underflow: " + name);
}
if (numberCorks == 1) {
sCorks.remove(name);
invalidateCacheLocked(name);
if (DEBUG) {
Log.d(TAG, "uncorked: " + name);
}
} else {
sCorks.put(name, numberCorks - 1);
}
}
}
这个方法是和corkInvalidations(@NonNull String name)相反的操作,在最后一次numberCorks == 1的时候,会设置缓存无效的状态invalidateCacheLocked(name)。缓存无效见下面,先说下自动cork。
自动cork
在该类中,还提供了一个自动cork的帮助类AutoCorker。这个类允许缓存数据提供者分摊这个缓存无效的花费,通过改变之后立即cork。并且在之后的一段时间自动uncork。
最好是用之前提到的corkInvalidations(@NonNull String name)和uncorkInvalidations(@NonNull String name)成对明确调用。但是,有一些无效批次明确出现不太现实,这时候AutoCorker就是一个具体的选择。
这个类有个延迟时间是可以配置的,但是不能配置太大,这个时间的目的是为了让Server写这个属性值的时间减小到最小。每50ms写一次不伤害系统性能。
AutoCorker初始化
public AutoCorker(@NonNull String propertyName) {
this(propertyName, DEFAULT_AUTO_CORK_DELAY_MS);
}
public AutoCorker(@NonNull String propertyName, int autoCorkDelayMs) {
mPropertyName = propertyName;
mAutoCorkDelayMs = autoCorkDelayMs;
// We can't initialize mHandler here: when we're created, the main loop might not
// be set up yet! Wait until we have a main loop to initialize our
// corking callback.
}
第一个是将延迟时间默认设置成50ms。
autoCork()
public void autoCork() {
if (Looper.getMainLooper() == null) {
// We're not ready to auto-cork yet, so just invalidate the cache immediately.
if (DEBUG) {
Log.w(TAG, "invalidating instead of autocorking early in init: "
+ mPropertyName);
}
PropertyInvalidatedCache.invalidateCache(mPropertyName);
return;
}
synchronized (mLock) {
boolean alreadyQueued = mUncorkDeadlineMs >= 0;
if (DEBUG) {
Log.w(TAG, String.format(
"autoCork %s mUncorkDeadlineMs=%s", mPropertyName,
mUncorkDeadlineMs));
}
mUncorkDeadlineMs = SystemClock.uptimeMillis() + mAutoCorkDelayMs;
if (!alreadyQueued) {
getHandlerLocked().sendEmptyMessageAtTime(0, mUncorkDeadlineMs);
PropertyInvalidatedCache.corkInvalidations(mPropertyName);
} else {
final long count = sCorkedInvalidates.getOrDefault(mPropertyName, (long) 0);
sCorkedInvalidates.put(mPropertyName, count + 1);
}
}
}
autoCork()方法是一个主要的方法,每次会在更改数据前调用它。在第一次执行的时候,alreadyQueued为false,所以会发送一个延迟设置时间的消息(默认50ms),接着就会调用PropertyInvalidatedCache.corkInvalidations(mPropertyName)用来设置corked nunce。可见,如果在消息延迟时间还没到的时候,再次调用autoCork(),这时alreadyQueued 为true,并且又会更改mUncorkDeadlineMs 的值。
再接着看看处理消息:
private void handleMessage(Message msg) {
synchronized (mLock) {
if (DEBUG) {
Log.w(TAG, String.format(
"handleMsesage %s mUncorkDeadlineMs=%s",
mPropertyName, mUncorkDeadlineMs));
}
if (mUncorkDeadlineMs < 0) {
return; // ???
}
long nowMs = SystemClock.uptimeMillis();
if (mUncorkDeadlineMs > nowMs) {
mUncorkDeadlineMs = nowMs + mAutoCorkDelayMs;
if (DEBUG) {
Log.w(TAG, String.format(
"scheduling uncork at %s",
mUncorkDeadlineMs));
}
getHandlerLocked().sendEmptyMessageAtTime(0, mUncorkDeadlineMs);
return;
}
if (DEBUG) {
Log.w(TAG, "automatic uncorking " + mPropertyName);
}
mUncorkDeadlineMs = -1;
PropertyInvalidatedCache.uncorkInvalidations(mPropertyName);
}
}
@GuardedBy("mLock")
private Handler getHandlerLocked() {
if (mHandler == null) {
mHandler = new Handler(Looper.getMainLooper()) {
@Override
public void handleMessage(Message msg) {
AutoCorker.this.handleMessage(msg);
}
};
}
return mHandler;
}
主要在handleMessage(Message msg)中,如果mUncorkDeadlineMs 比现在的时间大,说明中间又调用了autoCork(),所以接着再发送一个延迟消息。如果小于等于现在时间,表明时间到了,应该uncork。所以,将 mUncorkDeadlineMs = -1,PropertyInvalidatedCache.uncorkInvalidations(mPropertyName)。将mUncorkDeadlineMs = -1是为了后续接着调用该自动类的时候,就再实现发送延迟消息。
缓存无效
再接着看下invalidateCache(@NonNull String name)
/**
* Invalidate PropertyInvalidatedCache caches in all processes that are keyed on
* {@var name}. This function is synchronous: caches are invalidated upon return.
*
* @param name Name of the cache-key property to invalidate
*/
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);
}
final long count = sCorkedInvalidates.getOrDefault(name, (long) 0);
sCorkedInvalidates.put(name, count + 1);
return;
}
invalidateCacheLocked(name);
}
}
在sCorks中对应的属性名大于0的时候,是不会实际去设置属性值的,只会记录一次nops操作次数在sCorkedInvalidates中,因为这个时候是在corked,等会会通过uncorkInvalidations(@NonNull String name)方法设置。
接着看invalidateCacheLocked(name)
@GuardedBy("sCorkLock")
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 >= 0 && newValue < NONCE_RESERVED);
final String newValueString = Long.toString(newValue);
if (DEBUG) {
Log.d(TAG,
String.format("invalidating cache [%s]: [%s] -> [%s]",
name,
nonce,
newValueString));
}
// TODO(dancol): add an atomic compare and exchange property set operation to avoid a
// small race with concurrent disable here.
SystemProperties.set(name, newValueString);
long invalidateCount = sInvalidates.getOrDefault(name, (long) 0);
sInvalidates.put(name, ++invalidateCount);
}
这个是无效缓存属性值的方法。可以看到NoPreloadHolder.next()每次产生新值,设置到属性中。咱们知道,无效的情况就是上次的属性值和当前的属性值不一样,看看它是怎么实现生成的新值和之前的不一样的。
// Inner class avoids initialization in processes that don't do any invalidation
private static final class NoPreloadHolder {
private static final AtomicLong sNextNonce = new AtomicLong((new Random()).nextLong());
public static long next() {
return sNextNonce.getAndIncrement();
}
}
看到了吧,随机生成了一个随机数,然后封装成了一个AtomicLong 对象。以后加一,这样,就会和上次的不一样了。当然,同一个属性key的值,不见得会每次相差1,因为可能存在多个key对应的缓存无效。