Why public.xml Matters: Mastering Android Resource IDs and R Class Integration
This article explains how public.xml is generated during APK decompilation, its role in fixing resource IDs, the structure of those IDs, the relationship with the R class, and detailed steps and code for correctly merging resources and correcting R class values during Android package integration.
Background
This article is intended for Android developers in the game publishing industry, but the concepts apply to other fields as well.
Where does public.xml come from?
The file is generated by apktool when decompiling an APK, based on the resources.arsc file inside the package.
What does public.xml do?
During resource packaging, aapt uses public.xml to fix resource IDs; if an ID already exists in public.xml, the same ID is reused. Developers retrieve resources via Resources.getXXX(resId), where the ID is defined in the automatically generated R class.
Resource IDs are constants; once the APK is built, the ID values are fixed. When apktool repackages an APK, it recompiles resources into a new resources.arsc, potentially reassigning IDs (except those fixed in public.xml), which can cause mismatches if an ID is reassigned to a different resource.
ID format in public.xml
Each ID is a 32‑bit (four‑byte) value:
First byte: PackageID (system resources use 01, app resources use 7f)
Second byte: TypeID (e.g., attr = 01, string = 02, but not fixed)
Last two bytes: Entry value
The R class
In library modules the generated R class members are non‑final variables, while in the app module they are final constants. Constants are inlined by the Java compiler, whereas variables remain as R.id.xxx references.
Relationship between R class and public.xml
Conceptually they are unrelated, but code uses R.id to look up resources, which ties them together. The public.xml file corresponds to the values stored in resources.arsc, and the R class provides the indices to access those resources.
Handling R class and public.xml during package merging
When merging packages, the R class from the channel is overwritten, but its values differ from the base package. The channel’s public.xml (referred to as channelPublic) must be merged into the base public.xml ( matrixPublic) without breaking existing IDs.
Steps:
Decompile the base APK.
Merge channel resources.
Merge resources from the new SDK.
Parsing channel public.xml
During decompilation, the values from channelPublic are stored in a PublicXmlBean object.
private void init() {
List<Element> elements = mDocument.getRootElement().elements();
for (Element element : elements) {
String type = element.attribute(TYPE).getStringValue();
String name = element.attribute(NAME).getStringValue();
String id = element.attribute(ID).getStringValue();
Map<String, String> typeMap = mTypeMap.get(type);
if (typeMap == null) {
typeMap = new HashMap<>();
typeMap.put(name, id);
mTypeMap.put(type, typeMap);
} else {
typeMap.put(name, id);
}
}
}Merging channel public.xml into base public.xml
If a type exists in the channel but not in the base, add the entries, correcting the PackageID+TypeID to match the base and assigning a new entry value that does not conflict.
If the type already exists, keep the base values unchanged.
If the type is completely new, allocate a new TypeID (the next highest value) and assign appropriate IDs.
<public type="attr" name="iconSrc" id="0x7f0200a8" />Merging new SDK resources and correcting the R class
After the SDK resources are merged, the R class files are scanned (excluding R$styleable) to adjust their constant values based on the updated public.xml. The scanning logic is implemented in PublicAndRHelper and RValueHelper.
private void scannerRClass(String path) {
File smaliFilePath = new File(path);
for (File file : smaliFilePath.listFiles()) {
if (file.isDirectory()) {
scannerRClass(file.getAbsolutePath());
} else if (file.isFile()) {
if (file.getName().equals("R.smali") || file.getName().startsWith("R$")) {
// skip styleable files
if (!file.getName().endsWith("R$styleable.smali")) {
mRClassFileList.add(file.getAbsolutePath());
}
}
}
}
}For each R class field, the corresponding value is fetched from PublicXmlBean and replaced in the file. When handling R$attr, any related R$styleable entries are also updated using a cached mapping to avoid replacement conflicts.
static void handle(String RFilePath, PublicXmlBean publicXmlBean) {
File RFile = new File(RFilePath);
String RStyleFilePath = "";
Map<String, String> cacheMap = null;
if (RFile.getName().endsWith("R$attr.smali")) {
RStyleFilePath = RFilePath.replace("R$attr", "R$styleable");
File RStyleAbleFile = new File(RStyleFilePath);
if (RStyleAbleFile.exists()) {
cacheMap = new HashMap<>();
}
}
String rFileContent = FileUtil.read(RFilePath);
ArrayList<String> lines = FileUtil.readAllLines(RFilePath, ".field public static final");
String regex = ".field public static final (.*):(.*) = (.*)";
for (String line : lines) {
Pattern pattern = Pattern.compile(regex);
Matcher matcher = pattern.matcher(line);
if (matcher.find()) {
String type = RFile.getName().replace("R$", "").replace(".smali", "");
String name = matcher.group(1);
String resetValue = publicXmlBean.getValue(type, name);
if (StringUtils.isEmpty(resetValue)) {
resetValue = publicXmlBean.addValue(type, matcher.group(1));
}
rFileContent = rFileContent.replace(line, ".field public static final " + name + ":" + matcher.group(2) + " = " + resetValue);
if (cacheMap != null) {
cacheMap.put(matcher.group(3), resetValue);
}
}
}
FileUtil.write(RFilePath, rFileContent);
if (cacheMap != null) {
List<String> styleAbleLines = FileUtil.readAllLines(RStyleFilePath);
BufferedWriter bw = null;
try {
bw = new BufferedWriter(new FileWriter(RStyleFilePath));
for (String styleAbleLine : styleAbleLines) {
for (String key : cacheMap.keySet()) {
if (styleAbleLine.contains(key)) {
styleAbleLine = styleAbleLine.replace(key, cacheMap.get(key));
}
}
bw.write(styleAbleLine);
bw.newLine();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (bw != null) {
try { bw.close(); } catch (IOException e) { bw = null; }
}
}
}
}Images
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
