Add ResourceConstNumber for tracking resource references

This will add a special resource value to the IR for tracking resource
value references no matter if they are in an R class or not.

This means that we can more freely optimize callsites and remove the
fields from the R class.

Note that this is exclusively used in the optimization pipeline, we
map out again before writing.

Also note, that the ResourceAccessAnalysis tracing of the resources is
currently a no-op since it does not give rise to any code tracing (and
the actual tracing for the output resource table is done in the second
round use the ResourceConstNumbers)

Bug: 287398085
Change-Id: Ic9d648bb4bd086548b4500fb6a148e7519ba1f28
diff --git a/src/main/java/com/android/tools/r8/R8.java b/src/main/java/com/android/tools/r8/R8.java
index ed2b2cd..f9694e5 100644
--- a/src/main/java/com/android/tools/r8/R8.java
+++ b/src/main/java/com/android/tools/r8/R8.java
@@ -928,7 +928,9 @@
       LegacyResourceShrinker shrinker = resourceShrinkerBuilder.build();
       ShrinkerResult shrinkerResult;
       if (options.resourceShrinkerConfiguration.isOptimizedShrinking()) {
-        shrinkerResult = shrinker.shrinkModel(appView.getResourceAnalysisResult().getModel(), true);
+        shrinkerResult =
+            shrinker.shrinkModel(
+                appView.getResourceShrinkerState().getR8ResourceShrinkerModel(), true);
       } 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 84d8da6..e3c04a8 100644
--- a/src/main/java/com/android/tools/r8/graph/AppView.java
+++ b/src/main/java/com/android/tools/r8/graph/AppView.java
@@ -4,6 +4,7 @@
 
 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.classmerging.ClassMergerMode;
@@ -13,7 +14,6 @@
 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.AppliedGraphLens;
 import com.android.tools.r8.graph.lens.ClearCodeRewritingGraphLens;
@@ -154,7 +154,7 @@
 
   private SeedMapper applyMappingSeedMapper;
 
-  private ResourceAnalysisResult resourceAnalysisResult = null;
+  private R8ResourceShrinkerState resourceShrinkerState = null;
 
   // When input has been (partially) desugared these are the classes which has been library
   // desugared. This information is populated in the IR converter.
@@ -909,12 +909,12 @@
     testing().unboxedEnumsConsumer.accept(dexItemFactory(), unboxedEnums);
   }
 
