Inline string resource values into code
This will inline resource strings that have a single value into code if the Resources:getString(int id) method is used
This is currently only inlining into code, I will follow up with inlining into xml and then implement a second round of tracing to (hopefully) eliminate the resource table entry.
Bug: b/311321134
Bug: b/287398085
Change-Id: Id5f5301a13a3d0d3a17da8a539ba0a0c7dfe9297
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index 6e8edf7..a72492a 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -906,8 +906,7 @@
LegacyResourceShrinker shrinker = resourceShrinkerBuilder.build();
ShrinkerResult shrinkerResult;
if (options.resourceShrinkerConfiguration.isOptimizedShrinking()) {
- shrinkerResult =
- shrinker.shrinkModel(appView.getResourceShrinkerState().getR8ResourceShrinkerModel());
+ shrinkerResult = shrinker.shrinkModel(appView.getResourceAnalysisResult().getModel());
} else {
shrinkerResult = shrinker.run();
}
diff --git a/src/main/java/com/android/tools/r8/graph/AppView.java b/src/main/java/com/android/tools/r8/graph/AppView.java
index 3e3fe4c..f806710 100644
--- a/src/main/java/com/android/tools/r8/graph/AppView.java
+++ b/src/main/java/com/android/tools/r8/graph/AppView.java
@@ -4,7 +4,6 @@
package com.android.tools.r8.graph;
-import com.android.build.shrinker.r8integration.R8ResourceShrinkerState;
import com.android.tools.r8.androidapi.AndroidApiLevelCompute;
import com.android.tools.r8.androidapi.ComputedApiLevel;
import com.android.tools.r8.contexts.CompilationContext;
@@ -13,6 +12,7 @@
import com.android.tools.r8.features.ClassToFeatureSplitMap;
import com.android.tools.r8.graph.DexValue.DexValueString;
import com.android.tools.r8.graph.analysis.InitializedClassesInInstanceMethodsAnalysis.InitializedClassesInInstanceMethods;
+import com.android.tools.r8.graph.analysis.ResourceAccessAnalysis.ResourceAnalysisResult;
import com.android.tools.r8.graph.classmerging.MergedClassesCollection;
import com.android.tools.r8.graph.lens.GraphLens;
import com.android.tools.r8.graph.lens.InitClassLens;
@@ -149,7 +149,7 @@
private SeedMapper applyMappingSeedMapper;
- R8ResourceShrinkerState resourceShrinkerState = null;
+ private ResourceAnalysisResult resourceAnalysisResult = null;
// When input has been (partially) desugared these are the classes which has been library
// desugared. This information is populated in the IR converter.
@@ -871,12 +871,12 @@
testing().unboxedEnumsConsumer.accept(dexItemFactory(), unboxedEnums);
}
- public R8ResourceShrinkerState getResourceShrinkerState() {
- return resourceShrinkerState;
+ public void setResourceAnalysisResult(ResourceAnalysisResult resourceAnalysisResult) {
+ this.resourceAnalysisResult = resourceAnalysisResult;
}
- public void setResourceShrinkerState(R8ResourceShrinkerState resourceShrinkerState) {
- this.resourceShrinkerState = resourceShrinkerState;
+ public ResourceAnalysisResult getResourceAnalysisResult() {
+ return resourceAnalysisResult;
}
public boolean validateUnboxedEnumsHaveBeenPruned() {
@@ -971,6 +971,9 @@
setProguardCompatibilityActions(
getProguardCompatibilityActions().withoutPrunedItems(prunedItems, timing));
}
+ if (resourceAnalysisResult != null) {
+ resourceAnalysisResult.withoutPrunedItems(prunedItems, timing);
+ }
if (hasRootSet()) {
rootSet.pruneItems(prunedItems, timing);
}
@@ -1175,6 +1178,17 @@
new ThreadTask() {
@Override
public void run(Timing threadTiming) {
+ appView.resourceAnalysisResult.rewrittenWithLens(lens, threadTiming);
+ }
+
+ @Override
+ public boolean shouldRun() {
+ return appView.resourceAnalysisResult != null;
+ }
+ },
+ new ThreadTask() {
+ @Override
+ public void run(Timing threadTiming) {
appView.setRootSet(appView.rootSet().rewrittenWithLens(lens, threadTiming));
}
diff --git a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
index 92ceda8..39c4168 100644
--- a/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
+++ b/src/main/java/com/android/tools/r8/graph/DexItemFactory.java
@@ -93,6 +93,7 @@
public static final String androidMediaMediaDrmDescriptorString = "Landroid/media/MediaDrm;";
public static final String androidMediaMediaMetadataRetrieverDescriptorString =
"Landroid/media/MediaMetadataRetriever;";
+ public static final String androidResourcesDescriptorString = "Landroid/content/res/Resources;";
/** Set of types that may be synthesized during compilation. */
private final Set<DexType> possibleCompilerSynthesizedTypes = Sets.newIdentityHashSet();
@@ -643,6 +644,13 @@
createStaticallyKnownType(androidMediaMediaDrmDescriptorString);
public final DexType androidMediaMediaMetadataRetrieverType =
createStaticallyKnownType(androidMediaMediaMetadataRetrieverDescriptorString);
+ public final DexType androidResourcesType =
+ createStaticallyKnownType(androidResourcesDescriptorString);
+ public final DexString androidResourcesGetStringName = createString("getString");
+ public final DexProto androidResourcesGetStringProto = createProto(stringType, intType);
+ public final DexMethod androidResourcesGetStringMethod =
+ createMethod(
+ androidResourcesType, androidResourcesGetStringProto, androidResourcesGetStringName);
public final StringBuildingMethods stringBuilderMethods =
new StringBuildingMethods(stringBuilderType);
diff --git a/src/main/java/com/android/tools/r8/graph/analysis/ResourceAccessAnalysis.java b/src/main/java/com/android/tools/r8/graph/analysis/ResourceAccessAnalysis.java
index d4ebe2f..ed61adb 100644
--- a/src/main/java/com/android/tools/r8/graph/analysis/ResourceAccessAnalysis.java
+++ b/src/main/java/com/android/tools/r8/graph/analysis/ResourceAccessAnalysis.java
@@ -5,6 +5,7 @@
package com.android.tools.r8.graph.analysis;
import com.android.build.shrinker.r8integration.R8ResourceShrinkerState;
+import com.android.build.shrinker.r8integration.R8ResourceShrinkerState.R8ResourceShrinkerModel;
import com.android.tools.r8.AndroidResourceInput;
import com.android.tools.r8.AndroidResourceInput.Kind;
import com.android.tools.r8.ResourceException;
@@ -12,9 +13,12 @@
import com.android.tools.r8.graph.AppView;
import com.android.tools.r8.graph.DexField;
import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.graph.FieldResolutionResult.SingleFieldResolutionResult;
import com.android.tools.r8.graph.ProgramField;
import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.graph.PrunedItems;
+import com.android.tools.r8.graph.lens.GraphLens;
import com.android.tools.r8.ir.code.IRCode;
import com.android.tools.r8.ir.code.Instruction;
import com.android.tools.r8.ir.code.NewArrayEmpty;
@@ -23,8 +27,10 @@
import com.android.tools.r8.ir.conversion.MethodConversionOptions;
import com.android.tools.r8.shaking.Enqueuer;
import com.android.tools.r8.shaking.EnqueuerWorklist;
+import com.android.tools.r8.shaking.KeepFieldInfo;
import com.android.tools.r8.utils.DescriptorUtils;
import com.android.tools.r8.utils.StringUtils;
+import com.android.tools.r8.utils.Timing;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import it.unimi.dsi.fastutil.ints.IntList;
import java.util.IdentityHashMap;
@@ -34,18 +40,15 @@
public class ResourceAccessAnalysis implements EnqueuerFieldAccessAnalysis {
private final R8ResourceShrinkerState resourceShrinkerState;
- private final Map<DexProgramClass, RClassFieldToValueStore> fieldToValueMapping =
- new IdentityHashMap<>();
+ private final Map<DexType, RClassFieldToValueStore> fieldToValueMapping = new IdentityHashMap<>();
private final AppView<? extends AppInfoWithClassHierarchy> appView;
+ private final Enqueuer enqueuer;
- @SuppressWarnings("UnusedVariable")
private ResourceAccessAnalysis(
- AppView<? extends AppInfoWithClassHierarchy> appView,
- Enqueuer enqueuer,
- R8ResourceShrinkerState resourceShrinkerState) {
+ AppView<? extends AppInfoWithClassHierarchy> appView, Enqueuer enqueuer) {
this.appView = appView;
- this.resourceShrinkerState = resourceShrinkerState;
- appView.setResourceShrinkerState(resourceShrinkerState);
+ this.enqueuer = enqueuer;
+ this.resourceShrinkerState = new R8ResourceShrinkerState();
try {
for (AndroidResourceInput androidResource :
appView.options().androidResourceProvider.getAndroidResources()) {
@@ -62,14 +65,14 @@
public static void register(
AppView<? extends AppInfoWithClassHierarchy> appView, Enqueuer enqueuer) {
if (enabled(appView, enqueuer)) {
- enqueuer.registerFieldAccessAnalysis(
- new ResourceAccessAnalysis(appView, enqueuer, new R8ResourceShrinkerState()));
+ enqueuer.registerFieldAccessAnalysis(new ResourceAccessAnalysis(appView, enqueuer));
}
}
@Override
public void done(Enqueuer enqueuer) {
- appView.setResourceShrinkerState(resourceShrinkerState);
+ appView.setResourceAnalysisResult(
+ new ResourceAnalysisResult(resourceShrinkerState, fieldToValueMapping));
EnqueuerFieldAccessAnalysis.super.done(enqueuer);
}
@@ -93,13 +96,15 @@
return;
}
if (getMaybeCachedIsRClass(resolvedField.getHolder())) {
- DexProgramClass holderType = resolvedField.getHolder();
+ DexType holderType = resolvedField.getHolderType();
if (!fieldToValueMapping.containsKey(holderType)) {
populateRClassValues(resolvedField);
}
assert fieldToValueMapping.containsKey(holderType);
RClassFieldToValueStore rClassFieldToValueStore = fieldToValueMapping.get(holderType);
IntList integers = rClassFieldToValueStore.valueMapping.get(field);
+ enqueuer.applyMinimumKeepInfoWhenLive(
+ resolvedField, KeepFieldInfo.newEmptyJoiner().disallowOptimization());
for (Integer integer : integers) {
resourceShrinkerState.trace(integer);
}
@@ -161,7 +166,7 @@
rClassValueBuilder.addMapping(staticPut.getField(), values);
}
- fieldToValueMapping.put(field.getHolder(), rClassValueBuilder.build());
+ fieldToValueMapping.put(field.getHolderType(), rClassValueBuilder.build());
}
private final Map<DexProgramClass, Boolean> cachedClassLookups = new IdentityHashMap<>();
@@ -189,13 +194,103 @@
return isRClass;
}
+ public static class ResourceAnalysisResult {
+
+ private final R8ResourceShrinkerState resourceShrinkerState;
+ private Map<DexType, RClassFieldToValueStore> rClassFieldToValueStoreMap;
+
+ private ResourceAnalysisResult(
+ R8ResourceShrinkerState resourceShrinkerState,
+ Map<DexType, RClassFieldToValueStore> rClassFieldToValueStoreMap) {
+ this.resourceShrinkerState = resourceShrinkerState;
+ this.rClassFieldToValueStoreMap = rClassFieldToValueStoreMap;
+ }
+
+ public R8ResourceShrinkerModel getModel() {
+ return resourceShrinkerState.getR8ResourceShrinkerModel();
+ }
+
+ @SuppressWarnings("ReferenceEquality")
+ public void rewrittenWithLens(GraphLens lens, Timing timing) {
+ Map<DexType, DexType> changed = new IdentityHashMap<>();
+ for (DexType dexType : rClassFieldToValueStoreMap.keySet()) {
+ DexType rewritten = lens.lookupClassType(dexType);
+ if (rewritten != dexType) {
+ changed.put(dexType, rewritten);
+ }
+ }
+ if (changed.size() > 0) {
+ Map<DexType, RClassFieldToValueStore> rewrittenMap = new IdentityHashMap<>();
+ rClassFieldToValueStoreMap.forEach(
+ (type, map) -> {
+ rewrittenMap.put(changed.getOrDefault(type, type), map);
+ map.rewrittenWithLens(lens);
+ });
+ rClassFieldToValueStoreMap = rewrittenMap;
+ }
+ }
+
+ public void withoutPrunedItems(PrunedItems prunedItems, Timing timing) {
+ rClassFieldToValueStoreMap.keySet().removeIf(prunedItems::isRemoved);
+ rClassFieldToValueStoreMap.values().forEach(store -> store.pruneItems(prunedItems));
+ }
+
+ public String getSingleStringValueForField(ProgramField programField) {
+ RClassFieldToValueStore rClassFieldToValueStore =
+ rClassFieldToValueStoreMap.get(programField.getHolderType());
+ if (rClassFieldToValueStore == null) {
+ return null;
+ }
+ if (!rClassFieldToValueStore.hasField(programField.getReference())) {
+ return null;
+ }
+ return getModel()
+ .getStringResourcesWithSingleValue()
+ .get(rClassFieldToValueStore.getResourceId(programField.getReference()));
+ }
+ }
+
private static class RClassFieldToValueStore {
- private final Map<DexField, IntList> valueMapping;
+ private Map<DexField, IntList> valueMapping;
private RClassFieldToValueStore(Map<DexField, IntList> valueMapping) {
this.valueMapping = valueMapping;
}
+ boolean hasField(DexField field) {
+ return valueMapping.containsKey(field);
+ }
+
+ void pruneItems(PrunedItems prunedItems) {
+ valueMapping.keySet().removeIf(prunedItems::isRemoved);
+ }
+
+ int getResourceId(DexField field) {
+ IntList integers = valueMapping.get(field);
+ assert integers.size() == 1;
+ return integers.get(0);
+ }
+
+ @SuppressWarnings("ReferenceEquality")
+ public void rewrittenWithLens(GraphLens lens) {
+ Map<DexField, DexField> changed = new IdentityHashMap<>();
+ valueMapping
+ .keySet()
+ .forEach(
+ dexField -> {
+ DexField rewritten = lens.lookupField(dexField);
+ if (rewritten != dexField) {
+ changed.put(dexField, rewritten);
+ }
+ });
+ if (changed.size() > 0) {
+ Map<DexField, IntList> rewrittenMapping = new IdentityHashMap<>();
+ valueMapping.forEach(
+ (key, value) -> rewrittenMapping.put(changed.getOrDefault(key, key), value));
+ valueMapping = rewrittenMapping;
+ }
+ }
+
public static class Builder {
private final Map<DexField, IntList> valueMapping = new IdentityHashMap<>();
diff --git a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoResourceClasses.java b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoResourceClasses.java
index acf1bce..11a3951 100644
--- a/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoResourceClasses.java
+++ b/src/main/java/com/android/tools/r8/horizontalclassmerging/policies/NoResourceClasses.java
@@ -11,7 +11,8 @@
@Override
public boolean canMerge(DexProgramClass program) {
- return !program.getSimpleName().startsWith("R$");
+ String simpleName = program.getSimpleName();
+ return !simpleName.startsWith("R$") && !simpleName.contains("$R$");
}
@Override
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMemberOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMemberOptimizer.java
index f47e771..1dc3ca2 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMemberOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/LibraryMemberOptimizer.java
@@ -50,6 +50,10 @@
register(new ObjectsMethodOptimizer(appView));
register(new StringBuilderMethodOptimizer(appView));
register(new StringMethodOptimizer(appView));
+ if (appView.enableWholeProgramOptimizations()
+ && appView.options().resourceShrinkerConfiguration.isOptimizedShrinking()) {
+ register(new ResourcesMemberOptimizer(appView));
+ }
if (appView.enableWholeProgramOptimizations()) {
// Subtyping is required to prove the enum class is a subtype of java.lang.Enum.
register(new EnumMethodOptimizer(appView));
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/library/ResourcesMemberOptimizer.java b/src/main/java/com/android/tools/r8/ir/optimize/library/ResourcesMemberOptimizer.java
new file mode 100644
index 0000000..1246516
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/optimize/library/ResourcesMemberOptimizer.java
@@ -0,0 +1,147 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.ir.optimize.library;
+
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexClass;
+import com.android.tools.r8.graph.DexClassAndMethod;
+import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexField;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexString;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.FieldResolutionResult;
+import com.android.tools.r8.graph.ProgramField;
+import com.android.tools.r8.graph.TopDownClassHierarchyTraversal;
+import com.android.tools.r8.ir.code.BasicBlock;
+import com.android.tools.r8.ir.code.BasicBlockIterator;
+import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.InstructionListIterator;
+import com.android.tools.r8.ir.code.InvokeMethod;
+import com.android.tools.r8.ir.code.InvokeVirtual;
+import com.android.tools.r8.ir.optimize.AffectedValues;
+import java.util.IdentityHashMap;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+public class ResourcesMemberOptimizer extends StatelessLibraryMethodModelCollection {
+
+ private final AppView<?> appView;
+ private final DexItemFactory dexItemFactory;
+ private Optional<Boolean> allowStringInlining = Optional.empty();
+
+ ResourcesMemberOptimizer(AppView<?> appView) {
+ this.appView = appView;
+ this.dexItemFactory = appView.dexItemFactory();
+ }
+
+ @SuppressWarnings("ReferenceEquality")
+ private synchronized boolean allowInliningOfGetStringCalls() {
+ if (allowStringInlining.isPresent()) {
+ return allowStringInlining.get();
+ }
+ // TODO(b/312406163): Allow androidx classes that overwrite this, but don't change the value
+ // or have side effects.
+ allowStringInlining = Optional.of(true);
+ Map<DexClass, Boolean> cachedResults = new IdentityHashMap<>();
+ TopDownClassHierarchyTraversal.forProgramClasses(appView.withClassHierarchy())
+ .visit(
+ appView.appInfo().classes(),
+ clazz -> {
+ if (isResourcesSubtype(cachedResults, clazz)) {
+ DexEncodedMethod dexEncodedMethod =
+ clazz.lookupMethod(
+ dexItemFactory.androidResourcesGetStringProto,
+ dexItemFactory.androidResourcesGetStringName);
+ if (dexEncodedMethod != null) {
+ // TODO(b/312695444): Break out of traversal when supported.
+ allowStringInlining = Optional.of(false);
+ }
+ }
+ });
+ return allowStringInlining.get();
+ }
+
+ @SuppressWarnings("ReferenceEquality")
+ private boolean isResourcesSubtype(Map<DexClass, Boolean> cachedLookups, DexClass dexClass) {
+ Boolean cachedValue = cachedLookups.get(dexClass);
+ if (cachedValue != null) {
+ return cachedValue;
+ }
+ if (dexClass.type == dexItemFactory.androidResourcesType) {
+ return true;
+ }
+ if (dexClass.type == dexItemFactory.objectType) {
+ return false;
+ }
+
+ if (dexClass.superType != null) {
+ DexClass superClass = appView.definitionFor(dexClass.superType);
+ if (superClass != null) {
+ boolean superIsResourcesSubtype = isResourcesSubtype(cachedLookups, superClass);
+ cachedLookups.put(dexClass, superIsResourcesSubtype);
+ return superIsResourcesSubtype;
+ }
+ }
+ cachedLookups.put(dexClass, false);
+ return false;
+ }
+
+ @Override
+ public DexType getType() {
+ return dexItemFactory.androidResourcesType;
+ }
+
+ @Override
+ public InstructionListIterator optimize(
+ IRCode code,
+ BasicBlockIterator blockIterator,
+ InstructionListIterator instructionIterator,
+ InvokeMethod invoke,
+ DexClassAndMethod singleTarget,
+ AffectedValues affectedValues,
+ Set<BasicBlock> blocksToRemove) {
+ if (allowInliningOfGetStringCalls()
+ && singleTarget
+ .getReference()
+ .isIdenticalTo(dexItemFactory.androidResourcesGetStringMethod)) {
+ maybeInlineGetString(code, instructionIterator, invoke, affectedValues);
+ }
+ return instructionIterator;
+ }
+
+ @SuppressWarnings("ReferenceEquality")
+ private void maybeInlineGetString(
+ IRCode code,
+ InstructionListIterator instructionIterator,
+ InvokeMethod invoke,
+ AffectedValues affectedValues) {
+ if (invoke.isInvokeVirtual()) {
+ InvokeVirtual invokeVirtual = invoke.asInvokeVirtual();
+ DexMethod invokedMethod = invokeVirtual.getInvokedMethod();
+ assert invokedMethod.isIdenticalTo(dexItemFactory.androidResourcesGetStringMethod);
+ assert invoke.inValues().size() == 2;
+ Instruction valueDefinition = invoke.getLastArgument().definition;
+ if (valueDefinition != null && valueDefinition.isStaticGet()) {
+ DexField field = valueDefinition.asStaticGet().getField();
+ FieldResolutionResult fieldResolutionResult =
+ appView.appInfo().resolveField(field, code.context());
+ ProgramField resolvedField = fieldResolutionResult.getProgramField();
+ if (resolvedField != null) {
+ String singleStringValueForField =
+ appView.getResourceAnalysisResult().getSingleStringValueForField(resolvedField);
+ if (singleStringValueForField != null) {
+ DexString value = dexItemFactory.createString(singleStringValueForField);
+ instructionIterator.replaceCurrentInstructionWithConstString(
+ appView, code, value, affectedValues);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
index c8d9806..d1a2dda 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -3778,7 +3778,7 @@
}
}
- private void applyMinimumKeepInfoWhenLive(
+ public void applyMinimumKeepInfoWhenLive(
ProgramField field, KeepFieldInfo.Joiner minimumKeepInfo) {
applyMinimumKeepInfoWhenLive(field, minimumKeepInfo, EnqueuerEvent.unconditional());
}
diff --git a/src/resourceshrinker/java/com/android/build/shrinker/r8integration/R8ResourceShrinkerState.java b/src/resourceshrinker/java/com/android/build/shrinker/r8integration/R8ResourceShrinkerState.java
index 8273c96..e0cca4f 100644
--- a/src/resourceshrinker/java/com/android/build/shrinker/r8integration/R8ResourceShrinkerState.java
+++ b/src/resourceshrinker/java/com/android/build/shrinker/r8integration/R8ResourceShrinkerState.java
@@ -4,7 +4,11 @@
package com.android.build.shrinker.r8integration;
+import com.android.aapt.Resources.ConfigValue;
+import com.android.aapt.Resources.Entry;
+import com.android.aapt.Resources.Item;
import com.android.aapt.Resources.ResourceTable;
+import com.android.aapt.Resources.Value;
import com.android.build.shrinker.NoDebugReporter;
import com.android.build.shrinker.ResourceShrinkerModel;
import com.android.build.shrinker.ResourceTableUtilKt;
@@ -16,7 +20,9 @@
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
public class R8ResourceShrinkerState {
@@ -38,12 +44,17 @@
}
public static class R8ResourceShrinkerModel extends ResourceShrinkerModel {
+ private final Map<Integer, String> stringResourcesWithSingleValue = new HashMap<>();
public R8ResourceShrinkerModel(
ShrinkerDebugReporter debugReporter, boolean supportMultipackages) {
super(debugReporter, supportMultipackages);
}
+ public Map<Integer, String> getStringResourcesWithSingleValue() {
+ return stringResourcesWithSingleValue;
+ }
+
// Similar to instantiation in ProtoResourceTableGatherer, but using an inputstream.
void instantiateFromResourceTable(InputStream inputStream) {
try {
@@ -60,14 +71,34 @@
.forEachRemaining(
entryWrapper -> {
ResourceType resourceType = ResourceType.fromClassName(entryWrapper.getType());
+ Entry entry = entryWrapper.getEntry();
+ int entryId = entryWrapper.getId();
+ recordSingleValueResources(resourceType, entry, entryId);
if (resourceType != ResourceType.STYLEABLE) {
this.addResource(
resourceType,
entryWrapper.getPackageName(),
- ResourcesUtil.resourceNameToFieldName(entryWrapper.getEntry().getName()),
- entryWrapper.getId());
+ ResourcesUtil.resourceNameToFieldName(entry.getName()),
+ entryId);
}
});
}
+
+ private void recordSingleValueResources(ResourceType resourceType, Entry entry, int entryId) {
+ if (!entry.hasOverlayableItem() && entry.getConfigValueList().size() == 1) {
+ if (resourceType == ResourceType.STRING) {
+ ConfigValue configValue = entry.getConfigValue(0);
+ if (configValue.hasValue()) {
+ Value value = configValue.getValue();
+ if (value.hasItem()) {
+ Item item = value.getItem();
+ if (item.hasStr()) {
+ stringResourcesWithSingleValue.put(entryId, item.getStr().getValue());
+ }
+ }
+ }
+ }
+ }
+ }
}
}
diff --git a/src/test/java/com/android/tools/r8/R8TestBuilder.java b/src/test/java/com/android/tools/r8/R8TestBuilder.java
index 7ae3463..bbab1ac 100644
--- a/src/test/java/com/android/tools/r8/R8TestBuilder.java
+++ b/src/test/java/com/android/tools/r8/R8TestBuilder.java
@@ -291,6 +291,11 @@
return self();
}
+ public T enableOptimizedShrinking() {
+ builder.setResourceShrinkerConfiguration(b -> b.enableOptimizedShrinkingWithR8().build());
+ return self();
+ }
+
/**
* Allow info, warning, and error diagnostics.
*
diff --git a/src/test/java/com/android/tools/r8/androidresources/AndroidResourceTestingUtils.java b/src/test/java/com/android/tools/r8/androidresources/AndroidResourceTestingUtils.java
index 0ee66c7..785a049 100644
--- a/src/test/java/com/android/tools/r8/androidresources/AndroidResourceTestingUtils.java
+++ b/src/test/java/com/android/tools/r8/androidresources/AndroidResourceTestingUtils.java
@@ -10,9 +10,12 @@
import com.android.aapt.Resources.ConfigValue;
import com.android.aapt.Resources.Item;
import com.android.aapt.Resources.ResourceTable;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestBase.Backend;
import com.android.tools.r8.TestRuntime.CfRuntime;
import com.android.tools.r8.ToolHelper;
import com.android.tools.r8.ToolHelper.ProcessResult;
+import com.android.tools.r8.graph.DexItemFactory;
import com.android.tools.r8.transformers.ClassTransformer;
import com.android.tools.r8.transformers.MethodTransformer;
import com.android.tools.r8.utils.AndroidApiLevel;
@@ -34,12 +37,16 @@
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
+import java.util.Set;
import java.util.TreeMap;
+import java.util.TreeSet;
import java.util.stream.Collectors;
import org.junit.Assert;
import org.junit.rules.TemporaryFolder;
public class AndroidResourceTestingUtils {
+ private static final String RESOURCES_DESCRIPTOR =
+ TestBase.descriptor(com.android.tools.r8.androidresources.Resources.class);
enum RClassType {
STRING,
@@ -53,6 +60,32 @@
}
}
+ public static Path resourcesClassAsDex(TemporaryFolder temp) throws Exception {
+ return TestBase.testForD8(temp, Backend.DEX)
+ .addProgramClassFileData(resouresClassAsJavaClass())
+ .compile()
+ .writeToZip();
+ }
+
+ public static byte[] resouresClassAsJavaClass() throws IOException {
+ return transformer(com.android.tools.r8.androidresources.Resources.class)
+ .setClassDescriptor(DexItemFactory.androidResourcesDescriptorString)
+ .replaceClassDescriptorInMethodInstructions(
+ RESOURCES_DESCRIPTOR, DexItemFactory.androidResourcesDescriptorString)
+ .replaceClassDescriptorInMembers(
+ RESOURCES_DESCRIPTOR, DexItemFactory.androidResourcesDescriptorString)
+ .transform();
+ }
+
+ public static byte[] transformResourcesReferences(Class clazz) throws IOException {
+ return transformer(clazz)
+ .replaceClassDescriptorInMethodInstructions(
+ RESOURCES_DESCRIPTOR, DexItemFactory.androidResourcesDescriptorString)
+ .replaceClassDescriptorInMembers(
+ RESOURCES_DESCRIPTOR, DexItemFactory.androidResourcesDescriptorString)
+ .transform();
+ }
+
private static String rClassWithoutNamespaceAndOuter(Class clazz) {
return rClassWithoutNamespaceAndOuter(clazz.getName());
}
@@ -223,6 +256,8 @@
public static class AndroidTestResourceBuilder {
private String manifest;
private final Map<String, String> stringValues = new TreeMap<>();
+ private final Set<String> stringValuesWithExtraLanguage = new TreeSet<>();
+ private final Map<String, String> overlayableValues = new TreeMap<>();
private final Map<String, Integer> styleables = new TreeMap<>();
private final Map<String, byte[]> drawables = new TreeMap<>();
private final Map<String, String> xmlFiles = new TreeMap<>();
@@ -282,6 +317,16 @@
return this;
}
+ AndroidTestResourceBuilder addExtraLanguageString(String name) {
+ stringValuesWithExtraLanguage.add(name);
+ return this;
+ }
+
+ AndroidTestResourceBuilder setOverlayableFor(String type, String name) {
+ overlayableValues.put(type, name);
+ return this;
+ }
+
AndroidTestResourceBuilder setPackageId(int packageId) {
this.packageId = packageId;
return this;
@@ -298,7 +343,16 @@
Path resFolder = temp.newFolder("res").toPath();
Path valuesFolder = temp.newFolder("res", "values").toPath();
if (stringValues.size() > 0) {
- FileUtils.writeTextFile(valuesFolder.resolve("strings.xml"), createStringResourceXml());
+ FileUtils.writeTextFile(
+ valuesFolder.resolve("strings.xml"), createStringResourceXml(false));
+ if (stringValuesWithExtraLanguage.size() > 0) {
+ Path languageValues = temp.newFolder("res", "values-da").toPath();
+ FileUtils.writeTextFile(
+ languageValues.resolve("strings.xml"), createStringResourceXml(true));
+ }
+ }
+ if (overlayableValues.size() > 0) {
+ FileUtils.writeTextFile(valuesFolder.resolve("overlayable.xml"), createOverlayableXml());
}
if (styleables.size() > 0) {
FileUtils.writeTextFile(
@@ -401,11 +455,30 @@
new AndroidTestRClass(rClassJavaFile, rewrittenRClassFiles), output);
}
- private String createStringResourceXml() {
+ private String createOverlayableXml() {
StringBuilder stringBuilder = new StringBuilder("<resources>\n");
+
+ stringBuilder.append("<overlayable name=\"OurOverlayables\">\n");
+ stringBuilder.append("<policy type=\"public\">\n");
+ overlayableValues.forEach(
+ (type, name) ->
+ stringBuilder.append("<item type=\"" + type + "\" name=\"" + name + "\" />\n"));
+ stringBuilder.append("</policy>\n");
+ stringBuilder.append("</overlayable>");
+ stringBuilder.append("</resources>");
+ return stringBuilder.toString();
+ }
+
+ private String createStringResourceXml(boolean nonDefaultLanguage) {
+ StringBuilder stringBuilder = new StringBuilder("<resources>\n");
+ String languagePostFix = nonDefaultLanguage ? "_da" : "";
stringValues.forEach(
- (name, value) ->
- stringBuilder.append("<string name=\"" + name + "\">" + value + "</string>\n"));
+ (name, value) -> {
+ if (!nonDefaultLanguage || stringValuesWithExtraLanguage.contains(name)) {
+ stringBuilder.append(
+ "<string name=\"" + name + "\">" + value + languagePostFix + "</string>\n");
+ }
+ });
stringBuilder.append("</resources>");
return stringBuilder.toString();
}
@@ -496,4 +569,5 @@
// The XML document <x/> as a proto packed with AAPT2
public static final byte[] TINY_PROTO_XML =
new byte[] {0xa, 0x3, 0x1a, 0x1, 0x78, 0x1a, 0x2, 0x8, 0x1};
+
}
diff --git a/src/test/java/com/android/tools/r8/androidresources/Resources.java b/src/test/java/com/android/tools/r8/androidresources/Resources.java
new file mode 100644
index 0000000..ef01a99
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/androidresources/Resources.java
@@ -0,0 +1,16 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.androidresources;
+
+public class Resources {
+ public static String GET_STRING_VALUE = "GET_STRING_VALUE";
+
+ // Returns the GET_STRING_VALUE to be able to distinguish resource inlined values from values
+ // we get from this call (i.e., not inlined). Inlined values are the actual values from the
+ // resource table.
+ public String getString(int id) {
+ return GET_STRING_VALUE;
+ }
+}
diff --git a/src/test/java/com/android/tools/r8/androidresources/TestResourceInlining.java b/src/test/java/com/android/tools/r8/androidresources/TestResourceInlining.java
new file mode 100644
index 0000000..bd97b91
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/androidresources/TestResourceInlining.java
@@ -0,0 +1,173 @@
+// Copyright (c) 2023, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+package com.android.tools.r8.androidresources;
+
+import static com.android.tools.r8.utils.codeinspector.Matchers.isPresent;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.R8TestBuilder;
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.androidresources.AndroidResourceTestingUtils.AndroidTestResource;
+import com.android.tools.r8.androidresources.AndroidResourceTestingUtils.AndroidTestResourceBuilder;
+import com.android.tools.r8.graph.DexItemFactory;
+import com.android.tools.r8.utils.BooleanUtils;
+import com.android.tools.r8.utils.codeinspector.InstructionSubject;
+import com.android.tools.r8.utils.codeinspector.MethodSubject;
+import java.io.IOException;
+import java.nio.file.Path;
+import java.util.List;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameter;
+import org.junit.runners.Parameterized.Parameters;
+
+@RunWith(Parameterized.class)
+public class TestResourceInlining extends TestBase {
+
+ @Parameter(0)
+ public TestParameters parameters;
+
+ @Parameter(1)
+ public boolean optimize;
+
+ @Parameter(2)
+ public boolean addResourcesSubclass;
+
+ @Parameters(name = "{0}, optimize: {1}, addResourcesSubclass: {2}")
+ public static List<Object[]> data() {
+ return buildParameters(
+ getTestParameters().withDefaultDexRuntime().withAllApiLevels().build(),
+ BooleanUtils.values(),
+ BooleanUtils.values());
+ }
+
+ public static AndroidTestResource getTestResources(TemporaryFolder temp) throws Exception {
+ return new AndroidTestResourceBuilder()
+ .withSimpleManifestAndAppNameString()
+ .addRClassInitializeWithDefaultValues(R.string.class)
+ .setOverlayableFor("string", "overlayable")
+ .addExtraLanguageString("bar")
+ .build(temp);
+ }
+
+ public static Path getAndroidResourcesClassInJar(TemporaryFolder temp) throws Exception {
+ byte[] androidResourcesClass =
+ transformer(Resources.class)
+ .setClassDescriptor(DexItemFactory.androidResourcesDescriptorString)
+ .transform();
+ return testForR8(temp, Backend.DEX)
+ .addProgramClassFileData(androidResourcesClass)
+ .compile()
+ .writeToZip();
+ }
+
+ public byte[] getResourcesSubclass() throws IOException {
+ return transformer(ResourcesSubclass.class)
+ .setSuper(DexItemFactory.androidResourcesDescriptorString)
+ .replaceClassDescriptorInMethodInstructions(
+ descriptor(Resources.class), DexItemFactory.androidResourcesDescriptorString)
+ .transform();
+ }
+
+ @Test
+ public void testR8Optimized() throws Exception {
+ testForR8(parameters.getBackend())
+ .setMinApi(parameters)
+ .addProgramClassFileData(
+ AndroidResourceTestingUtils.transformResourcesReferences(FooBar.class))
+ .applyIf(
+ addResourcesSubclass,
+ builder -> {
+ builder.addProgramClassFileData(getResourcesSubclass());
+ builder.addKeepClassRules(ResourcesSubclass.class);
+ })
+ .addAndroidResources(getTestResources(temp))
+ .addKeepMainRule(FooBar.class)
+ .applyIf(optimize, R8TestBuilder::enableOptimizedShrinking)
+ .addRunClasspathFiles(AndroidResourceTestingUtils.resourcesClassAsDex(temp))
+ .compile()
+ .inspectShrunkenResources(
+ resourceTableInspector -> {
+ // We should eventually remove this when optimizing
+ resourceTableInspector.assertContainsResourceWithName("string", "foo");
+ // Has multiple values, don't inline
+ resourceTableInspector.assertContainsResourceWithName("string", "bar");
+ // Has overlayable value, don't inline
+ resourceTableInspector.assertContainsResourceWithName("string", "bar");
+ resourceTableInspector.assertDoesNotContainResourceWithName(
+ "string", "unused_string");
+ })
+ .inspect(
+ inspector -> {
+ // We should have removed one of the calls to getString if we are optimizing.
+ MethodSubject mainMethodSubject = inspector.clazz(FooBar.class).mainMethod();
+ assertThat(mainMethodSubject, isPresent());
+ assertEquals(
+ mainMethodSubject
+ .streamInstructions()
+ .filter(InstructionSubject::isInvokeVirtual)
+ .filter(
+ i ->
+ i.getMethod()
+ .holder
+ .descriptor
+ .toString()
+ .equals(DexItemFactory.androidResourcesDescriptorString))
+ .count(),
+ optimize && !addResourcesSubclass ? 2 : 3);
+ })
+ .run(parameters.getRuntime(), FooBar.class)
+ .applyIf(
+ optimize && !addResourcesSubclass,
+ result -> {
+ result.assertSuccessWithOutputLines(
+ "foo", Resources.GET_STRING_VALUE, Resources.GET_STRING_VALUE);
+ })
+ .applyIf(
+ !optimize || addResourcesSubclass,
+ result -> {
+ result.assertSuccessWithOutputLines(
+ Resources.GET_STRING_VALUE,
+ Resources.GET_STRING_VALUE,
+ Resources.GET_STRING_VALUE);
+ });
+ }
+
+ public static class FooBar {
+
+ public static void main(String[] args) {
+ Resources resources = new Resources();
+ // Ensure that we correctly handle the out value propagation
+ String s = resources.getString(R.string.foo);
+ String t = "X";
+ String u = System.currentTimeMillis() > 0 ? s : t;
+ System.out.println(u);
+ System.out.println(resources.getString(R.string.bar));
+ System.out.println(resources.getString(R.string.overlayable));
+ }
+ }
+
+ public static class ResourcesSubclass extends Resources {
+
+ @Override
+ public String getString(int id) {
+ System.out.println("foobar");
+ return super.getString(id);
+ }
+ }
+
+ public static class R {
+
+ public static class string {
+ public static int foo;
+ public static int bar;
+ public static int overlayable;
+ public static int unused_string;
+ }
+ }
+}