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.arscfile inside the package.
What does public.xml do?
During resource packaging, aapt uses
public.xmlto 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
Rclass.
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
Rclass 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.xxxreferences.
Relationship between R class and public.xml
Conceptually they are unrelated, but code uses
R.idto look up resources, which ties them together. The
public.xmlfile corresponds to the values stored in
resources.arsc, and the
Rclass 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
channelPublicare stored in a
PublicXmlBeanobject.
<code>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);
}
}
}
</code>Merging channel public.xml into base public.xml
If a
typeexists in the channel but not in the base, add the entries, correcting the
PackageID+TypeIDto match the base and assigning a new entry value that does not conflict.
If the
typealready exists, keep the base values unchanged.
If the
typeis completely new, allocate a new
TypeID(the next highest value) and assign appropriate IDs.
<code><public type="attr" name="iconSrc" id="0x7f0200a8" />
</code>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
PublicAndRHelperand
RValueHelper.
<code>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());
}
}
}
}
}
</code>For each R class field, the corresponding value is fetched from
PublicXmlBeanand replaced in the file. When handling
R$attr, any related
R$styleableentries are also updated using a cached mapping to avoid replacement conflicts.
<code>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; }
}
}
}
}
</code>Images
37 Mobile Game Tech Team
37 Mobile Game Tech Team
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.