-  public void setResourceAnalysisResult(ResourceAnalysisResult resourceAnalysisResult) {
-    this.resourceAnalysisResult = resourceAnalysisResult;
+  public R8ResourceShrinkerState getResourceShrinkerState() {
+    return resourceShrinkerState;
   }
 
-  public ResourceAnalysisResult getResourceAnalysisResult() {
-    return resourceAnalysisResult;
+  public void setResourceShrinkerState(R8ResourceShrinkerState resourceShrinkerState) {
+    this.resourceShrinkerState = resourceShrinkerState;
   }
 
   public boolean validateUnboxedEnumsHaveBeenPruned() {
@@ -1009,9 +1009,6 @@
       setProguardCompatibilityActions(
           getProguardCompatibilityActions().withoutPrunedItems(prunedItems, timing));
     }
-    if (resourceAnalysisResult != null) {
-      resourceAnalysisResult.withoutPrunedItems(prunedItems, timing);
-    }
     if (hasRootSet()) {
       rootSet.pruneItems(prunedItems, timing);
     }
@@ -1229,18 +1226,6 @@
               new ThreadTask() {
                 @Override
                 public void run(Timing threadTiming) {
-                  appView.resourceAnalysisResult.rewrittenWithLens(
-                      lens, appliedLensInModifiedLens, 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/UseRegistry.java b/src/main/java/com/android/tools/r8/graph/UseRegistry.java
index 1d9e498..d7d1b0b 100644
--- a/src/main/java/com/android/tools/r8/graph/UseRegistry.java
+++ b/src/main/java/com/android/tools/r8/graph/UseRegistry.java
@@ -158,6 +158,8 @@
     registerTypeReference(type);
   }
 
+  public void registerConstResourceNumber(int value) {}
+
   public void registerCheckCast(DexType type, boolean ignoreCompatRules) {
     registerTypeReference(type);
   }
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 d9547d9..6b64f53 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,10 +5,10 @@
 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;
+import com.android.tools.r8.errors.Unreachable;
 import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
 import com.android.tools.r8.graph.AppView;
 import com.android.tools.r8.graph.DexEncodedField;
@@ -19,8 +19,6 @@
 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;
@@ -29,14 +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;
-import java.util.List;
 import java.util.Map;
 
 public class ResourceAccessAnalysis implements EnqueuerFieldAccessAnalysis {
@@ -66,21 +60,23 @@
 
   public static void register(
       AppView<? extends AppInfoWithClassHierarchy> appView, Enqueuer enqueuer) {
-    if (enabled(appView)) {
+    if (enabled(appView, enqueuer)) {
       enqueuer.registerFieldAccessAnalysis(new ResourceAccessAnalysis(appView, enqueuer));
     }
   }
 
   @Override
   public void done(Enqueuer enqueuer) {
-    appView.setResourceAnalysisResult(
-        new ResourceAnalysisResult(resourceShrinkerState, fieldToValueMapping));
     EnqueuerFieldAccessAnalysis.super.done(enqueuer);
   }
 
-  private static boolean enabled(AppView<? extends AppInfoWithClassHierarchy> appView) {
+  private static boolean enabled(
+      AppView<? extends AppInfoWithClassHierarchy> appView, Enqueuer enqueuer) {
     return appView.options().androidResourceProvider != null
-        && appView.options().resourceShrinkerConfiguration.isOptimizedShrinking();
+        && appView.options().resourceShrinkerConfiguration.isOptimizedShrinking()
+        // Only run this in the first round, we explicitly trace the resource values
+        // with ResourceConstNumber in the optimizing pipeline.
+        && enqueuer.getMode().isInitialTreeShaking();
   }
 
   @Override
@@ -101,8 +97,6 @@
       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);
       }
@@ -140,6 +134,8 @@
       if (definition.isConstNumber()) {
         values = new IntArrayList(1);
         values.add(definition.asConstNumber().getIntValue());
+      } else if (definition.isResourceConstNumber()) {
+        throw new Unreachable("Only running ResourceAccessAnalysis in initial tree shaking");
       } else if (definition.isNewArrayEmpty()) {
         NewArrayEmpty newArrayEmpty = definition.asNewArrayEmpty();
         values = new IntArrayList();
@@ -148,6 +144,8 @@
             Value constValue = uniqueUser.asArrayPut().value();
             if (constValue.isConstNumber()) {
               values.add(constValue.getDefinition().asConstNumber().getIntValue());
+            } else if (constValue.isConstResourceNumber()) {
+              throw new Unreachable("Only running ResourceAccessAnalysis in initial tree shaking");
             }
           } else {
             assert uniqueUser == staticPut;
@@ -162,6 +160,8 @@
           Instruction valueDefinition = inValue.definition;
           if (valueDefinition.isConstNumber()) {
             values.add(valueDefinition.asConstNumber().getIntValue());
+          } else if (valueDefinition.isResourceConstNumber()) {
+            throw new Unreachable("Only running ResourceAccessAnalysis in initial tree shaking");
           }
         }
       } else {
@@ -192,79 +192,11 @@
     if (result != null) {
       return result;
     }
-    String simpleClassName =
-        DescriptorUtils.getSimpleClassNameFromDescriptor(holder.getType().toDescriptorString());
-    List<String> split = StringUtils.split(simpleClassName, '$');
-
-    if (split.size() < 2) {
-      cachedClassLookups.put(holder, false);
-      return false;
-    }
-    String type = split.get(split.size() - 1);
-    String rClass = split.get(split.size() - 2);
-    // We match on R if:
-    // - The name of the Class is R$type - we allow R to be an inner class.
-    //   - The inner type should be with lower case
-    boolean isRClass = Character.isLowerCase(type.charAt(0)) && rClass.equals("R");
+    boolean isRClass = DescriptorUtils.isRClassDescriptor(holder.getType().toDescriptorString());
     cachedClassLookups.put(holder, isRClass);
     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();
-    }
-
-    public void rewrittenWithLens(GraphLens lens, GraphLens appliedLens, Timing timing) {
-      Map<DexType, DexType> changed = new IdentityHashMap<>();
-      for (DexType dexType : rClassFieldToValueStoreMap.keySet()) {
-        DexType rewritten = lens.lookupClassType(dexType, appliedLens);
-        if (rewritten.isNotIdenticalTo(dexType)) {
-          changed.put(dexType, rewritten);
-        }
-      }
-      if (!changed.isEmpty()) {
-        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 Map<DexField, IntList> valueMapping;
 
@@ -272,40 +204,6 @@
       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/ir/analysis/value/AbstractValue.java b/src/main/java/com/android/tools/r8/ir/analysis/value/AbstractValue.java
index c71f037..e050ed4 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/value/AbstractValue.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/value/AbstractValue.java
@@ -205,6 +205,14 @@
     return null;
   }
 
+  public boolean isSingleResourceNumberValue() {
+    return false;
+  }
+
+  public SingleResourceNumberValue asSingleResourceNumberValue() {
+    return null;
+  }
+
   public boolean isSingleStringValue() {
     return false;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/value/AbstractValueFactory.java b/src/main/java/com/android/tools/r8/ir/analysis/value/AbstractValueFactory.java
index 79eca14..567d39a 100644
--- a/src/main/java/com/android/tools/r8/ir/analysis/value/AbstractValueFactory.java
+++ b/src/main/java/com/android/tools/r8/ir/analysis/value/AbstractValueFactory.java
@@ -26,6 +26,8 @@
       new ConcurrentHashMap<>();
   private final ConcurrentHashMap<Long, SingleNumberValue> singleNumberValues =
       new ConcurrentHashMap<>();
+  private final ConcurrentHashMap<Integer, SingleResourceNumberValue> singleResourceNumberValues =
+      new ConcurrentHashMap<>();
   private final ConcurrentHashMap<DexString, SingleStringValue> singleStringValues =
       new ConcurrentHashMap<>();
   private final ConcurrentHashMap<Integer, KnownLengthArrayState> knownArrayLengthStates =
@@ -122,6 +124,10 @@
     return createUncheckedSingleNumberValue(value);
   }
 
+  public SingleResourceNumberValue createSingleResourceNumberValue(int value) {
+    return singleResourceNumberValues.computeIfAbsent(value, SingleResourceNumberValue::new);
+  }
+
   public SingleNumberValue createUncheckedSingleNumberValue(long value) {
     return singleNumberValues.computeIfAbsent(value, SingleNumberValue::new);
   }
diff --git a/src/main/java/com/android/tools/r8/ir/analysis/value/SingleResourceNumberValue.java b/src/main/java/com/android/tools/r8/ir/analysis/value/SingleResourceNumberValue.java
new file mode 100644
index 0000000..8be7c10
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/analysis/value/SingleResourceNumberValue.java
@@ -0,0 +1,119 @@
+// Copyright (c) 2024, 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.analysis.value;
+
+import com.android.tools.r8.graph.AppInfoWithClassHierarchy;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DebugLocalInfo;
+import com.android.tools.r8.graph.DexType;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.graph.lens.GraphLens;
+import com.android.tools.r8.graph.proto.ArgumentInfoCollection;
+import com.android.tools.r8.ir.analysis.type.TypeElement;
+import com.android.tools.r8.ir.code.Instruction;
+import com.android.tools.r8.ir.code.MaterializingInstructionsInfo;
+import com.android.tools.r8.ir.code.Position;
+import com.android.tools.r8.ir.code.ResourceConstNumber;
+import com.android.tools.r8.ir.code.ValueFactory;
+import com.android.tools.r8.ir.optimize.info.field.InstanceFieldInitializationInfo;
+import com.android.tools.r8.shaking.AppInfoWithLiveness;
+
+public class SingleResourceNumberValue extends SingleConstValue {
+
+  private final int value;
+
+  /** Intentionally package private, use {@link AbstractValueFactory} instead. */
+  SingleResourceNumberValue(int value) {
+    this.value = value;
+  }
+
+  @Override
+  public boolean hasSingleMaterializingInstruction() {
+    return true;
+  }
+
+  @Override
+  public boolean isSingleResourceNumberValue() {
+    return true;
+  }
+
+  @Override
+  public SingleResourceNumberValue asSingleResourceNumberValue() {
+    return this;
+  }
+
+  public int getValue() {
+    return value;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    return this == o;
+  }
+
+  @Override
+  public int hashCode() {
+    return System.identityHashCode(this);
+  }
+
+  @Override
+  public String toString() {
+    return "SingleResourceNumberValue(" + value + ")";
+  }
+
+  @Override
+  public Instruction[] createMaterializingInstructions(
+      AppView<?> appView,
+      ProgramMethod context,
+      ValueFactory valueFactory,
+      MaterializingInstructionsInfo info) {
+    ResourceConstNumber materializingInstruction =
+        createMaterializingInstruction(appView, valueFactory, info);
+    return new Instruction[] {materializingInstruction};
+  }
+
+  public ResourceConstNumber createMaterializingInstruction(
+      AppView<?> appView, ValueFactory valueFactory, MaterializingInstructionsInfo info) {
+    return createMaterializingInstruction(
+        appView, valueFactory, info.getOutType(), info.getLocalInfo(), info.getPosition());
+  }
+
+  public ResourceConstNumber createMaterializingInstruction(
+      AppView<?> appView,
+      ValueFactory valueFactory,
+      TypeElement type,
+      DebugLocalInfo localInfo,
+      Position position) {
+    assert type.isInt();
+    return ResourceConstNumber.builder()
+        .setFreshOutValue(valueFactory, type, localInfo)
+        .setPositionForNonThrowingInstruction(position, appView.options())
+        .setValue(value)
+        .build();
+  }
+
+  @Override
+  boolean internalIsMaterializableInContext(
+      AppView<? extends AppInfoWithClassHierarchy> appView, ProgramMethod context) {
+    return true;
+  }
+
+  @Override
+  public boolean isMaterializableInAllContexts(AppView<AppInfoWithLiveness> appView) {
+    return true;
+  }
+
+  @Override
+  public InstanceFieldInitializationInfo fixupAfterParametersChanged(
+      ArgumentInfoCollection argumentInfoCollection) {
+    return this;
+  }
+
+  @Override
+  public SingleValue rewrittenWithLens(
+      AppView<AppInfoWithLiveness> appView, DexType newType, GraphLens lens, GraphLens codeLens) {
+    return this;
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/code/DefaultInstructionVisitor.java b/src/main/java/com/android/tools/r8/ir/code/DefaultInstructionVisitor.java
index a21882a..a3bbdee 100644
--- a/src/main/java/com/android/tools/r8/ir/code/DefaultInstructionVisitor.java
+++ b/src/main/java/com/android/tools/r8/ir/code/DefaultInstructionVisitor.java
@@ -95,6 +95,11 @@
   }
 
   @Override
+  public T visit(ResourceConstNumber instruction) {
+    return null;
+  }
+
+  @Override
   public T visit(ConstString instruction) {
     return null;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/IRMetadata.java b/src/main/java/com/android/tools/r8/ir/code/IRMetadata.java
index 2d8cc3e..56f38c3 100644
--- a/src/main/java/com/android/tools/r8/ir/code/IRMetadata.java
+++ b/src/main/java/com/android/tools/r8/ir/code/IRMetadata.java
@@ -103,6 +103,10 @@
     return get(Opcodes.CONST_NUMBER);
   }
 
+  public boolean mayHaveResourceConstNumber() {
+    return get(Opcodes.RESOURCE_CONST_NUMBER);
+  }
+
   public boolean mayHaveConstString() {
     return get(Opcodes.CONST_STRING);
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/Instruction.java b/src/main/java/com/android/tools/r8/ir/code/Instruction.java
index d0426ec..08143ae 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Instruction.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Instruction.java
@@ -869,6 +869,14 @@
     return null;
   }
 
+  public boolean isResourceConstNumber() {
+    return false;
+  }
+
+  public ResourceConstNumber asResourceConstNumber() {
+    return null;
+  }
+
   public boolean isConstInstruction() {
     return false;
   }
diff --git a/src/main/java/com/android/tools/r8/ir/code/InstructionVisitor.java b/src/main/java/com/android/tools/r8/ir/code/InstructionVisitor.java
index 8318365..62bd3de 100644
--- a/src/main/java/com/android/tools/r8/ir/code/InstructionVisitor.java
+++ b/src/main/java/com/android/tools/r8/ir/code/InstructionVisitor.java
@@ -38,6 +38,8 @@
 
   T visit(ConstNumber instruction);
 
+  T visit(ResourceConstNumber instruction);
+
   T visit(ConstString instruction);
 
   T visit(DebugLocalRead instruction);
diff --git a/src/main/java/com/android/tools/r8/ir/code/Opcodes.java b/src/main/java/com/android/tools/r8/ir/code/Opcodes.java
index 44ad346..58889ac 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Opcodes.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Opcodes.java
@@ -77,4 +77,5 @@
   int XOR = 68;
   int UNINITIALIZED_THIS_LOCAL_READ = 69;
   int RECORD_FIELD_VALUES = 70;
+  int RESOURCE_CONST_NUMBER = 71;
 }
diff --git a/src/main/java/com/android/tools/r8/ir/code/ResourceConstNumber.java b/src/main/java/com/android/tools/r8/ir/code/ResourceConstNumber.java
new file mode 100644
index 0000000..d18a410
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/code/ResourceConstNumber.java
@@ -0,0 +1,162 @@
+// Copyright (c) 2024, 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.code;
+
+import com.android.tools.r8.cf.LoadStoreHelper;
+import com.android.tools.r8.errors.Unreachable;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.ProgramMethod;
+import com.android.tools.r8.ir.analysis.type.TypeElement;
+import com.android.tools.r8.ir.analysis.value.AbstractValue;
+import com.android.tools.r8.ir.conversion.CfBuilder;
+import com.android.tools.r8.ir.conversion.DexBuilder;
+import com.android.tools.r8.lightir.LirBuilder;
+
+/**
+ * Instruction representing the SSA value an R class field value.
+ *
+ * <p>This instruction allows us to correctly trace inlined resource values in the second round of
+ * tree shaking.
+ *
+ * <p>The instruction is simple converted back to a const number before writing.
+ */
+public class ResourceConstNumber extends ConstInstruction {
+
+  private final int value;
+
+  public ResourceConstNumber(Value dest, int value) {
+    super(dest);
+    assert dest.type.isPrimitiveType();
+    this.value = value;
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  @Override
+  public int opcode() {
+    return Opcodes.RESOURCE_CONST_NUMBER;
+  }
+
+  @Override
+  public <T> T accept(InstructionVisitor<T> visitor) {
+    return visitor.visit(this);
+  }
+
+  @Override
+  public boolean instructionTypeCanBeCanonicalized() {
+    return true;
+  }
+
+  @Override
+  public void buildDex(DexBuilder builder) {
+    throw new Unreachable("We never write out ResourceConstNumber");
+  }
+
+  @Override
+  public void insertLoadAndStores(InstructionListIterator it, LoadStoreHelper helper) {
+    throw new Unreachable("We never write cf code with resource numbers");
+  }
+
+  public Value dest() {
+    return outValue;
+  }
+
+  @Override
+  public void buildCf(CfBuilder builder) {
+    throw new Unreachable("We never write out a resource const number");
+  }
+
+  @Override
+  public int maxInValueRegister() {
+    throw new Unreachable("We map out of ResourceConstNumber before register allocation");
+  }
+
+  @Override
+  public int maxOutValueRegister() {
+    throw new Unreachable("We map out of ResourceConstNumber before register allocation");
+  }
+
+  @Override
+  public boolean identicalNonValueNonPositionParts(Instruction other) {
+    if (other == this) {
+      return true;
+    }
+    if (!other.isResourceConstNumber()) {
+      return false;
+    }
+    ResourceConstNumber otherNumber = other.asResourceConstNumber();
+    return otherNumber.getValue() == getValue();
+  }
+
+  @Override
+  public boolean isOutConstant() {
+    return true;
+  }
+
+  @Override
+  public boolean isResourceConstNumber() {
+    return true;
+  }
+
+  @Override
+  public TypeElement evaluate(AppView<?> appView) {
+    assert getOutType().isInt();
+    return TypeElement.getInt();
+  }
+
+  @Override
+  public AbstractValue getAbstractValue(
+      AppView<?> appView, ProgramMethod context, AbstractValueSupplier abstractValueSupplier) {
+    if (outValue.hasLocalInfo()) {
+      return AbstractValue.unknown();
+    }
+    return appView.abstractValueFactory().createSingleResourceNumberValue(getValue());
+  }
+
+  @Override
+  public void buildLir(LirBuilder<Value, ?> builder) {
+    builder.addResourceConstNumber(getValue());
+  }
+
+  public static ResourceConstNumber copyOf(IRCode code, ResourceConstNumber original) {
+    Value newValue = code.createValue(TypeElement.getInt(), original.getLocalInfo());
+    return new ResourceConstNumber(newValue, original.getValue());
+  }
+
+  @Override
+  public ResourceConstNumber asResourceConstNumber() {
+    return this;
+  }
+
+  public int getValue() {
+    return value;
+  }
+
+  public static class Builder extends BuilderBase<Builder, ResourceConstNumber> {
+
+    private int value;
+
+    public Builder setValue(int value) {
+      this.value = value;
+      return this;
+    }
+
+    @Override
+    public ResourceConstNumber build() {
+      return amend(new ResourceConstNumber(outValue, value));
+    }
+
+    @Override
+    public Builder self() {
+      return this;
+    }
+
+    @Override
+    protected boolean verifyInstructionTypeCannotThrow() {
+      return true;
+    }
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/code/Value.java b/src/main/java/com/android/tools/r8/ir/code/Value.java
index fb150fb..0a16889 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Value.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Value.java
@@ -842,6 +842,10 @@
     return isConstant() && getConstInstruction().isConstNumber();
   }
 
+  public boolean isConstResourceNumber() {
+    return isConstant() && getConstInstruction().isResourceConstNumber();
+  }
+
   public boolean isConstNumber(long rawValue) {
     return isConstant()
         && getConstInstruction().isConstNumber()
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/LirConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/LirConverter.java
index 2c7d62b..5f5a9c9 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/LirConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/LirConverter.java
@@ -12,6 +12,8 @@
 import com.android.tools.r8.graph.bytecodemetadata.BytecodeMetadataProvider;
 import com.android.tools.r8.graph.lens.GraphLens;
 import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.conversion.passes.ConstResourceNumberRemover;
+import com.android.tools.r8.ir.conversion.passes.ConstResourceNumberRewriter;
 import com.android.tools.r8.ir.conversion.passes.FilledNewArrayRewriter;
 import com.android.tools.r8.ir.optimize.ConstantCanonicalizer;
 import com.android.tools.r8.ir.optimize.DeadCodeRemover;
@@ -34,6 +36,8 @@
     assert appView.testing().canUseLir(appView);
     assert appView.testing().isPreLirPhase();
     appView.testing().enterLirSupportedPhase();
+    ConstResourceNumberRewriter constResourceNumberRewriter =
+        new ConstResourceNumberRewriter(appView);
     // Convert code objects to LIR.
     ThreadUtils.processItems(
         appView.appInfo().classes(),
@@ -45,10 +49,11 @@
           clazz.forEachProgramMethodMatching(
               method ->
                   method.hasCode()
-                      && !method.isInitializer()
+                      && !method.isInstanceInitializer()
                       && !appView.isCfByteCodePassThrough(method),
               method -> {
                 IRCode code = method.buildIR(appView, MethodConversionOptions.forLirPhase(appView));
+                constResourceNumberRewriter.run(code, Timing.empty());
                 LirCode<Integer> lirCode =
                     IR2LirConverter.translate(
                         code,
@@ -179,6 +184,8 @@
     }
     IRCode irCode = method.buildIR(appView, MethodConversionOptions.forPostLirPhase(appView));
     assert irCode.verifyInvokeInterface(appView);
+    ConstResourceNumberRemover constResourceNumberRemover = new ConstResourceNumberRemover(appView);
+    constResourceNumberRemover.run(irCode, onThreadTiming);
     FilledNewArrayRewriter filledNewArrayRewriter = new FilledNewArrayRewriter(appView);
     boolean changed = filledNewArrayRewriter.run(irCode, onThreadTiming).hasChanged().toBoolean();
     if (appView.options().isGeneratingDex() && changed) {
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/ConstResourceNumberRemover.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/ConstResourceNumberRemover.java
new file mode 100644
index 0000000..ca379dd
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/ConstResourceNumberRemover.java
@@ -0,0 +1,48 @@
+// Copyright (c) 2024, 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.conversion.passes;
+
+import com.android.tools.r8.graph.AppInfo;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.ir.code.ConstNumber;
+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.ResourceConstNumber;
+import com.android.tools.r8.ir.conversion.MethodProcessor;
+import com.android.tools.r8.ir.conversion.passes.result.CodeRewriterResult;
+
+public class ConstResourceNumberRemover extends CodeRewriterPass<AppInfo> {
+
+  public ConstResourceNumberRemover(AppView<?> appView) {
+    super(appView);
+  }
+
+  @Override
+  protected String getRewriterId() {
+    return "ConstResourceNumberRemover";
+  }
+
+  @Override
+  protected boolean shouldRewriteCode(IRCode code, MethodProcessor methodProcessor) {
+    return code.metadata().mayHaveResourceConstNumber();
+  }
+
+  @Override
+  protected CodeRewriterResult rewriteCode(IRCode code) {
+    boolean hasChanged = false;
+    InstructionListIterator iterator = code.instructionListIterator();
+    while (iterator.hasNext()) {
+      Instruction current = iterator.next();
+      if (current.isResourceConstNumber()) {
+        ResourceConstNumber resourceConstNumber = current.asResourceConstNumber();
+        iterator.replaceCurrentInstruction(
+            new ConstNumber(resourceConstNumber.dest(), resourceConstNumber.getValue()));
+        hasChanged = true;
+      }
+    }
+    return CodeRewriterResult.hasChanged(hasChanged);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/ConstResourceNumberRewriter.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/ConstResourceNumberRewriter.java
new file mode 100644
index 0000000..e6806fc
--- /dev/null
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/ConstResourceNumberRewriter.java
@@ -0,0 +1,65 @@
+// Copyright (c) 2024, 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.conversion.passes;
+
+import com.android.tools.r8.graph.AppInfo;
+import com.android.tools.r8.graph.AppView;
+import com.android.tools.r8.graph.DexProgramClass;
+import com.android.tools.r8.ir.code.ConstNumber;
+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.ResourceConstNumber;
+import com.android.tools.r8.ir.code.Value;
+import com.android.tools.r8.ir.conversion.MethodProcessor;
+import com.android.tools.r8.ir.conversion.passes.result.CodeRewriterResult;
+import com.android.tools.r8.utils.DescriptorUtils;
+
+public class ConstResourceNumberRewriter extends CodeRewriterPass<AppInfo> {
+  public ConstResourceNumberRewriter(AppView<?> appView) {
+    super(appView);
+  }
+
+  @Override
+  protected String getRewriterId() {
+    return "ConstResourceNumberRewriter";
+  }
+
+  @Override
+  protected boolean shouldRewriteCode(IRCode code, MethodProcessor methodProcessor) {
+    return appView.options().isOptimizedResourceShrinking()
+        && code.context().getDefinition().isClassInitializer()
+        && isRClass(code.context().getHolder());
+  }
+
+  private boolean isRClass(DexProgramClass holder) {
+    return DescriptorUtils.isRClassDescriptor(holder.getType().toDescriptorString());
+  }
+
+  @Override
+  protected CodeRewriterResult rewriteCode(IRCode code) {
+    boolean hasChanged = false;
+    InstructionListIterator iterator = code.instructionListIterator();
+    while (iterator.hasNext()) {
+      Instruction current = iterator.next();
+      if (current.isConstNumber()) {
+        ConstNumber constNumber = current.asConstNumber();
+        // The resource const numbers should always have a single value here
+        Value currentValue = current.outValue();
+        if (currentValue.hasSingleUniqueUser() && !currentValue.hasPhiUsers()) {
+          Instruction singleUser = currentValue.singleUniqueUser();
+          if (singleUser.isStaticPut()
+              || singleUser.isNewArrayFilled()
+              || (singleUser.isArrayPut() && singleUser.asArrayPut().value() == currentValue)) {
+            iterator.replaceCurrentInstruction(
+                new ResourceConstNumber(constNumber.dest(), constNumber.getIntValue()));
+            hasChanged = true;
+          }
+        }
+      }
+    }
+    return CodeRewriterResult.hasChanged(hasChanged);
+  }
+}
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/passes/DexConstantOptimizer.java b/src/main/java/com/android/tools/r8/ir/conversion/passes/DexConstantOptimizer.java
index 9584e8a..a213f7f 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/passes/DexConstantOptimizer.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/passes/DexConstantOptimizer.java
@@ -9,6 +9,7 @@
 import static com.android.tools.r8.ir.code.Opcodes.CONST_STRING;
 import static com.android.tools.r8.ir.code.Opcodes.DEX_ITEM_BASED_CONST_STRING;
 import static com.android.tools.r8.ir.code.Opcodes.INSTANCE_GET;
+import static com.android.tools.r8.ir.code.Opcodes.RESOURCE_CONST_NUMBER;
 import static com.android.tools.r8.ir.code.Opcodes.STATIC_GET;
 
 import com.android.tools.r8.errors.Unreachable;
@@ -29,6 +30,7 @@
 import com.android.tools.r8.ir.code.InstructionOrPhi;
 import com.android.tools.r8.ir.code.Phi;
 import com.android.tools.r8.ir.code.Position;
+import com.android.tools.r8.ir.code.ResourceConstNumber;
 import com.android.tools.r8.ir.code.StaticGet;
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.conversion.MethodProcessor;
@@ -466,6 +468,9 @@
         case CONST_NUMBER:
           copy = ConstNumber.copyOf(code, instruction.asConstNumber());
           break;
+        case RESOURCE_CONST_NUMBER:
+          copy = ResourceConstNumber.copyOf(code, instruction.asResourceConstNumber());
+          break;
         case CONST_STRING:
           copy = ConstString.copyOf(code, instruction.asConstString());
           break;
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/ClassInitializerDefaultsOptimization.java b/src/main/java/com/android/tools/r8/ir/optimize/ClassInitializerDefaultsOptimization.java
index 6880ed1..f10854f 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/ClassInitializerDefaultsOptimization.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/ClassInitializerDefaultsOptimization.java
@@ -440,7 +440,7 @@
                     isWrittenBefore.remove(fieldReference);
                   }
                   continue;
-                } else if (fieldReference.type.isPrimitiveType()
+                } else if ((fieldReference.type.isPrimitiveType() && !hasPutOfConstResource(put))
                     || fieldReference.type == dexItemFactory.stringType) {
                   finalFieldPuts.put(field, put);
                   unnecessaryStaticPuts.add(put);
@@ -507,6 +507,10 @@
     return validateFinalFieldPuts(finalFieldPuts, isWrittenBefore);
   }
 
+  private boolean hasPutOfConstResource(StaticPut put) {
+    return put.value().isConstResourceNumber();
+  }
+
   private Map<DexEncodedField, StaticPut> validateFinalFieldPuts(
       Map<DexEncodedField, StaticPut> finalFieldPuts,
       Map<DexField, Set<StaticPut>> isWrittenBefore) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/ConstantCanonicalizer.java b/src/main/java/com/android/tools/r8/ir/optimize/ConstantCanonicalizer.java
index 1ef539a..c1f203a 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/ConstantCanonicalizer.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/ConstantCanonicalizer.java
@@ -8,6 +8,7 @@
 import static com.android.tools.r8.ir.code.Opcodes.CONST_STRING;
 import static com.android.tools.r8.ir.code.Opcodes.DEX_ITEM_BASED_CONST_STRING;
 import static com.android.tools.r8.ir.code.Opcodes.INSTANCE_GET;
+import static com.android.tools.r8.ir.code.Opcodes.RESOURCE_CONST_NUMBER;
 import static com.android.tools.r8.ir.code.Opcodes.STATIC_GET;
 import static com.android.tools.r8.utils.MapUtils.ignoreKey;
 
@@ -39,6 +40,7 @@
 import com.android.tools.r8.ir.code.NewInstance;
 import com.android.tools.r8.ir.code.Phi;
 import com.android.tools.r8.ir.code.Position;
+import com.android.tools.r8.ir.code.ResourceConstNumber;
 import com.android.tools.r8.ir.code.StaticGet;
 import com.android.tools.r8.ir.code.Value;
 import com.android.tools.r8.ir.conversion.passes.BranchSimplifier;
@@ -164,6 +166,8 @@
                   case CONST_NUMBER:
                     return Long.hashCode(candidate.asConstNumber().getRawValue())
                         + 13 * candidate.outType().hashCode();
+                  case RESOURCE_CONST_NUMBER:
+                    return Integer.hashCode(candidate.asResourceConstNumber().getValue());
                   case CONST_STRING:
                     return candidate.asConstString().getValue().hashCode();
                   case DEX_ITEM_BASED_CONST_STRING:
@@ -349,6 +353,7 @@
     switch (newInstruction.opcode()) {
       case CONST_CLASS:
       case CONST_NUMBER:
+      case RESOURCE_CONST_NUMBER:
       case CONST_STRING:
       case DEX_ITEM_BASED_CONST_STRING:
       case STATIC_GET:
@@ -388,6 +393,8 @@
         return ConstClass.copyOf(code, canonicalizedConstant.asConstClass());
       case CONST_NUMBER:
         return ConstNumber.copyOf(code, canonicalizedConstant.asConstNumber());
+      case RESOURCE_CONST_NUMBER:
+        return ResourceConstNumber.copyOf(code, canonicalizedConstant.asResourceConstNumber());
       case CONST_STRING:
         return ConstString.copyOf(code, canonicalizedConstant.asConstString());
       case DEX_ITEM_BASED_CONST_STRING:
@@ -434,6 +441,7 @@
           return false;
         }
         break;
+      case RESOURCE_CONST_NUMBER:
       case CONST_NUMBER:
         break;
       case CONST_STRING:
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadAndStoreElimination.java b/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadAndStoreElimination.java
index ee03422..d9acaba 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadAndStoreElimination.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/RedundantFieldLoadAndStoreElimination.java
@@ -442,6 +442,7 @@
                       || instruction.isConstMethodHandle()
                       || instruction.isConstMethodType()
                       || instruction.isConstNumber()
+                      || instruction.isResourceConstNumber()
                       || instruction.isConstString()
                       || instruction.isDebugInstruction()
                       || instruction.isDexItemBasedConstString()
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
index 1246516..207d757 100644
--- 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
@@ -8,13 +8,10 @@
 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;
@@ -127,19 +124,16 @@
       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);
-          }
+      if (valueDefinition.isResourceConstNumber()) {
+        String singleStringValue =
+            appView
+                .getResourceShrinkerState()
+                .getR8ResourceShrinkerModel()
+                .getSingleStringValueOrNull(valueDefinition.asResourceConstNumber().getValue());
+        if (singleStringValue != null) {
+          DexString value = dexItemFactory.createString(singleStringValue);
+          instructionIterator.replaceCurrentInstructionWithConstString(
+              appView, code, value, affectedValues);
         }
       }
     }
diff --git a/src/main/java/com/android/tools/r8/lightir/Lir2IRConverter.java b/src/main/java/com/android/tools/r8/lightir/Lir2IRConverter.java
index 46e73aa..d7ee8d3 100644
--- a/src/main/java/com/android/tools/r8/lightir/Lir2IRConverter.java
+++ b/src/main/java/com/android/tools/r8/lightir/Lir2IRConverter.java
@@ -83,6 +83,7 @@
 import com.android.tools.r8.ir.code.Position;
 import com.android.tools.r8.ir.code.RecordFieldValues;
 import com.android.tools.r8.ir.code.Rem;
+import com.android.tools.r8.ir.code.ResourceConstNumber;
 import com.android.tools.r8.ir.code.Return;
 import com.android.tools.r8.ir.code.SafeCheckCast;
 import com.android.tools.r8.ir.code.Shl;
@@ -518,6 +519,12 @@
     }
 
     @Override
+    public void onConstResourceNumber(int value) {
+      Value dest = getOutValueForNextInstruction(TypeElement.getInt());
+      addInstruction(new ResourceConstNumber(dest, value));
+    }
+
+    @Override
     public void onAdd(NumericType type, EV leftValueIndex, EV rightValueIndex) {
       Value dest = getOutValueForNextInstruction(valueTypeElement(type));
       addInstruction(
diff --git a/src/main/java/com/android/tools/r8/lightir/LirBuilder.java b/src/main/java/com/android/tools/r8/lightir/LirBuilder.java
index 5d988d8..c243c7b 100644
--- a/src/main/java/com/android/tools/r8/lightir/LirBuilder.java
+++ b/src/main/java/com/android/tools/r8/lightir/LirBuilder.java
@@ -579,6 +579,13 @@
     }
   }
 
+  public LirBuilder<V, EV> addResourceConstNumber(int value) {
+    advanceInstructionState();
+    writer.writeInstruction(LirOpcodes.RESOURCENUMBER, ByteUtils.intEncodingSize(value));
+    ByteUtils.writeEncodedInt(value, writer::writeOperand);
+    return this;
+  }
+
   public LirBuilder<V, EV> addConstString(DexString string) {
     return addOneItemInstruction(LirOpcodes.LDC, string);
   }
diff --git a/src/main/java/com/android/tools/r8/lightir/LirOpcodes.java b/src/main/java/com/android/tools/r8/lightir/LirOpcodes.java
index 393cd05..702dbba 100644
--- a/src/main/java/com/android/tools/r8/lightir/LirOpcodes.java
+++ b/src/main/java/com/android/tools/r8/lightir/LirOpcodes.java
@@ -212,6 +212,7 @@
   int CHECKCAST_IGNORE_COMPAT = 225;
   int CONSTCLASS_IGNORE_COMPAT = 226;
   int STRINGSWITCH = 227;
+  int RESOURCENUMBER = 228;
 
   static String toString(int opcode) {
     switch (opcode) {
@@ -551,6 +552,8 @@
         return "CONSTCLASS_IGNORE_COMPAT";
       case STRINGSWITCH:
         return "STRINGSWITCH";
+      case RESOURCENUMBER:
+        return "RESOURCENUMBER";
 
       default:
         throw new Unreachable("Unexpected LIR opcode: " + opcode);
diff --git a/src/main/java/com/android/tools/r8/lightir/LirParsedInstructionCallback.java b/src/main/java/com/android/tools/r8/lightir/LirParsedInstructionCallback.java
index cdd50cc..4fe7d83 100644
--- a/src/main/java/com/android/tools/r8/lightir/LirParsedInstructionCallback.java
+++ b/src/main/java/com/android/tools/r8/lightir/LirParsedInstructionCallback.java
@@ -82,6 +82,10 @@
     onConstNumber(NumericType.INT, value);
   }
 
+  public void onConstResourceNumber(int value) {
+    onInstruction();
+  }
+
   public void onConstFloat(int value) {
     onConstNumber(NumericType.FLOAT, value);
   }
@@ -1278,6 +1282,12 @@
           onRecordFieldValues(payload.fields, values);
           return;
         }
+      case LirOpcodes.RESOURCENUMBER:
+        {
+          int value = view.getNextIntegerOperand();
+          onConstResourceNumber(value);
+          return;
+        }
       default:
         throw new Unimplemented("No dispatch for opcode " + LirOpcodes.toString(opcode));
     }
diff --git a/src/main/java/com/android/tools/r8/lightir/LirPrinter.java b/src/main/java/com/android/tools/r8/lightir/LirPrinter.java
index 2e0273d..c27b1ed 100644
--- a/src/main/java/com/android/tools/r8/lightir/LirPrinter.java
+++ b/src/main/java/com/android/tools/r8/lightir/LirPrinter.java
@@ -153,6 +153,11 @@
   }
 
   @Override
+  public void onConstResourceNumber(int value) {
+    appendOutValue().append(value);
+  }
+
+  @Override
   public void onConstFloat(int value) {
     appendOutValue().append(Float.intBitsToFloat(value));
   }
diff --git a/src/main/java/com/android/tools/r8/lightir/LirSizeEstimation.java b/src/main/java/com/android/tools/r8/lightir/LirSizeEstimation.java
index 7b78efa..3c0961e 100644
--- a/src/main/java/com/android/tools/r8/lightir/LirSizeEstimation.java
+++ b/src/main/java/com/android/tools/r8/lightir/LirSizeEstimation.java
@@ -352,6 +352,9 @@
       case CONSTCLASS_IGNORE_COMPAT:
         return DexConstClass.SIZE;
 
+      case RESOURCENUMBER:
+        return DexConst4.SIZE;
+
       default:
         throw new Unreachable("Unexpected LIR opcode: " + opcode);
     }
diff --git a/src/main/java/com/android/tools/r8/lightir/LirUseRegistryCallback.java b/src/main/java/com/android/tools/r8/lightir/LirUseRegistryCallback.java
index 83a34f1..d97b0a9 100644
--- a/src/main/java/com/android/tools/r8/lightir/LirUseRegistryCallback.java
+++ b/src/main/java/com/android/tools/r8/lightir/LirUseRegistryCallback.java
@@ -227,6 +227,11 @@
   }
 
   @Override
+  public void onConstResourceNumber(int value) {
+    registry.registerConstResourceNumber(value);
+  }
+
+  @Override
   public void onRecordFieldValues(DexField[] fields, List<EV> values) {
     registry.registerRecordFieldValues(fields);
   }
diff --git a/src/main/java/com/android/tools/r8/shaking/DefaultEnqueuerUseRegistry.java b/src/main/java/com/android/tools/r8/shaking/DefaultEnqueuerUseRegistry.java
index 7e988c4..736f285 100644
--- a/src/main/java/com/android/tools/r8/shaking/DefaultEnqueuerUseRegistry.java
+++ b/src/main/java/com/android/tools/r8/shaking/DefaultEnqueuerUseRegistry.java
@@ -57,6 +57,12 @@
   }
 
   @Override
+  public void registerConstResourceNumber(int value) {
+    super.registerConstResourceNumber(value);
+    enqueuer.traceResourceValue(value);
+  }
+
+  @Override
   public void registerInvokeVirtual(DexMethod invokedMethod) {
     super.registerInvokeVirtual(invokedMethod);
     enqueuer.traceInvokeVirtual(invokedMethod, getContext());
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 5279f1d..34cb2ec 100644
--- a/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
+++ b/src/main/java/com/android/tools/r8/shaking/Enqueuer.java
@@ -17,7 +17,11 @@
 import static com.android.tools.r8.utils.MapUtils.ignoreKey;
 import static java.util.Collections.emptySet;
 
+import com.android.build.shrinker.r8integration.R8ResourceShrinkerState;
+import com.android.tools.r8.AndroidResourceInput;
+import com.android.tools.r8.AndroidResourceInput.Kind;
 import com.android.tools.r8.Diagnostic;
+import com.android.tools.r8.ResourceException;
 import com.android.tools.r8.cf.code.CfInstruction;
 import com.android.tools.r8.cf.code.CfInvoke;
 import com.android.tools.r8.contexts.CompilationContext.MethodProcessingContext;
@@ -479,6 +483,8 @@
 
   private final ProfileCollectionAdditions profileCollectionAdditions;
 
+  private final R8ResourceShrinkerState r8ResourceShrinkerState;
+
   Enqueuer(
       AppView<? extends AppInfoWithClassHierarchy> appView,
       ProfileCollectionAdditions profileCollectionAdditions,
@@ -537,6 +543,27 @@
 
     objectAllocationInfoCollection =
         ObjectAllocationInfoCollectionImpl.builder(mode.isInitialTreeShaking(), graphReporter);
+    r8ResourceShrinkerState = setupResourceShrinkerState(appView);
+  }
+
+  private R8ResourceShrinkerState setupResourceShrinkerState(
+      AppView<? extends AppInfoWithClassHierarchy> appView) {
+    R8ResourceShrinkerState r8ResourceShrinkerState = new R8ResourceShrinkerState();
+    if (options.resourceShrinkerConfiguration.isOptimizedShrinking()
+        && options.androidResourceProvider != null) {
+      try {
+        for (AndroidResourceInput androidResource :
+            options.androidResourceProvider.getAndroidResources()) {
+          if (androidResource.getKind() == Kind.RESOURCE_TABLE) {
+            r8ResourceShrinkerState.setResourceTableInput(androidResource.getByteStream());
+            break;
+          }
+        }
+      } catch (ResourceException e) {
+        throw appView.reporter().fatalError("Failed initializing resource table");
+      }
+    }
+    return r8ResourceShrinkerState;
   }
 
   private AppInfoWithClassHierarchy appInfo() {
@@ -1107,6 +1134,10 @@
     }
   }
 
+  public void traceResourceValue(int value) {
+    r8ResourceShrinkerState.trace(value);
+  }
+
   public void traceReflectiveFieldWrite(ProgramField field, ProgramMethod context) {
     deferredTracing.notifyReflectiveFieldAccess(field, context);
     if (registerReflectiveFieldWrite(field, context)) {
@@ -3740,6 +3771,7 @@
     EnqueuerResult result = createEnqueuerResult(appInfo, timing);
     profileCollectionAdditions.commit(appView);
     timing.end();
+    appView.setResourceShrinkerState(r8ResourceShrinkerState);
     return result;
   }
 
diff --git a/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java b/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java
index df0ac25..7d8e995 100644
--- a/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java
+++ b/src/main/java/com/android/tools/r8/utils/DescriptorUtils.java
@@ -17,6 +17,7 @@
 import com.google.common.collect.ImmutableMap;
 import java.io.File;
 import java.nio.file.Path;
+import java.util.List;
 import java.util.Map;
 import java.util.function.Function;
 
@@ -805,6 +806,22 @@
     return new ModuleAndDescriptor(module, 'L' + descriptor + ';');
   }
 
+  public static boolean isRClassDescriptor(String descriptor) {
+    String simpleClassName = DescriptorUtils.getSimpleClassNameFromDescriptor(descriptor);
+    List<String> split = StringUtils.split(simpleClassName, '$');
+
+    if (split.size() < 2) {
+      return false;
+    }
+    String type = split.get(split.size() - 1);
+    String rClass = split.get(split.size() - 2);
+    // We match on R if:
+    // - The name of the Class is R$type - we allow R to be an inner class.
+    //   - The inner type should be with lower case
+    boolean isRClass = Character.isLowerCase(type.charAt(0)) && rClass.equals("R");
+    return isRClass;
+  }
+
   public static String getPathFromDescriptor(String descriptor) {
     // We are quite loose on names here to support testing illegal names, too.
     assert descriptor.startsWith("L");
diff --git a/src/main/java/com/android/tools/r8/utils/InternalOptions.java b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
index afc71ff..29f4710 100644
--- a/src/main/java/com/android/tools/r8/utils/InternalOptions.java
+++ b/src/main/java/com/android/tools/r8/utils/InternalOptions.java
@@ -229,6 +229,10 @@
     return proguardConfiguration != null;
   }
 
+  public boolean isOptimizedResourceShrinking() {
+    return androidResourceProvider != null && resourceShrinkerConfiguration.isOptimizedShrinking();
+  }
+
   public ProguardConfiguration getProguardConfiguration() {
     return proguardConfiguration;
   }
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 088547e..5e0a8ae 100644
--- a/src/resourceshrinker/java/com/android/build/shrinker/r8integration/R8ResourceShrinkerState.java
+++ b/src/resourceshrinker/java/com/android/build/shrinker/r8integration/R8ResourceShrinkerState.java
@@ -51,8 +51,8 @@
       super(debugReporter, supportMultipackages);
     }
 
-    public Map<Integer, String> getStringResourcesWithSingleValue() {
-      return stringResourcesWithSingleValue;
+    public String getSingleStringValueOrNull(int id) {
+      return stringResourcesWithSingleValue.get(id);
     }
 
     // Similar to instantiation in ProtoResourceTableGatherer, but using an inputstream.
diff --git a/src/test/java/com/android/tools/r8/R8TestBuilder.java b/src/test/java/com/android/tools/r8/R8TestBuilder.java
index f803d78..c9b9401 100644
--- a/src/test/java/com/android/tools/r8/R8TestBuilder.java
+++ b/src/test/java/com/android/tools/r8/R8TestBuilder.java
@@ -946,4 +946,12 @@
     return addProgramClassFileData(testResource.getRClass().getClassFileData());
   }
 
+  public T setAndroidResourcesFromPath(Path input, Path output) {
+    resourceShrinkerOutput = output;
+    getBuilder()
+        .setAndroidResourceProvider(
+            new ArchiveProtoAndroidResourceProvider(input, new PathOrigin(input)));
+    getBuilder().setAndroidResourceConsumer(new ArchiveProtoAndroidResourceConsumer(output, input));
+    return self();
+  }
 }
diff --git a/src/test/java/com/android/tools/r8/androidresources/ResourceIDCannonicalizationTest.java b/src/test/java/com/android/tools/r8/androidresources/ResourceIDCannonicalizationTest.java
new file mode 100644
index 0000000..60ce942
--- /dev/null
+++ b/src/test/java/com/android/tools/r8/androidresources/ResourceIDCannonicalizationTest.java
@@ -0,0 +1,87 @@
+// Copyright (c) 2024, 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 org.junit.Assert.assertEquals;
+
+import com.android.tools.r8.TestBase;
+import com.android.tools.r8.TestParameters;
+import com.android.tools.r8.TestParametersCollection;
+import com.android.tools.r8.androidresources.AndroidResourceTestingUtils.AndroidTestResource;
+import com.android.tools.r8.androidresources.AndroidResourceTestingUtils.AndroidTestResourceBuilder;
+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 ResourceIDCannonicalizationTest extends TestBase {
+  public static final int EXPECTED_RESOURCE_NUMBER = 0x7f010001;
+
+  @Parameter(0)
+  public TestParameters parameters;
+
+  @Parameters(name = "{0}")
+  public static TestParametersCollection parameters() {
+    return getTestParameters().withDefaultDexRuntime().withAllApiLevels().build();
+  }
+
+  public static AndroidTestResource getTestResources(TemporaryFolder temp) throws Exception {
+    return new AndroidTestResourceBuilder()
+        .withSimpleManifestAndAppNameString()
+        .addRClassInitializeWithDefaultValues(R.string.class)
+        .build(temp);
+  }
+
+  @Test
+  public void testR8() throws Exception {
+    testForR8(parameters.getBackend())
+        .setMinApi(parameters)
+        .addProgramClasses(FooBar.class)
+        .addAndroidResources(getTestResources(temp))
+        .addKeepMainRule(FooBar.class)
+        .enableOptimizedShrinking()
+        .compile()
+        .inspect(
+            codeInspector -> {
+              // We should canonicalize the resource numbers separately from the normal const
+              // numbers.
+              // This implies that the output have two distinct const numbers with the same value.
+              long constNumbers =
+                  codeInspector
+                      .clazz(FooBar.class)
+                      .mainMethod()
+                      .streamInstructions()
+                      .filter(i -> i.isConstNumber(EXPECTED_RESOURCE_NUMBER))
+                      .count();
+              assertEquals(2, constNumbers);
+            })
+        .inspectShrunkenResources(
+            resourceTableInspector -> {
+              resourceTableInspector.assertContainsResourceWithName("string", "foo");
+            });
+  }
+
+  public static class FooBar {
+
+    public static void main(String[] args) {
+      if (System.currentTimeMillis() == 0) {
+        System.out.println(EXPECTED_RESOURCE_NUMBER);
+        System.out.println(R.string.foo);
+        System.out.println(EXPECTED_RESOURCE_NUMBER);
+        System.out.println(R.string.foo);
+        System.out.println(EXPECTED_RESOURCE_NUMBER);
+        System.out.println(EXPECTED_RESOURCE_NUMBER);
+      }
+    }
+  }
+
+  public static class R {
+    public static class string {
+      public static int foo;
+    }
+  }
+}
diff --git a/src/test/java/com/android/tools/r8/androidresources/optimizedshrinking/TestOptimizedShrinking.java b/src/test/java/com/android/tools/r8/androidresources/optimizedshrinking/TestOptimizedShrinking.java
index 662d00f..74c7102 100644
--- a/src/test/java/com/android/tools/r8/androidresources/optimizedshrinking/TestOptimizedShrinking.java
+++ b/src/test/java/com/android/tools/r8/androidresources/optimizedshrinking/TestOptimizedShrinking.java
@@ -71,7 +71,7 @@
               resourceTableInspector.assertContainsResourceWithName("drawable", "foobar");
               // In debug mode legacy shrinker will not attribute our $R inner class as an R class
               // (this is only used for testing, _real_ R classes are not inner classes.
-              if (!debug || optimized) {
+              if (!debug) {
                 resourceTableInspector.assertDoesNotContainResourceWithName(
                     "string", "unused_string");
                 resourceTableInspector.assertDoesNotContainResourceWithName(
@@ -107,7 +107,7 @@
 
                 // In optimized mode we track these correctly, so we should not unconditionally keep
                 // all attributes.
-                if (optimized) {
+                if (optimized && !debug) {
                   resourceTableInspector.assertDoesNotContainResourceWithName(
                       "attr", "attr_unused_styleable" + i);
                 } else {