Merge "Reduce RM createResources lock contention" into rvc-qpr-dev-plus-aosp

This commit is contained in:
TreeHugger Robot
2020-09-22 16:26:52 +00:00
committed by Android (Google) Code Review

View File

@@ -39,7 +39,6 @@ import android.os.Trace;
import android.util.ArrayMap; import android.util.ArrayMap;
import android.util.DisplayMetrics; import android.util.DisplayMetrics;
import android.util.Log; import android.util.Log;
import android.util.LruCache;
import android.util.Pair; import android.util.Pair;
import android.util.Slog; import android.util.Slog;
import android.view.Display; import android.view.Display;
@@ -62,7 +61,6 @@ import java.util.List;
import java.util.Objects; import java.util.Objects;
import java.util.WeakHashMap; import java.util.WeakHashMap;
import java.util.function.Consumer; import java.util.function.Consumer;
import java.util.function.Predicate;
/** @hide */ /** @hide */
public class ResourcesManager { public class ResourcesManager {
@@ -129,17 +127,30 @@ public class ResourcesManager {
} }
} }
private static final boolean ENABLE_APK_ASSETS_CACHE = false;
/** /**
* The ApkAssets we are caching and intend to hold strong references to. * Loads {@link ApkAssets} and caches them to prevent their garbage collection while the
* instance is alive and reachable.
*/ */
private final LruCache<ApkKey, ApkAssets> mLoadedApkAssets = private class ApkAssetsSupplier {
(ENABLE_APK_ASSETS_CACHE) ? new LruCache<>(3) : null; final ArrayMap<ApkKey, ApkAssets> mLocalCache = new ArrayMap<>();
/**
* Retrieves the {@link ApkAssets} corresponding to the specified key, caches the ApkAssets
* within this instance, and inserts the loaded ApkAssets into the {@link #mCachedApkAssets}
* cache.
*/
ApkAssets load(final ApkKey apkKey) throws IOException {
ApkAssets apkAssets = mLocalCache.get(apkKey);
if (apkAssets == null) {
apkAssets = loadApkAssets(apkKey);
mLocalCache.put(apkKey, apkAssets);
}
return apkAssets;
}
}
/** /**
* The ApkAssets that are being referenced in the wild that we can reuse, even if they aren't * The ApkAssets that are being referenced in the wild that we can reuse.
* in our LRU cache. Bonus resources :)
*/ */
private final ArrayMap<ApkKey, WeakReference<ApkAssets>> mCachedApkAssets = new ArrayMap<>(); private final ArrayMap<ApkKey, WeakReference<ApkAssets>> mCachedApkAssets = new ArrayMap<>();
@@ -337,48 +348,77 @@ public class ResourcesManager {
return "/data/resource-cache/" + path.substring(1).replace('/', '@') + "@idmap"; return "/data/resource-cache/" + path.substring(1).replace('/', '@') + "@idmap";
} }
private @NonNull ApkAssets loadApkAssets(String path, boolean sharedLib, boolean overlay) private @NonNull ApkAssets loadApkAssets(@NonNull final ApkKey key) throws IOException {
throws IOException { ApkAssets apkAssets;
final ApkKey newKey = new ApkKey(path, sharedLib, overlay);
ApkAssets apkAssets = null;
if (mLoadedApkAssets != null) {
apkAssets = mLoadedApkAssets.get(newKey);
if (apkAssets != null && apkAssets.isUpToDate()) {
return apkAssets;
}
}
// Optimistically check if this ApkAssets exists somewhere else. // Optimistically check if this ApkAssets exists somewhere else.
final WeakReference<ApkAssets> apkAssetsRef = mCachedApkAssets.get(newKey); synchronized (this) {
if (apkAssetsRef != null) { final WeakReference<ApkAssets> apkAssetsRef = mCachedApkAssets.get(key);
apkAssets = apkAssetsRef.get(); if (apkAssetsRef != null) {
if (apkAssets != null && apkAssets.isUpToDate()) { apkAssets = apkAssetsRef.get();
if (mLoadedApkAssets != null) { if (apkAssets != null && apkAssets.isUpToDate()) {
mLoadedApkAssets.put(newKey, apkAssets); return apkAssets;
} else {
// Clean up the reference.
mCachedApkAssets.remove(key);
} }
return apkAssets;
} else {
// Clean up the reference.
mCachedApkAssets.remove(newKey);
} }
} }
// We must load this from disk. // We must load this from disk.
if (overlay) { if (key.overlay) {
apkAssets = ApkAssets.loadOverlayFromPath(overlayPathToIdmapPath(path), 0 /*flags*/); apkAssets = ApkAssets.loadOverlayFromPath(overlayPathToIdmapPath(key.path),
0 /*flags*/);
} else { } else {
apkAssets = ApkAssets.loadFromPath(path, sharedLib ? ApkAssets.PROPERTY_DYNAMIC : 0); apkAssets = ApkAssets.loadFromPath(key.path,
key.sharedLib ? ApkAssets.PROPERTY_DYNAMIC : 0);
} }
if (mLoadedApkAssets != null) { synchronized (this) {
mLoadedApkAssets.put(newKey, apkAssets); mCachedApkAssets.put(key, new WeakReference<>(apkAssets));
} }
mCachedApkAssets.put(newKey, new WeakReference<>(apkAssets));
return apkAssets; return apkAssets;
} }
/**
* Retrieves a list of apk keys representing the ApkAssets that should be loaded for
* AssetManagers mapped to the {@param key}.
*/
private static @NonNull ArrayList<ApkKey> extractApkKeys(@NonNull final ResourcesKey key) {
final ArrayList<ApkKey> apkKeys = new ArrayList<>();
// resDir can be null if the 'android' package is creating a new Resources object.
// This is fine, since each AssetManager automatically loads the 'android' package
// already.
if (key.mResDir != null) {
apkKeys.add(new ApkKey(key.mResDir, false /*sharedLib*/, false /*overlay*/));
}
if (key.mSplitResDirs != null) {
for (final String splitResDir : key.mSplitResDirs) {
apkKeys.add(new ApkKey(splitResDir, false /*sharedLib*/, false /*overlay*/));
}
}
if (key.mLibDirs != null) {
for (final String libDir : key.mLibDirs) {
// Avoid opening files we know do not have resources, like code-only .jar files.
if (libDir.endsWith(".apk")) {
apkKeys.add(new ApkKey(libDir, true /*sharedLib*/, false /*overlay*/));
}
}
}
if (key.mOverlayDirs != null) {
for (final String idmapPath : key.mOverlayDirs) {
apkKeys.add(new ApkKey(idmapPath, false /*sharedLib*/, true /*overlay*/));
}
}
return apkKeys;
}
/** /**
* Creates an AssetManager from the paths within the ResourcesKey. * Creates an AssetManager from the paths within the ResourcesKey.
* *
@@ -390,64 +430,38 @@ public class ResourcesManager {
@VisibleForTesting @VisibleForTesting
@UnsupportedAppUsage @UnsupportedAppUsage
protected @Nullable AssetManager createAssetManager(@NonNull final ResourcesKey key) { protected @Nullable AssetManager createAssetManager(@NonNull final ResourcesKey key) {
return createAssetManager(key, /* apkSupplier */ null);
}
/**
* Variant of {@link #createAssetManager(ResourcesKey)} that attempts to load ApkAssets
* from an {@link ApkAssetsSupplier} if non-null; otherwise ApkAssets are loaded using
* {@link #loadApkAssets(ApkKey)}.
*/
private @Nullable AssetManager createAssetManager(@NonNull final ResourcesKey key,
@Nullable ApkAssetsSupplier apkSupplier) {
final AssetManager.Builder builder = new AssetManager.Builder(); final AssetManager.Builder builder = new AssetManager.Builder();
// resDir can be null if the 'android' package is creating a new Resources object. final ArrayList<ApkKey> apkKeys = extractApkKeys(key);
// This is fine, since each AssetManager automatically loads the 'android' package for (int i = 0, n = apkKeys.size(); i < n; i++) {
// already. final ApkKey apkKey = apkKeys.get(i);
if (key.mResDir != null) {
try { try {
builder.addApkAssets(loadApkAssets(key.mResDir, false /*sharedLib*/, builder.addApkAssets(
false /*overlay*/)); (apkSupplier != null) ? apkSupplier.load(apkKey) : loadApkAssets(apkKey));
} catch (IOException e) { } catch (IOException e) {
Log.e(TAG, "failed to add asset path " + key.mResDir); if (apkKey.overlay) {
return null; Log.w(TAG, String.format("failed to add overlay path '%s'", apkKey.path), e);
} } else if (apkKey.sharedLib) {
} Log.w(TAG, String.format(
"asset path '%s' does not exist or contains no resources",
if (key.mSplitResDirs != null) { apkKey.path), e);
for (final String splitResDir : key.mSplitResDirs) { } else {
try { Log.e(TAG, String.format("failed to add asset path '%s'", apkKey.path), e);
builder.addApkAssets(loadApkAssets(splitResDir, false /*sharedLib*/,
false /*overlay*/));
} catch (IOException e) {
Log.e(TAG, "failed to add split asset path " + splitResDir);
return null; return null;
} }
} }
} }
if (key.mLibDirs != null) {
for (final String libDir : key.mLibDirs) {
if (libDir.endsWith(".apk")) {
// Avoid opening files we know do not have resources,
// like code-only .jar files.
try {
builder.addApkAssets(loadApkAssets(libDir, true /*sharedLib*/,
false /*overlay*/));
} catch (IOException e) {
Log.w(TAG, "Asset path '" + libDir +
"' does not exist or contains no resources.");
// continue.
}
}
}
}
if (key.mOverlayDirs != null) {
for (final String idmapPath : key.mOverlayDirs) {
try {
builder.addApkAssets(loadApkAssets(idmapPath, false /*sharedLib*/,
true /*overlay*/));
} catch (IOException e) {
Log.w(TAG, "failed to add overlay path " + idmapPath);
// continue.
}
}
}
if (key.mLoaders != null) { if (key.mLoaders != null) {
for (final ResourcesLoader loader : key.mLoaders) { for (final ResourcesLoader loader : key.mLoaders) {
builder.addLoader(loader); builder.addLoader(loader);
@@ -480,24 +494,6 @@ public class ResourcesManager {
pw.println("ResourcesManager:"); pw.println("ResourcesManager:");
pw.increaseIndent(); pw.increaseIndent();
if (mLoadedApkAssets != null) {
pw.print("cached apks: total=");
pw.print(mLoadedApkAssets.size());
pw.print(" created=");
pw.print(mLoadedApkAssets.createCount());
pw.print(" evicted=");
pw.print(mLoadedApkAssets.evictionCount());
pw.print(" hit=");
pw.print(mLoadedApkAssets.hitCount());
pw.print(" miss=");
pw.print(mLoadedApkAssets.missCount());
pw.print(" max=");
pw.print(mLoadedApkAssets.maxSize());
} else {
pw.print("cached apks: 0 [cache disabled]");
}
pw.println();
pw.print("total apks: "); pw.print("total apks: ");
pw.println(countLiveReferences(mCachedApkAssets.values())); pw.println(countLiveReferences(mCachedApkAssets.values()));
@@ -533,11 +529,12 @@ public class ResourcesManager {
return config; return config;
} }
private @Nullable ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key) { private @Nullable ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key,
@Nullable ApkAssetsSupplier apkSupplier) {
final DisplayAdjustments daj = new DisplayAdjustments(key.mOverrideConfiguration); final DisplayAdjustments daj = new DisplayAdjustments(key.mOverrideConfiguration);
daj.setCompatibilityInfo(key.mCompatInfo); daj.setCompatibilityInfo(key.mCompatInfo);
final AssetManager assets = createAssetManager(key); final AssetManager assets = createAssetManager(key, apkSupplier);
if (assets == null) { if (assets == null) {
return null; return null;
} }
@@ -575,9 +572,18 @@ public class ResourcesManager {
*/ */
private @Nullable ResourcesImpl findOrCreateResourcesImplForKeyLocked( private @Nullable ResourcesImpl findOrCreateResourcesImplForKeyLocked(
@NonNull ResourcesKey key) { @NonNull ResourcesKey key) {
return findOrCreateResourcesImplForKeyLocked(key, /* apkSupplier */ null);
}
/**
* Variant of {@link #findOrCreateResourcesImplForKeyLocked(ResourcesKey)} that attempts to
* load ApkAssets from a {@link ApkAssetsSupplier} when creating a new ResourcesImpl.
*/
private @Nullable ResourcesImpl findOrCreateResourcesImplForKeyLocked(
@NonNull ResourcesKey key, @Nullable ApkAssetsSupplier apkSupplier) {
ResourcesImpl impl = findResourcesImplForKeyLocked(key); ResourcesImpl impl = findResourcesImplForKeyLocked(key);
if (impl == null) { if (impl == null) {
impl = createResourcesImpl(key); impl = createResourcesImpl(key, apkSupplier);
if (impl != null) { if (impl != null) {
mResourceImpls.put(key, new WeakReference<>(impl)); mResourceImpls.put(key, new WeakReference<>(impl));
} }
@@ -766,7 +772,7 @@ public class ResourcesManager {
} }
// Now request an actual Resources object. // Now request an actual Resources object.
return createResources(token, key, classLoader); return createResources(token, key, classLoader, /* apkSupplier */ null);
} finally { } finally {
Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
} }
@@ -809,6 +815,31 @@ public class ResourcesManager {
(ref) -> ref == null || deadReferences.contains(ref)); (ref) -> ref == null || deadReferences.contains(ref));
} }
/**
* Creates an {@link ApkAssetsSupplier} and loads all the ApkAssets required by the {@param key}
* into the supplier. This should be done while the lock is not held to prevent performing I/O
* while holding the lock.
*/
private @NonNull ApkAssetsSupplier createApkAssetsSupplierNotLocked(@NonNull ResourcesKey key) {
Trace.traceBegin(Trace.TRACE_TAG_RESOURCES,
"ResourcesManager#createApkAssetsSupplierNotLocked");
try {
final ApkAssetsSupplier supplier = new ApkAssetsSupplier();
final ArrayList<ApkKey> apkKeys = extractApkKeys(key);
for (int i = 0, n = apkKeys.size(); i < n; i++) {
final ApkKey apkKey = apkKeys.get(i);
try {
supplier.load(apkKey);
} catch (IOException e) {
Log.w(TAG, String.format("failed to preload asset path '%s'", apkKey.path), e);
}
}
return supplier;
} finally {
Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
}
}
/** /**
* Creates a Resources object set with a ResourcesImpl object matching the given key. * Creates a Resources object set with a ResourcesImpl object matching the given key.
* *
@@ -816,12 +847,14 @@ public class ResourcesManager {
* @param key The key describing the parameters of the ResourcesImpl object. * @param key The key describing the parameters of the ResourcesImpl object.
* @param classLoader The classloader to use for the Resources object. * @param classLoader The classloader to use for the Resources object.
* If null, {@link ClassLoader#getSystemClassLoader()} is used. * If null, {@link ClassLoader#getSystemClassLoader()} is used.
* @param apkSupplier The apk assets supplier to use when creating a new ResourcesImpl object.
* @return A Resources object that gets updated when * @return A Resources object that gets updated when
* {@link #applyConfigurationToResourcesLocked(Configuration, CompatibilityInfo)} * {@link #applyConfigurationToResourcesLocked(Configuration, CompatibilityInfo)}
* is called. * is called.
*/ */
private @Nullable Resources createResources(@Nullable IBinder activityToken, private @Nullable Resources createResources(@Nullable IBinder activityToken,
@NonNull ResourcesKey key, @NonNull ClassLoader classLoader) { @NonNull ResourcesKey key, @NonNull ClassLoader classLoader,
@Nullable ApkAssetsSupplier apkSupplier) {
synchronized (this) { synchronized (this) {
if (DEBUG) { if (DEBUG) {
Throwable here = new Throwable(); Throwable here = new Throwable();
@@ -829,7 +862,7 @@ public class ResourcesManager {
Slog.w(TAG, "!! Get resources for activity=" + activityToken + " key=" + key, here); Slog.w(TAG, "!! Get resources for activity=" + activityToken + " key=" + key, here);
} }
ResourcesImpl resourcesImpl = findOrCreateResourcesImplForKeyLocked(key); ResourcesImpl resourcesImpl = findOrCreateResourcesImplForKeyLocked(key, apkSupplier);
if (resourcesImpl == null) { if (resourcesImpl == null) {
return null; return null;
} }
@@ -898,7 +931,10 @@ public class ResourcesManager {
rebaseKeyForActivity(activityToken, key); rebaseKeyForActivity(activityToken, key);
} }
return createResources(activityToken, key, classLoader); // Preload the ApkAssets required by the key to prevent performing heavy I/O while the
// ResourcesManager lock is held.
final ApkAssetsSupplier assetsSupplier = createApkAssetsSupplierNotLocked(key);
return createResources(activityToken, key, classLoader, assetsSupplier);
} finally { } finally {
Trace.traceEnd(Trace.TRACE_TAG_RESOURCES); Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
} }
@@ -969,7 +1005,13 @@ public class ResourcesManager {
final ResourcesKey newKey = rebaseActivityOverrideConfig(resources, oldConfig, final ResourcesKey newKey = rebaseActivityOverrideConfig(resources, oldConfig,
overrideConfig, displayId); overrideConfig, displayId);
if (newKey != null) { if (newKey != null) {
updateActivityResources(resources, newKey, false); final ResourcesImpl resourcesImpl =
findOrCreateResourcesImplForKeyLocked(newKey);
if (resourcesImpl != null && resourcesImpl != resources.getImpl()) {
// Set the ResourcesImpl, updating it for all users of this Resources
// object.
resources.setImpl(resourcesImpl);
}
} }
} }
} }
@@ -1024,24 +1066,6 @@ public class ResourcesManager {
return newKey; return newKey;
} }
private void updateActivityResources(Resources resources, ResourcesKey newKey,
boolean hasLoader) {
final ResourcesImpl resourcesImpl;
if (hasLoader) {
// Loaders always get new Impls because they cannot be shared
resourcesImpl = createResourcesImpl(newKey);
} else {
resourcesImpl = findOrCreateResourcesImplForKeyLocked(newKey);
}
if (resourcesImpl != null && resourcesImpl != resources.getImpl()) {
// Set the ResourcesImpl, updating it for all users of this Resources
// object.
resources.setImpl(resourcesImpl);
}
}
public final boolean applyConfigurationToResources(@NonNull Configuration config, public final boolean applyConfigurationToResources(@NonNull Configuration config,
@Nullable CompatibilityInfo compat) { @Nullable CompatibilityInfo compat) {
synchronized(this) { synchronized(this) {