La tabla de recursos: resources.arsc
Antes conviene leer «XML binario (AXML)», «Contenedor ZIP»
Todos los enteros de este documento son little-endian, con dos excepciones que se señalan
en su fila: los campos language y country de ResTable_config, cuya disposición es
big-endian con independencia de la arquitectura.
Este documento no repite el sustrato de chunks ni el formato del string pool: viven en
«XML binario (AXML)», secciones «2» y «3», y son literalmente las mismas
estructuras binarias.
1. Qué problema resuelve
Una aplicación tiene un botón que dice «Guardar». En español dice «Guardar», en alemán «Speichern», en japonés «保存». Su icono existe en cinco densidades de pantalla. Su margen es uno en teléfono y otro en tableta. Y el código, que está compilado, tiene que referirse a todo eso con un solo número que no cambie nunca.
Ese número es el resource ID, un uint32_t con la forma 0xPPTTEEEE. R.string.save no es
una cadena: es la constante 0x7f120075 que el compilador incrustó en el DEX. Y
resources.arsc es la tabla que, en tiempo de ejecución y para la configuración concreta del
dispositivo, convierte ese número en el valor que toca.
resources.arsc
│
R.string.app_name ──────────►│ 0x7f120076
(0x7f120076) │ ├── () → "F-Droid Basic"
│ ├── (ca) → "F-Droid Bàsic"
│ ├── (da) → "F-Droid Basis"
│ ├── (fa) → "افدروید پایه"
│ ├── (ga) → "F-Droid Bunúsach"
│ └── (ta) → "எஃப்-டிராய்டு அடிப்படை"
│
configuración del ─────────┘
dispositivo
El ejemplo es real: es string/app_name_basic de org.fdroid.fdroid_1023052.apk, tal y como lo
imprime aapt2 dump resources. En ese fichero el tipo string tiene 100 variantes de
configuración, una por cada uno de los 85 idiomas traducidos más las combinaciones de versión
de API.
La alternativa sería resolver por nombre en tiempo de ejecución con un mapa de cadenas por idioma: más lento, más grande, y sin comprobación en compilación de que el recurso existe. La tabla paga ese precio por adelantado a cambio de un índice binario, mapeable, con búsqueda por número y filtrado por configuración.
Y es el camino crítico de cualquier implementación fuera de Java. Escritores en Rust
existen dos —arsc 0.1.5 expone write()/write_to(), y resand 0.3.0, ResTable::write_all()—,
pero ninguno mantenido ni con adopción: el primero lleva abandonado desde abril de 2022 y el
segundo es un proyecto de un solo autor. Sin un escritor fiable no hay editor, hay visor, y ya
existen cinco visores. Ese es el motivo de que este sea el documento más largo de esta
documentación.
2. El árbol de chunks
Todo el fichero es un solo chunk RES_TABLE_TYPE con un árbol dentro:
RES_TABLE_TYPE (0x0002) ── el fichero entero
│
├── RES_STRING_POOL_TYPE (0x0001) pool global de valores
├── RES_TABLE_FLAG_LIST (0x0208) opcional, ver 12.5
│
├── RES_TABLE_PACKAGE_TYPE (0x0200) uno por paquete
│ ├── RES_STRING_POOL_TYPE typeStrings: "anim", "drawable", "string"…
│ ├── RES_STRING_POOL_TYPE keyStrings: "app_name", "abc_fade_in"…
│ │
│ ├── RES_TABLE_TYPE_SPEC_TYPE (0x0202) tipo 1: qué ejes hacen variar cada entrada
│ ├── RES_TABLE_FLAGGED (0x0207) opcional, ver 12.5
│ │ └── RES_TABLE_TYPE_TYPE (0x0201) valores tras un flag de lectura/escritura
│ ├── RES_TABLE_TYPE_TYPE (0x0201) tipo 1, configuración por defecto
│ ├── RES_TABLE_TYPE_TYPE (0x0201) tipo 1, configuración "es"
│ ├── RES_TABLE_TYPE_TYPE (0x0201) tipo 1, configuración "xhdpi"
│ ├── RES_TABLE_TYPE_SPEC_TYPE (0x0202) tipo 2
│ ├── RES_TABLE_TYPE_TYPE (0x0201) tipo 2, …
│ │ …
│ ├── RES_TABLE_LIBRARY_TYPE (0x0203) opcional
│ ├── RES_TABLE_OVERLAYABLE_TYPE (0x0204) opcional
│ │ └── RES_TABLE_OVERLAYABLE_POLICY_TYPE (0x0205)
│ └── RES_TABLE_STAGED_ALIAS_TYPE (0x0206) opcional
│
└── RES_TABLE_PACKAGE_TYPE (0x0200) siguiente paquete, si lo hay
Dos reglas de orden que AOSP sí impone y conviene tener presentes:
- Un
RES_TABLE_TYPE_TYPEtiene que venir después delRES_TABLE_TYPE_SPEC_TYPEde su mismoid.LoadedArsc.cppaborta conRES_TABLE_TYPE_TYPE with ID %02x found without preceding RES_TABLE_TYPE_SPEC_TYPE. - Si aparecen dos
RES_TABLE_TYPE_SPEC_TYPEcon el mismoid, el segundo se ignora con un aviso.
Fuera de eso, el orden es el que quiera el escritor.
3. RES_TABLE_TYPE / ResTable_header
| Campo | Offset | Tamaño | Tipo | Significado |
|---|---|---|---|---|
header |
0x00 |
8 | ResChunk_header |
type = RES_TABLE_TYPE = 0x0002; size es el fichero entero |
packageCount |
0x08 |
4 | uint32_t |
Número de chunks ResTable_package que siguen |
headerSize vale 12 en los 563 APK con resources.arsc del «muestrario». Un APK normal tiene
packageCount = 1 —así 557 de ellos—; el máximo observado es 6, en
org.totschnig.myexpenses_858.apk, por sus módulos de funcionalidad («sección 12.1 · RES_TABLE_LIBRARY_TYPE (0x0203)»).
Y hay un caso límite que conviene tener presente antes de escribir la primera línea de código:
packageCount = 0 es válido. Cinco APK del muestrario, todos splits de módulo de funcionalidad
del zip SAI de Netflix —split_voip.config.xhdpi.apk y cuatro más—, traen un resources.arsc
de 40 bytes exactos: los 12 de ResTable_header más los 28 de un RES_STRING_POOL_TYPE
vacío, y ningún paquete. Es la tabla de recursos mínima válida, y un lector que dé por supuesto
que hay al menos un paquete falla con ella.
4. El string pool global
Justo detrás de la cabecera viene un RES_STRING_POOL_TYPE. Su formato es exactamente el
descrito en «XML binario, sección 3»,
incluidas las dos codificaciones y el doble campo de longitud en UTF-8, que aquí importa más
todavía porque este pool es, con diferencia, el mayor del APK: 44.157 cadenas y 2,1 MiB en
org.fdroid.fdroid_1023052.apk.
Contiene los valores de tipo cadena de toda la tabla: los textos traducidos y las rutas de
fichero de los recursos (res/layout/main.xml, res/vp1.xml). No contiene los nombres de
los tipos ni los de las entradas: esos viven en dos pools propios de cada paquete («sección 5 · ResTable_package»).
Un Res_value de tipo TYPE_STRING dentro de una entrada indexa este pool, no el del
paquete. Es un error frecuente al implementar.
Medido sobre los 58 APK standalone del muestrario, hay 184 string pools en total —tres por APK más alguno extra por paquete— de los cuales 121 son UTF-8 y 63 UTF-16, y 33 llevan estilos.
5. ResTable_package
| Campo | Offset | Tamaño | Tipo | Significado |
|---|---|---|---|---|
header |
0x00 |
8 | ResChunk_header |
type = RES_TABLE_PACKAGE_TYPE = 0x0200 |
id |
0x08 |
4 | uint32_t |
El PP del resource ID. 0 significa paquete dinámico: el runtime le asigna un ID al cargarlo |
name |
0x0c |
256 | uint16_t[128] |
Nombre del paquete en UTF-16, terminado en \0 y rellenado con ceros |
typeStrings |
0x10c |
4 | uint32_t |
Offset desde el inicio de este chunk al pool de nombres de tipo |
lastPublicType |
0x110 |
4 | uint32_t |
Último índice de typeStrings de uso público. 0 en todo el muestrario |
keyStrings |
0x114 |
4 | uint32_t |
Offset desde el inicio de este chunk al pool de nombres de entrada |
lastPublicKey |
0x118 |
4 | uint32_t |
Último índice de keyStrings de uso público. 0 en todo el muestrario |
typeIdOffset |
0x11c |
4 | uint32_t |
Desplazamiento a sumar a los id de tipo. 0 en todo el muestrario |
headerSize vale 288 (0x120), que es exactamente sizeof(ResTable_package). También es válido
284, la cabecera sin typeIdOffset de los productores anteriores a ese campo: LoadedArsc.cpp
la acepta y solo lee typeIdOffset si headerSize ≥ 288. Y en todo el
muestrario typeStrings vale también 0x120: el pool de tipos empieza inmediatamente después de
la cabecera.
Los dos pools propios de cada paquete son la razón de que resolver un recurso por nombre exija tres pools distintos:
| Pool | Contiene | Indexado por |
|---|---|---|
| Global («sección 4 · El string pool global») | Valores de tipo cadena y rutas de fichero | Res_value.data cuando dataType = TYPE_STRING |
typeStrings |
"anim", "attr", "drawable", "string", "style"… |
ResTable_type.id - 1 y ResTable_typeSpec.id - 1 |
keyStrings |
"app_name", "abc_fade_in", "actionBarDivider"… |
ResTable_entry.key |
Los índices de tipo son base 1 y los pools base 0: el tipo id = 0x01 es
typeStrings[0]. id = 0 es inválido y LoadedArsc.cpp lo rechaza.
De com.termux_1002.apk, el pool de tipos completo:
[1] anim [2] animator [3] attr [4] bool [5] color [6] dimen [7] drawable
[8] id [9] integer [10] interpolator [11] layout [12] menu [13] mipmap
[14] plurals [15] raw [16] string [17] style [18] ?18 [19] xml
El ?18 no es un error del volcador: es la cadena literal "?18" que aapt2 escribe en el pool
para rellenar el hueco del ID de tipo que ocupa styleable (o macro), que no son tipos reales y
no llegan al binario. Un lector tiene que tolerarlo.
6. ResTable_typeSpec
| Campo | Offset | Tamaño | Tipo | Significado |
|---|---|---|---|---|
header |
0x00 |
8 | ResChunk_header |
type = RES_TABLE_TYPE_SPEC_TYPE = 0x0202 |
id |
0x08 |
1 | uint8_t |
Identificador de tipo, base 1. 0 es inválido |
res0 |
0x09 |
1 | uint8_t |
Debe ser 0 |
typesCount |
0x0a |
2 | uint16_t |
Antes reservado; si es > 0, número de ResTable_type de este tipo |
entryCount |
0x0c |
4 | uint32_t |
Número de máscaras uint32_t que siguen |
headerSize vale 16. Detrás vienen entryCount valores uint32_t, uno por entrada del tipo:
una máscara de bits de los ejes de configuración que hacen variar esa entrada, con las
constantes ResTable_config::CONFIG_* («sección 8.4 · Las banderas CONFIG_*»), más dos flags propios:
| Flag | Valor | Significado |
|---|---|---|
SPEC_PUBLIC |
0x40000000 |
La entrada es pública y otras aplicaciones pueden referenciarla |
SPEC_STAGED_API |
0x20000000 |
El resource ID de esta entrada puede cambiar en una compilación futura. Implica SPEC_PUBLIC |
Para qué sirve realmente: es el índice que dice qué se puede tirar. Si la máscara de la
entrada 5 del tipo string no tiene el bit CONFIG_LOCALE, esa entrada vale igual en todos los
idiomas y un split de idioma no necesita llevarla. bundletool no la usa para decidir qué
entra en config.es.apk: divide el resources.pb del bundle por los ConfigValue de cada
entrada, y las máscaras del typeSpec las calcula aapt2 después, al convertir cada split a
binario. Sí es la información que un merge necesita
para saber si dos entradas homónimas de dos splits distintos son la misma o compiten.
entryCount del typeSpec es el número total de entradas del tipo, que puede ser mucho
mayor que el entryCount de un ResTable_type concreto cuando este es disperso («sección 7.3 · FLAG_SPARSE y ResTable_sparseTypeEntry»).
En config.xhdpi.apk de Google Keep, el typeSpec del tipo 0x08 declara
entryCount = 810 y el ResTable_type que le sigue solo trae 42.
typesCount es el campo menos fiable de la estructura. La cabecera lo describe como «used to be
reserved»: los productores antiguos lo dejaban a cero y los modernos escriben el número de
ResTable_type del tipo. En el muestrario conviven los dos comportamientos dentro del mismo
universo: de los 1.067 typeSpec de los 58 APK standalone, 172 llevan 0 y 895 un valor
real; de los 1.618 de los APK internos, 121 llevan 0 y 1.497 un valor real. Un lector no debe
apoyarse en él —es información redundante con el recuento efectivo de chunks—, pero un escritor
que aspire al round-trip tiene que reemitir el valor que leyó, incluido el cero.
7. ResTable_type
Es el chunk que de verdad contiene los datos: una configuración concreta y los valores de todas las entradas del tipo para esa configuración.
| Campo | Offset | Tamaño | Tipo | Significado |
|---|---|---|---|---|
header |
0x00 |
8 | ResChunk_header |
type = RES_TABLE_TYPE_TYPE = 0x0201 |
id |
0x08 |
1 | uint8_t |
Identificador de tipo, base 1 |
flags |
0x09 |
1 | uint8_t |
FLAG_SPARSE = 0x01, FLAG_OFFSET16 = 0x02 |
reserved |
0x0a |
2 | uint16_t |
Debe ser 0 |
entryCount |
0x0c |
4 | uint32_t |
Número de entradas del array de offsets |
entriesStart |
0x10 |
4 | uint32_t |
Offset desde el inicio del chunk donde empiezan los ResTable_entry |
config |
0x14 |
var | ResTable_config |
La configuración. Debe ser siempre el último campo |
Ese «debe ser siempre el último campo» no es una recomendación: es un static_assert de
ResourceTypes.h, con el comentario «This poses a problem for extending ResTable_type in the
future, as ResTable_config is variable». headerSize vale
20 + config.size, que en la práctica es 20 + 64 = 84.
La disposición completa:
+0x00 ┌──────────────────────────────────────────────────────────┐
│ ResTable_type (headerSize = 20 + config.size = 84) │
+0x54 ├──────────────────────────────────────────────────────────┤
│ array de offsets: │
│ uint32_t[entryCount] si no hay flags │
│ uint16_t[entryCount] si FLAG_OFFSET16 │
│ ResTable_sparseTypeEntry[entryCount] si FLAG_SPARSE │
├──────────────────────────────────────────────────────────┤
│ relleno hasta múltiplo de 4 │
entriesStart ─────────────────────────────────────────────────────┤
│ ResTable_entry + Res_value o ResTable_map[] │
│ ResTable_entry + … │
│ … │
└──────────────────────────────────────────────────────────┘
entriesStart es relativo al inicio del chunk, y los offsets del array son relativos a
entriesStart. Confundir las dos bases es el segundo error más frecuente del formato, tras
el doble campo de longitud del string pool.
7.1 El array denso
Sin flags, el array son entryCount valores uint32_t. El valor NO_ENTRY = 0xFFFFFFFF
significa que esa entrada no está definida para esta configuración. De
com.termux_1002.apk, tipo 0x04 (bool), configuración por defecto:
array de offsets (uint32_t):
[ 0] 0x00000000
[ 1] 0x00000010
[ 2] 0x00000020
[ 3] 0x00000030
entradas:
id=0x7f040000 size=8 flags=0x0000 key=1022('abc_action_bar_embed_tabs')
value: size=8 dataType=0x12 data=0xffffffff
id=0x7f040001 size=8 flags=0x0000 key=1023('abc_config_actionMenuItemAllCaps')
value: size=8 dataType=0x12 data=0xffffffff
Y el segundo ResTable_type del mismo tipo, para sw360dp, empieza con NO_ENTRY: no redefine
la entrada 0.
7.2 FLAG_OFFSET16
Con FLAG_OFFSET16 = 0x02 el array es de uint16_t y el offset real es valor * 4; el valor
0xffff significa NO_ENTRY. La conversión está en una función de una línea de la cabecera:
static inline uint32_t offset_from16(uint16_t off16) {
return dtohs(off16) == 0xffffu ? ResTable_type::NO_ENTRY : dtohs(off16) * 4u;
}
Existe en ResourceTypes.h de la etiqueta android-17.0.0_r1 y aapt2 solo lo emite cuando ya está usando
entradas compactas (FLAG_COMPACT, «sección 10.2 · La entrada compacta: FLAG_COMPACT») y los offsets caben en 16 bits.
No se ha observado ni una sola vez en el muestrario: 0 de 22.247 chunks ResTable_type
inspeccionados, sumando los 58 APK standalone y los 505 APK internos con resources.arsc.
7.3 FLAG_SPARSE y ResTable_sparseTypeEntry
Con FLAG_SPARSE = 0x01, entryCount deja de ser el número total de entradas del tipo y pasa
a ser el número de entradas realmente definidas. El array es de
ResTable_sparseTypeEntry, un uint32_t con dos mitades:
| Campo | Offset | Tamaño | Tipo | Significado |
|---|---|---|---|---|
idx |
0x00 |
2 | uint16_t |
Índice de la entrada (EEEE del resource ID) |
offset |
0x02 |
2 | uint16_t |
Offset desde entriesStart, dividido por 4 |
Las entradas están ordenadas por idx y AOSP hace std::lower_bound sobre ellas. Un
escritor que no las ordene produce un fichero que se lee mal sin dar error.
Es la variante que ahorra espacio cuando un split de configuración define pocas entradas de
un tipo con muchas. Del split config.xhdpi.apk de Google Keep:
+0x000bf8 RES_TABLE_TYPE_SPEC_TYPE headerSize=16 size=3256 id=0x08 typesCount=2 entryCount=810
+0x0018b0 RES_TABLE_TYPE_TYPE headerSize=84 size=924 id=0x08 flags=0x01[FLAG_SPARSE]
entryCount=42 entriesStart=0xfc config.size=64
array de offsets (ResTable_sparseTypeEntry):
[ 0] raw=0x00000161 → idx=353 offset=0x0
[ 1] raw=0x00040166 → idx=358 offset=0x10
[ 2] raw=0x00080167 → idx=359 offset=0x20
[ 3] raw=0x000c016c → idx=364 offset=0x30
0x00040166: la mitad baja 0x0166 = 358 es el índice, la alta 0x0004 × 4 = 0x10 es el
offset. El chunk ocupa 924 bytes en vez de los 810 × 4 = 3.240 bytes que costaría solo el array
denso.
Cuándo lo emite aapt2. De TableFlattener.cpp, cuatro condiciones simultáneas: que se
haya pedido con --enable-sparse-encoding (o, en aapt2 convert, con --force-sparse-encoding);
que minSdkVersion >= 32 (SDK_S_V2) o que no haya minSdk en el contexto y la codificación
sea forzada; que los offsets quepan en 16 bits, porque ResTable_sparseTypeEntry.offset es un
uint16_t que guarda el offset dividido entre 4 (values_buffer.size() / 4 < 65535, o sea
menos de unos 256 KiB de valores en el tipo); y que la proporción de entradas definidas esté por
debajo de kSparseEncodingThreshold = 60, o sea que menos del 60 % de las entradas del tipo
estén presentes. En el ejemplo, 42 de 810 es el 5,2 %.
El comentario de ResourceTypes.h dice que la codificación dispersa «solo está disponible en
plataformas ≥ O» —API 26— y recomienda marcar esos tipos con el calificador v26. aapt2
exige 32 por defecto. La diferencia importa a quien fusione splits: reemitir como disperso un
tipo que venía denso puede romper la instalación en dispositivos por debajo del minSdk
declarado.
Medido en el muestrario: 976 de 5.917 chunks ResTable_type de los APK internos llevan
FLAG_SPARSE, y ninguno de los 16.330 de los 58 APK standalone de F-Droid. Es una marca
casi perfecta de origen: los .apks y .apkm vienen de Play y llevan dispersos; los de F-Droid
no.
8. ResTable_config
La estructura que describe una configuración de dispositivo. Es la más grande, la que más ha cambiado con las versiones de Android y la que rompe los parsers ingenuos.
8.1 La tabla completa
Offsets relativos al inicio de la estructura, con sizeof(ResTable_config) = 64 en la etiqueta
android-17.0.0_r1:
| Campo | Offset | Tamaño | Tipo | Significado |
|---|---|---|---|---|
size |
0x00 |
4 | uint32_t |
Tamaño de esta estructura. Ver 8.2 |
mcc |
0x04 |
2 | uint16_t |
Mobile country code de la SIM. 0 = cualquiera |
mnc |
0x06 |
2 | uint16_t |
Mobile network code. 0 = cualquiera |
language |
0x08 |
2 | char[2] |
Código de idioma. Big-endian. Ver 8.3 |
country |
0x0a |
2 | char[2] |
Código de región. Big-endian. Ver 8.3 |
orientation |
0x0c |
1 | uint8_t |
ANY=0, PORT=1, LAND=2, SQUARE=3 |
touchscreen |
0x0d |
1 | uint8_t |
ANY=0, NOTOUCH=1, STYLUS=2, FINGER=3 |
density |
0x0e |
2 | uint16_t |
Densidad en dpi. Ver 8.5 |
keyboard |
0x10 |
1 | uint8_t |
ANY=0, NOKEYS=1, QWERTY=2, 12KEY=3 |
navigation |
0x11 |
1 | uint8_t |
ANY=0, NONAV=1, DPAD=2, TRACKBALL=3, WHEEL=4 |
inputFlags |
0x12 |
1 | uint8_t |
MASK_KEYSHIDDEN = 0x03, MASK_NAVHIDDEN = 0x0c (SHIFT_NAVHIDDEN = 2) |
grammaticalInflection |
0x13 |
1 | uint8_t |
Género gramatical. MASK = 0b11. Antes inputFieldPad0 |
screenWidth |
0x14 |
2 | uint16_t |
Anchura en píxeles. 0 = cualquiera |
screenHeight |
0x16 |
2 | uint16_t |
Altura en píxeles. 0 = cualquiera |
sdkVersion |
0x18 |
2 | uint16_t |
Nivel de API mínimo. 0 = cualquiera. Es el -v26 de los calificadores |
minorVersion |
0x1a |
2 | uint16_t |
Versión menor del SDK. Hasta Baklava (Android 16) siempre 0; después la usan las versiones menores |
screenLayout |
0x1c |
1 | uint8_t |
MASK_SCREENSIZE = 0x0f, MASK_SCREENLONG = 0x30, MASK_LAYOUTDIR = 0xc0 |
uiMode |
0x1d |
1 | uint8_t |
MASK_UI_MODE_TYPE = 0x0f, MASK_UI_MODE_NIGHT = 0x30 |
smallestScreenWidthDp |
0x1e |
2 | uint16_t |
El sw<N>dp del calificador |
screenWidthDp |
0x20 |
2 | uint16_t |
El w<N>dp |
screenHeightDp |
0x22 |
2 | uint16_t |
El h<N>dp |
localeScript |
0x24 |
4 | char[4] |
Código ISO-15924 del sistema de escritura (Latn, Hant) |
localeVariant |
0x28 |
8 | char[8] |
Subetiqueta de variante BCP-47, de 4 a 8 caracteres |
screenLayout2 |
0x30 |
1 | uint8_t |
MASK_SCREENROUND = 0x03 |
colorMode |
0x31 |
1 | uint8_t |
MASK_WIDE_COLOR_GAMUT = 0x03, MASK_HDR = 0x0c |
screenConfigPad2 |
0x32 |
2 | uint16_t |
Relleno reservado |
localeScriptWasComputed |
0x34 |
1 | bool |
Si es cierto, localeScript lo dedujo el sistema y no venía en el fuente |
localeNumberingSystem |
0x35 |
8 | char[8] |
Extensión BCP-47 de clave nu, de 3 a 8 caracteres |
endPadding |
0x3d |
3 | char[3] |
Relleno explícito hasta el múltiplo de 4 |
Varios de estos campos se agrupan en union con un nombre colectivo, que es el que usan las
funciones de comparación de la «sección 9 · Cómo elige Android qué entrada usar»: imsi (mcc + mnc), locale (language +
country), screenType (orientation + touchscreen + density), input, screenSize,
version, screenConfig, screenSizeDp y screenConfig2.
8.2 El problema del size variable
Este es EL detalle que rompe los parsers ingenuos. ResTable_config ha ido creciendo con
las versiones de Android, y un APK compilado hace ocho años trae una estructura más corta que
la que espera un lector escrito hoy.
La regla, tomada literalmente de ResTable_config::copyFromDeviceNoSwap, que en
android-17.0.0_r1 está definida en línea en ResourceTypes.h:
void copyFromDeviceNoSwap(const ResTable_config& o) {
const auto o_size = dtohl(o.size);
if (o_size >= sizeof(ResTable_config)) [[likely]] {
*this = o;
} else {
memcpy(this, &o, o_size);
memset(((uint8_t*)this) + o_size, 0, sizeof(ResTable_config) - o_size);
}
this->size = sizeof(*this);
}
Es decir:
Se leen exactamente
sizebytes y el resto de la estructura se rellena a cero. Sisizees mayor que la estructura conocida, se leen los campos conocidos y se ignora el exceso.
Nunca se lee sizeof de la propia struct. Y en el otro sentido —al escribir— hay que
reemitir size bytes, ni uno más, o el round-trip deja de ser byte-exacto.
ResourceTypes.h define la constante que hace falta para leer con seguridad la cabecera de un
ResTable_type antes de saber cuánto ocupa su config:
constexpr size_t kResTableTypeMinSize =
sizeof(ResTable_type) - sizeof(ResTable_config) + sizeof(ResTable_config::size);
O sea, 20 + 4 = 24 bytes: lo justo para llegar a leer config.size.
El historial de tamaños, medido sobre ResourceTypes.h en cada etiqueta
de AOSP:
size |
Último campo añadido | Presente desde |
|---|---|---|
| 36 | screenSizeDp |
android-3.2.4_r1 |
| 48 | localeScript, localeVariant |
android-5.0.0_r1 |
| 52 | screenConfig2 |
android-6.0.0_r1 |
| 56 | localeScriptWasComputed |
android-7.0.0_r1 |
| 64 | localeNumberingSystem |
android-9.0.0_r1 y posteriores, incluida android-17.0.0_r1 |
Los valores de la columna size están medidos compilando con clang la struct ResTable_config de cada etiqueta, solo con sus campos, para aarch64 y armv7a de Android: 36
de android-3.2.4_r1 a android-4.4_r1, 48 en android-5.0.0_r1 y android-5.1.0_r1, 52 en
android-6.0.0_r1, 56 de android-7.0.0_r1 a android-8.1.0_r1 —el colorMode de Android 8
ocupa un byte que era de relleno— y 64 en android-9.0.0_r1 y android-17.0.0_r1, que además
lo fija con static_assert(sizeof(ResTable_config) == 64). En el muestrario solo aparecen 56 y
64.
Y aparecen los dos. Sobre los 58 APK standalone: 16.028 configuraciones de 64 bytes y 302 de
56 bytes, todas las de 56 en un solo fichero, org.gnucash.android_2.4.0.apk, compilado con
una cadena de herramientas de la época de Android 7. Los 505 APK internos de los contenedores
usan 64 sin excepción.
Un lector que asuma 64 lee 8 bytes de más en cada una de las 302 configuraciones de ese fichero —invadiendo el array de offsets que viene detrás— y produce basura sin dar error.
8.3 language y country: el truco del bit alto
Los dos campos ocupan dos bytes y tienen que representar tres cosas: «cualquiera», un código
ISO-639-1 de dos letras, y un código ISO-639-2 de tres letras. La solución empaqueta tres
valores de 5 bits en los 16 disponibles y usa el bit alto como discriminador —
{1, t,t,t,t,t, s,s,s,s,s, f,f,f,f,f}, con las letras primera, segunda y tercera en los bits
0–4, 5–9 y 10–14—. «The layout is always big-endian irrespective of the runtime architecture»,
dice la cabecera. El desempaquetado real, de unpackLanguageOrRegion:
if (in[0] & 0x80) { // tres letras
const uint8_t first = in[1] & 0x1f;
const uint8_t second = ((in[1] & 0xe0) >> 5) + ((in[0] & 0x03) << 3);
const uint8_t third = (in[0] & 0x7c) >> 2;
out[0] = first + base; out[1] = second + base; out[2] = third + base;
return 3;
}
if (in[0]) { memcpy(out, in, 2); return 2; } // dos letras ASCII
return 0; // "cualquiera"
La base es 'a' para el idioma y '0' para la región, porque las regiones UN M.49 son
numéricas de tres dígitos.
Los dos casos, medidos sobre los ResTable_type del tipo string de
org.fdroid.fdroid_1023052.apk, que trae 85 idiomas:
catalán config +0x00 40 00 00 00 00 00 00 00 63 61 00 00 … language = 'c','a' en ASCII
filipino config +0x00 40 00 00 00 00 00 00 00 ad 05 00 00 … language = 0xad, 0x05
Para el segundo, 0xad & 0x80 ≠ 0, luego son tres letras:
first = 0x05 & 0x1f = 5 → 'a'+5 = 'f';
second = ((0x05 & 0xe0) >> 5) + ((0xad & 0x03) << 3) = 0 + 8 → 'a'+8 = 'i';
third = (0xad & 0x7c) >> 2 = 11 → 'a'+11 = 'l'. Resultado: fil, que es lo que
aapt2 dump resources imprime como (fil). En el mismo fichero están también empaquetados
ast y yue.
Cuando el locale necesita más que idioma y región —escritura, variante, sistema de numeración—
se usan además localeScript, localeVariant y localeNumberingSystem, y el calificador de
directorio pasa de la forma es-rES a la forma BCP-47 extendida b+es+Latn+ES.
8.4 Las banderas CONFIG_*
Son las que aparecen en las máscaras del ResTable_typeSpec («sección 6 · ResTable_typeSpec») y las que devuelve
ResTable_config::diff(). ResourceTypes.h las define por indirección a las constantes
ACONFIGURATION_* de frameworks/native/include/android/configuration.h, donde están los
valores:
| Bandera | Valor | Bandera | Valor |
|---|---|---|---|
CONFIG_MCC |
0x0001 |
CONFIG_SCREEN_SIZE |
0x0200 |
CONFIG_MNC |
0x0002 |
CONFIG_VERSION |
0x0400 |
CONFIG_LOCALE |
0x0004 |
CONFIG_SCREEN_LAYOUT |
0x0800 |
CONFIG_TOUCHSCREEN |
0x0008 |
CONFIG_UI_MODE |
0x1000 |
CONFIG_KEYBOARD |
0x0010 |
CONFIG_SMALLEST_SCREEN_SIZE |
0x2000 |
CONFIG_KEYBOARD_HIDDEN |
0x0020 |
CONFIG_LAYOUTDIR |
0x4000 |
CONFIG_NAVIGATION |
0x0040 |
CONFIG_SCREEN_ROUND |
0x8000 |
CONFIG_ORIENTATION |
0x0080 |
CONFIG_COLOR_MODE |
0x10000 |
CONFIG_DENSITY |
0x0100 |
CONFIG_GRAMMATICAL_GENDER |
0x20000 |
La cabecera advierte de que estos valores deben coincidir con los de
android.content.pm.ActivityInfo y attrs_manifest.xml. La máscara 0x4000 que imprime
aapt2 dump chunks como layout_dir en la receta 15.3 es CONFIG_LAYOUTDIR.
8.5 Densidad
density no es un enumerado cerrado: es el valor en dpi. Los nombres son atajos.
| Constante | Valor | Calificador |
|---|---|---|
DENSITY_DEFAULT |
0 | — |
DENSITY_LOW |
120 | ldpi |
DENSITY_MEDIUM |
160 | mdpi |
DENSITY_TV |
213 | tvdpi |
DENSITY_HIGH |
240 | hdpi |
DENSITY_XHIGH |
320 | xhdpi |
DENSITY_XXHIGH |
480 | xxhdpi |
DENSITY_XXXHIGH |
640 | xxxhdpi |
DENSITY_ANY |
0xfffe |
anydpi |
DENSITY_NONE |
0xffff |
nodpi |
El split config.xhdpi.apk de Google Keep trae density = 0x0140 = 320, consistente con la
tabla. Un detalle que muerde: mnc = 0 significa «cualquiera», así que la MNC real cero se
codifica con el valor centinela ACONFIGURATION_MNC_ZERO = 0xffff.
9. Cómo elige Android qué entrada usar
Dado un resource ID y la configuración del dispositivo, AssetManager recorre todos los
ResTable_type del tipo correspondiente y aplica dos filtros en cadena.
Primero match(). Descarta las configuraciones incompatibles. La regla es asimétrica:
«A default piece of data will match every request but a request for the default should not
match odd specifics». Una entrada sin idioma sirve para cualquier petición; una entrada es no
sirve para una petición de; una entrada sw600dp no sirve para un dispositivo de 480 dp, pero
sí al revés.
Después isBetterThan(). Entre las supervivientes elige la mejor, comparando eje por eje
en un orden fijo y devolviendo en el primer eje que difiera:
mcc → mnc → locale → grammaticalInflection → layoutDirection →
smallestScreenWidthDp → screenWidthDp/screenHeightDp → screenSize (small/normal/large) →
screenLong → screenRound → wideColorGamut → HDR → orientation →
uiModeType → uiModeNight → density → touchscreen →
keysHidden → navHidden → keyboard → navigation →
screenWidth/screenHeight → sdkVersion → minorVersion
La regla general, del comentario de la cabecera: si la petición se interesa por un eje y las dos
candidatas difieren, una de las dos tiene que ser genérica —porque match() ya eliminó las
incompatibles—, así que gana la que no es genérica.
Tres ejes tienen lógica propia y son los que más sorprenden:
- Densidad. No gana la más parecida sino la que se pueda reducir en vez de ampliar:
gana la menor de las que llegan a la pedida, y si ninguna llega, la mayor. Un dispositivo
mdpiconxhdpiyxxxhdpidisponibles eligexhdpi; conldpiyxhdpi,xhdpi.DENSITY_ANYgana siempre. Si no se pide densidad, se asumeDENSITY_MEDIUM(160). - Dimensiones en dp. Gana la que minimiza la suma de
pedido - candidataen anchura y altura, asumiendo quematch()ya descartó las mayores que lo pedido. sdkVersion. Gana la mayor, no la más cercana:return (sdkVersion > o.sdkVersion). Es lo que hace que un recurso marcado-v31sustituya al-v21en un dispositivo moderno.
El locale tiene su propia función, isLocaleBetterThan(), con las reglas de fallback de
BCP-47 —idioma, escritura, región, variante— y una puntuación de importancia
(getImportanceScoreOfLocale).
Reproducir isBetterThan exactamente no hace falta para leer ni para reescribir la tabla;
hace falta para elegir qué splits fusionar y para el modo de fusión que descarta
configuraciones. Cuando haya que reproducirlo, la única referencia válida es el código:
ResTable_config::match() y ResTable_config::isBetterThan() en
libs/androidfw/ResourceTypes.cpp.
10. ResTable_entry, ResTable_map_entry y ResTable_map
10.1 La entrada normal
ResTable_entry es una union de dos formas. La normal, Full:
| Campo | Offset | Tamaño | Tipo | Significado |
|---|---|---|---|---|
size |
0x00 |
2 | uint16_t |
Tamaño de esta cabecera de entrada, no del valor |
flags |
0x02 |
2 | uint16_t |
Ver tabla |
key |
0x04 |
4 | ResStringPool_ref |
Índice en el keyStrings del paquete |
| Flag | Valor | Significado |
|---|---|---|
FLAG_COMPLEX |
0x0001 |
La entrada es un mapa: le siguen count ResTable_map en vez de un Res_value |
FLAG_PUBLIC |
0x0002 |
Declarada pública; otras bibliotecas pueden referenciarla |
FLAG_WEAK |
0x0004 |
Recurso débil, sustituible por uno fuerte del mismo nombre al enlazar |
FLAG_COMPACT |
0x0008 |
La entrada usa la forma compacta de 10.2 |
FLAG_USES_FEATURE_FLAGS |
0x0010 |
La entrada depende de flags de lectura/escritura de Android («sección 12.5 · RES_TABLE_FLAGGED (0x0207) y RES_TABLE_FLAG_LIST (0x0208)») |
Si FLAG_COMPLEX no está puesto, inmediatamente después de los size bytes de la cabecera
viene un Res_value de 8 bytes («XML binario, sección 5»).
size vale 8 en ese caso.
Si FLAG_COMPLEX sí está puesto, la cabecera es un ResTable_map_entry, que extiende a
Full:
| Campo | Offset | Tamaño | Tipo | Significado |
|---|---|---|---|---|
size, flags, key |
0x00 |
8 | ResTable_entry::Full |
Igual que arriba. size vale 16 |
parent |
0x08 |
4 | ResTable_ref |
resource ID del mapa del que se hereda, o 0. Se trata siempre como TYPE_DYNAMIC_REFERENCE |
count |
0x0c |
4 | uint32_t |
Número de pares nombre-valor que siguen |
Y a continuación count estructuras ResTable_map de 12 bytes:
| Campo | Offset | Tamaño | Tipo | Significado |
|---|---|---|---|---|
name |
0x00 |
4 | ResTable_ref |
resource ID del atributo que se define, o un valor especial ATTR_* |
value |
0x04 |
8 | Res_value |
El valor |
Los valores especiales de name se construyen con la macro
Res_MAKEINTERNAL(x) = 0x01000000 | (x & 0xFFFF):
| Nombre | Valor | Uso |
|---|---|---|
ATTR_TYPE |
0x01000000 |
Máscara de tipos admitidos por el atributo |
ATTR_MIN |
0x01000001 |
Valor mínimo para atributos enteros |
ATTR_MAX |
0x01000002 |
Valor máximo |
ATTR_L10N |
0x01000003 |
L10N_NOT_REQUIRED = 0, L10N_SUGGESTED = 1 |
ATTR_OTHER, ATTR_ZERO, ATTR_ONE, ATTR_TWO, ATTR_FEW, ATTR_MANY |
0x01000004 a 0x01000009 |
Las seis categorías de plural, en ese orden |
Y la máscara de tipos que acompaña a ATTR_TYPE:
| Nombre | Valor | Significado |
|---|---|---|
TYPE_ANY |
0x0000FFFF |
Sin restricción entre los tipos genéricos |
TYPE_REFERENCE |
1 << 0 |
Admite una referencia |
TYPE_STRING |
1 << 1 |
Admite una cadena |
TYPE_INTEGER |
1 << 2 |
Admite un entero |
TYPE_BOOLEAN |
1 << 3 |
Admite un booleano |
TYPE_COLOR |
1 << 4 |
Admite un color |
TYPE_FLOAT |
1 << 5 |
Admite un decimal |
TYPE_DIMENSION |
1 << 6 |
Admite una dimensión |
TYPE_FRACTION |
1 << 7 |
Admite una fracción |
TYPE_ENUM |
1 << 16 |
Enumerado; los valores vienen como entradas más del mapa |
TYPE_FLAGS |
1 << 17 |
Máscara de bits; los valores vienen como entradas más del mapa |
Tres ejemplos reales de com.termux_1002.apk, del tipo 0x03 (attr) y del 0x11 (style):
id=0x7f030000 size=16 flags=0x0005[COMPLEX,WEAK] key=58('actionBarDivider')
parent=0x00000000 count=1
map name=0x01000000 ATTR_TYPE value dataType=0x10 data=0x00000001 ← TYPE_REFERENCE
id=0x7f03002f key='alphabeticModifiers' parent=0x00000000 count=7
map name=0x01000000 ATTR_TYPE value dataType=0x10 data=0x00020000 ← TYPE_FLAGS (1<<17)
map name=0x7f080005 value dataType=0x11 data=0x00010000 ← un valor de la máscara
map name=0x7f080003 value dataType=0x11 data=0x00001000
id=0x7f110000 size=16 flags=0x0001[COMPLEX] key=2728('AlertDialog.AppCompat')
parent=0x7f110008 count=0 ← estilo que solo hereda
10.2 La entrada compacta: FLAG_COMPACT
Es la adición más reciente. Cuando está puesto FLAG_COMPACT = 0x0008, los 8 bytes de la
entrada contienen ya el valor y no hay Res_value detrás:
| Campo | Offset | Tamaño | Tipo | Significado |
|---|---|---|---|---|
key |
0x00 |
2 | uint16_t |
Índice en keyStrings, en 16 bits |
flags |
0x02 |
2 | uint16_t |
Byte bajo: los flags. Byte alto: el dataType del valor |
data |
0x04 |
4 | uint32_t |
El dato |
El campo flags está deliberadamente en el mismo offset que en la forma Full, con un
static_assert que lo garantiza, para poder decidir qué forma es antes de interpretar el
resto. El size de la entrada y el del valor se dan por supuestos: 8 bytes ambos.
aapt2 solo lo emite con --enable-compact-entries y minSdkVersion > 33, es decir Android 14
en adelante, y únicamente si todas las claves del tipo caben en 16 bits. No se ha observado ni
una vez en el muestrario: 0 entradas compactas en los 563 APK con resources.arsc inspeccionados.
Un lector escrito hoy tiene que soportarlo igualmente, porque los APK que lo usen empezarán a
aparecer conforme suba el minSdk obligatorio de Play.
10.3 Lo que AOSP valida en cada entrada
De VerifyResTableEntry en LoadedArsc.cpp, y merece la pena copiarlo tal cual en un lector
propio: el offset debe estar alineado a 4; no puede desbordar al sumarle entriesStart; tiene
que quedar sitio para un ResTable_entry completo; entry->size() no puede ser menor que
sizeof(ResTable_entry) ni mayor que el chunk; si no es compleja, tiene que quedar sitio para
un Res_value cuyo size sea al menos 8; y si es compleja, los ResTable_map tienen que
empezar alineados a 4 y caber count de ellos en lo que queda de chunk.
11. La estructura del resource ID: 0xPPTTEEEE
| Parte | Bits | Significado |
|---|---|---|
PP |
31–24 | Paquete. Base 1: 0 no es un paquete real, sino la marca de un paquete dinámico cuyo ID asigna el runtime |
TT |
23–16 | Tipo dentro del paquete. Base 1: 0 es inválido |
EEEE |
15–0 | Entrada dentro del tipo. Base 0 |
Las macros de ResourceTypes.h lo dicen sin ambigüedad:
#define Res_MAKEID(package, type, entry) \
(((package+1)<<24) | (((type+1)&0xFF)<<16) | (entry&0xFFFF))
#define Res_GETPACKAGE(id) ((id>>24)-1)
#define Res_GETTYPE(id) (((id>>16)&0xFF)-1)
#define Res_GETENTRY(id) (id&0xFFFF)
Ojo: las macros toman los índices en base 0 y suman uno; los campos id de ResTable_package,
ResTable_typeSpec y ResTable_type guardan ya el valor en base 1, listo para meter en el
resource ID. Res_MAXPACKAGE y Res_MAXTYPE valen 255, y EEEE de 16 bits impone el límite
de 65.535 entradas por tipo, que LoadedArsc.cpp comprueba explícitamente: rechaza un
entryCount > 0xFFFF.
Los valores de PP:
PP |
Quién |
|---|---|
0x00 |
Paquete dinámico, sin identificador asignado: es lo que aapt2 link --shared-lib emite, y AssetManager2 le asigna uno real al cargar |
0x01 |
kFrameworkPackageId — el framework de Android, android.R |
0x02–0x7e |
Reservado históricamente. aapt2 solo lo permite con --allow-reserved-package-id, que existe para los antiguos feature splits previos a Android 8 |
0x7f |
kAppPackageId — la aplicación. Lo normal |
0x80–0xff |
Lo que AGP asigna a los módulos de funcionalidad (FeatureSetMetadata: 0x80, 0x81… con minSdk ≥ 26; por debajo, 0x7e hacia abajo con --allow-reserved-package-id). aapt2 --package-id acepta este rango sin más |
Link.cpp lo valida así, y el mensaje de error es la definición operativa del rango:
invalid package ID 0x%02x. Must be in the range 0x7f-0xff.
con la excepción documentada de que, antes de Android 8, el runtime trataba como inválido
cualquier ID con PP > 0x7f porque el resource ID se interpretaba como entero con signo. De
ahí la existencia del rango reservado 0x02–0x7e y del FeatureSplitSymbolTableDelegate que
reescribe identificadores cuando minSdkVersion < 26.
En el muestrario se ven los dos extremos. org.totschnig.myexpenses_858.apk trae seis paquetes:
0x7f para la aplicación y 0x80, 0x83, 0x84, 0x85 y 0x86 para cinco módulos de
funcionalidad suyos (ocr, webdav, sqlcrypt…, «sección 12.1 · RES_TABLE_LIBRARY_TYPE (0x0203)»).
12. Los chunks menos frecuentes
Un lector puede no entenderlos, pero tiene que conservarlos: si se pierden, la aplicación puede seguir arrancando y fallar en un caso concreto meses después.
12.1 RES_TABLE_LIBRARY_TYPE (0x0203)
Tabla de traducción de identificadores de paquete de bibliotecas compartidas dinámicas. El ID que una biblioteca tiene en tiempo de compilación no tiene por qué ser el que reciba al cargarse; este chunk permite traducirlos por nombre.
ResTable_lib_header: ResChunk_header (8 bytes) + count (uint32_t, offset 0x08).
headerSize = 12. Le siguen count estructuras ResTable_lib_entry de 260 bytes:
| Campo | Offset | Tamaño | Tipo | Significado |
|---|---|---|---|---|
packageId |
0x00 |
4 | uint32_t |
ID asignado en tiempo de compilación. uint32_t solo por alineación |
packageName |
0x04 |
256 | uint16_t[128] |
Nombre del paquete en UTF-16, terminado en \0 |
De org.totschnig.myexpenses_858.apk, el chunk del cuarto paquete:
RES_TABLE_LIBRARY_TYPE en +0x6d1704: headerSize=12 size=792 count=3
ResTable_lib_entry packageId=0x80 packageName='org.totschnig.myexpenses.ocr'
ResTable_lib_entry packageId=0x83 packageName='org.totschnig.myexpenses.webdav'
ResTable_lib_entry packageId=0x84 packageName='org.totschnig.myexpenses.sqlcrypt'
Cada paquete declara las bibliotecas de las que depende, así que el mismo nombre aparece en varios chunks. Observado en 11 chunks del muestrario.
12.2 RES_TABLE_OVERLAYABLE_TYPE (0x0204) y su política (0x0205)
Declara qué recursos pueden sobrescribir los Runtime Resource Overlays. Es lo que emite el
bloque <overlayable> del fuente.
ResTable_overlayable_header: ResChunk_header (8) + name (uint16_t[256], offset 0x08) +
actor (uint16_t[256], offset 0x208). headerSize = 1032, confirmado en el muestrario.
Dentro vienen uno o más ResTable_overlayable_policy_header de headerSize 16: header
(0x00, 8 bytes), policy_flags (0x08, uint32_t) y entry_count (0x0c, uint32_t),
seguidos de entry_count estructuras ResTable_ref de 4 bytes.
Bandera de policy_flags |
Valor | Requisito que impone al overlay |
|---|---|---|
NONE |
0x00000000 |
— |
PUBLIC |
0x00000001 |
Cualquier overlay |
SYSTEM_PARTITION |
0x00000002 |
Residir en la partición system |
VENDOR_PARTITION |
0x00000004 |
Residir en vendor |
PRODUCT_PARTITION |
0x00000008 |
Residir en product |
SIGNATURE |
0x00000010 |
Estar firmado con la misma clave que el paquete objetivo |
ODM_PARTITION |
0x00000020 |
Residir en odm |
OEM_PARTITION |
0x00000040 |
Residir en oem |
ACTOR_SIGNATURE |
0x00000080 |
Estar firmado con la clave del actor declarado |
CONFIG_SIGNATURE |
0x00000100 |
Estar firmado con la clave de referencia de SystemConfig |
De com.google.android.apps.maps.apk, extraído del .apks del muestrario:
RES_TABLE_OVERLAYABLE_TYPE +0x384e58 headerSize=1032 size=1056
name='AssistedDrivingBrandingResources' actor=''
RES_TABLE_OVERLAYABLE_POLICY_TYPE +0x385260 headerSize=16 size=24
policy_flags=0x0000000e entry_count=2
ResTable_ref ident=0x7f0802d0
ResTable_ref ident=0x7f14242a
0x0e = SYSTEM_PARTITION | VENDOR_PARTITION | PRODUCT_PARTITION. Observado en 34 chunks del
muestrario, todos en aplicaciones de Google con integración de Android Auto.
12.3 RES_TABLE_STAGED_ALIAS_TYPE (0x0206)
Reescribe identificadores staged —no finalizados— a sus equivalentes definitivos. Es un mecanismo del ciclo de desarrollo del framework.
ResTable_staged_alias_header: ResChunk_header (8) + count (uint32_t, offset 0x08).
Le siguen count ResTable_staged_alias_entry de 8 bytes: stagedResId en 0x00 y
finalizedResId en 0x04, ambos uint32_t.
No se ha observado en el muestrario: 0 ocurrencias. Aparece en el framework-res.apk de la
plataforma, no en aplicaciones.
12.4 RES_NULL_TYPE (0x0000)
Chunk de relleno. Un lector debe saltarlo por size como cualquier otro.
12.5 RES_TABLE_FLAGGED (0x0207) y RES_TABLE_FLAG_LIST (0x0208)
Llegaron después de congelarse main: están en android-17.0.0_r1. Sirven para los recursos
que dependen de un flag de lectura/escritura de Android, cuyo valor solo se conoce en el
dispositivo.
ResTable_flagged: ResChunk_header (8) + flag_name_index (ResStringPool_ref, offset
0x08) + flag_negated (bool, 0x0c) + 3 bytes de relleno. Va dentro del paquete, detrás
del RES_TABLE_TYPE_SPEC_TYPE, y contiene chunks RES_TABLE_TYPE_TYPE. LoadedArsc.cpp
los carga solo si el flag está activo —o inactivo, con flag_negated— y los pone por delante
de los no condicionados, así que ganan.
ResTable_flag_list: una ResChunk_header seguida de un array de ResStringPool_ref al pool
global con los nombres de todos los flags usados. Va una vez, en el nivel de RES_TABLE_TYPE,
detrás del pool global. Un RES_TABLE_FLAGGED cuyo flag no esté en la lista hace fallar la
carga del paquete.
Los dos se procesan solo con el flag de plataforma resource_readwrite_flags activo; sin él,
LoadedArsc.cpp los salta en silencio, sin mirar dentro (líneas 670-705 y 942-987 en
android-17.0.0_r1). Buscados en los 124 resources.arsc disponibles —los de los 58 APK
standalone y los de 66 APK internos de los tres .xapk—: ninguno contiene un 0x0207 ni
un 0x0208. ⚠️ sin verificar: los 25 contenedores restantes del muestrario no se han podido
revisar.
13. Qué entender y qué conservar en crudo
Esta es la estrategia de diseño sensata, explicada desde el lado del formato.
El sustrato de chunks garantiza que se puede reemitir sin pérdida lo que no se entiende: la
cabecera lleva el tamaño total, así que un chunk desconocido es un bloque opaco de size bytes
que se copia tal cual. Eso permite dividir el formato en dos:
| Se modela tipado, se puede mutar | Se conserva en crudo, se reemite byte a byte |
|---|---|
RES_TABLE_TYPE — hay que recalcular size |
RES_TABLE_LIBRARY_TYPE mientras no se remapeen paquetes |
RES_STRING_POOL_TYPE — hay que añadir y renombrar cadenas |
RES_TABLE_OVERLAYABLE_TYPE y su política |
RES_TABLE_PACKAGE_TYPE — hay que fusionar paquetes |
RES_TABLE_STAGED_ALIAS_TYPE |
RES_TABLE_TYPE_SPEC_TYPE — hay que unir máscaras al fusionar |
RES_NULL_TYPE y cualquier tipo futuro |
RES_TABLE_TYPE_TYPE — es donde viven los valores |
El reparto no es arbitrario: los cinco tipos de la izquierda son los que hay que tocar para fusionar dos tablas y para renombrar entradas, que son las dos únicas mutaciones que un fusionador necesita hacer. Todo lo demás se copia.
Dos matices que el reparto no debe ocultar:
- Conservar en crudo no es ignorar. Si
mergereasigna elPPde un paquete, losResTable_refque viven dentro de unRES_TABLE_OVERLAYABLE_POLICY_TYPEdejan de apuntar donde deben. La respuesta correcta no es reescribirlos a ciegas: es detectarlo y advertirlo en el informe. - La métrica de completitud es medible. El porcentaje de bytes que un fichero reemite desde el modelo tipado, frente a los que reemite en crudo, convierte «¿entendemos el formato?» en un número que se puede seguir compilación a compilación, en vez de en una apuesta. Es lo que hace gestionable el riesgo de dar por entendido un formato que solo se entiende a medias.
14. Recursos ofuscados
Hay dos cosas distintas que se llaman «ofuscación de recursos», y solo una de ellas es frecuente.
El acortamiento de rutas. Es lo que hace la optimización de recursos del Android Gradle
Plugin: res/layout/activity_main.xml pasa a llamarse res/vp1.xml dentro del ZIP, y la cadena
del pool global cambia en consecuencia. El nombre de la entrada —la key— no se toca.
El renombrado de claves. Sustituir app_name por a en el pool keyStrings. Es lo que
hacen herramientas como AndResGuard, y es mucho más destructivo porque no queda de dónde
recuperar el nombre.
Medido sobre el muestrario, la diferencia entre ambos es abrumadora:
| Fichero | Claves | % de claves ≤ 3 car. | Rutas res/ |
% de rutas ≤ 3 car. |
|---|---|---|---|---|
com.termux_1002.apk |
3.507 | 0,5 % | 637 | 84,8 % |
org.fdroid.fdroid_1023052.apk |
6.632 | 0,5 % | 1.067 | 78,5 % |
org.videolan.vlc_13070108.apk |
10.145 | 0,3 % | 2.424 | 89,2 % |
Google Keep, base.apk |
6.895 | 0,2 % | 954 | 74,7 % |
WhatsApp, base.apk |
17.250 | 0,2 % | 8.753 | 100,0 % |
Netflix, base.apk |
14.046 | 0,0 % | 4.656 | 100,0 % |
Twitch, base.apk |
19.893 | 0,2 % | 3.830 | 0,2 % |
Shazam, base.apk |
7.555 | 0,3 % | 1.203 | 0,0 % |
El acortamiento de rutas está en casi todas partes; las claves cortas al estilo AndResGuard,
en ninguna de las ocho aplicaciones medidas. Pero eso no quiere decir que las claves estén
intactas: esta métrica no detecta los otros dos esquemas de renombrado. WhatsApp sustituye los
nombres por la cadena centinela (name removed), Snapchat por 0_resource_name_obfuscated
(aapt2 optimize --collapse-resource-names) y Netflix, con DexGuard, por el propio
resource ID en decimal, como se mide en la «sección 2.9 de
Ofuscadores comerciales · Ofuscación de recursos». Donde las claves no se
han tocado, la información que hace falta para deshacer la ofuscación de recursos sigue en el
fichero.
El caso, tal como se ve en el split config.xhdpi.apk de Google Keep:
[ResTable_type] id: 0x08 name: drawable flags: 0x01 (SPARSE) entryCount: 42 config: xhdpi
[ResTable_entry] id: 0x0161 name: abc_ab_share_pack_mtrl_alpha keyIndex: 0 size: 8
[Res_value] size: 8 dataType: 0x03 data: 0x0000000b ((file) res/JJ.9.png type=PNG)
[ResTable_entry] id: 0x0166 name: abc_btn_check_to_on_mtrl_000 keyIndex: 1 size: 8
[Res_value] size: 8 dataType: 0x03 data: 0x00000000 ((file) res/-B.png type=PNG)
El fichero se llama res/JJ.9.png, pero la entrada sigue diciendo abc_ab_share_pack_mtrl_alpha
y el tipo sigue diciendo drawable. La ruta legible es reconstruible de forma determinista:
res/<tipo><calificadores>/<key>.<extensión>
│ │ │ └── del contenido, o del sufijo .9.png que sí se conserva
│ │ └── ResTable_entry.key → keyStrings
│ └── ResTable_type.config → calificador
└── ResTable_type.id → typeStrings
En el ejemplo: res/drawable-xhdpi/abc_ab_share_pack_mtrl_alpha.9.png. Reconstruir así la ruta
es deofuscación determinista y sin riesgo, al mismo nivel que aplicar un mapping.txt: la
información no se adivina, se lee de la propia tabla.
La receta solo vale si las claves están intactas. Con nombres colapsados, todas las entradas de
un tipo comparten la misma key y la receta genera la misma ruta para todas: un APK de prueba con
dos drawable pasado por aapt2 optimize --collapse-resource-names da dos veces
res/drawable/0_resource_name_obfuscated.xml. En ese caso hay que desambiguar, por ejemplo
añadiendo la parte EEEE del resource ID al nombre.
Las dos operaciones que exige son renombrar la entrada del ZIP y actualizar la cadena
correspondiente del pool global —no la del keyStrings, que ya está bien—, más los
AndroidManifest.xml y los res/*.xml que referencien la ruta por texto. Ninguna toca la
estructura de la tabla.
⚠️ sin verificar: el muestrario no contiene ninguna muestra con claves cortas al estilo
AndResGuard, así que no se ha podido comprobar si ese esquema se detecta con fiabilidad. En los
124 APK disponibles con resources.arsc, la proporción más alta de claves de tres caracteres o
menos es el 0,94 % de Bitwarden (25 de 2.661); en F-Droid y Termux es del 0,5 %, y todas son
nombres legítimamente cortos (add, all, top, on, off). Los esquemas que sí aparecen
—cadena centinela única y resource ID en decimal— no se ven por longitud; sus huellas están en
la «sección 2.9 de Ofuscadores comerciales · Ofuscación de recursos».
15. Recetas
Máquina de referencia: macOS, aapt2 2.20-15087165 de las build-tools 37.0.0.
15.1 aapt2 dump resources
BT=~/Library/Android/sdk/build-tools/37.0.0
MUESTRAS=~/muestras-apk
$BT/aapt2 dump resources $MUESTRAS/com.termux_1002.apk | head -6
Binary APK
Package name=com.termux id=7f
type anim id=01 entryCount=32
resource 0x7f010000 anim/abc_fade_in
() (file) res/vp1.xml type=XML
resource 0x7f010001 anim/abc_fade_out
El () es la configuración por defecto. Con un recurso traducido se ve la lista entera; de
org.fdroid.fdroid_1023052.apk:
resource 0x7f120076 string/app_name_basic
() "F-Droid Basic"
(fil) "F-Droid Basic"
(ca) "F-Droid Bàsic"
(da) "F-Droid Basis"
(fa) "افدروید پایه"
(ga) "F-Droid Bunúsach"
15.2 aapt2 dump configurations
Lista las configuraciones de la tabla, ya traducidas a calificadores de directorio.
$BT/aapt2 dump configurations $MUESTRAS/org.fdroid.fdroid_1023052.apk | head -8
h720dp
sw360dp
sw480dp
sw600dp
sw720dp
watch
night
La primera línea vacía es la configuración por defecto. El fichero tiene 141 configuraciones distintas.
15.3 aapt2 dump chunks
Es el volcador oficial del formato y el mejor oráculo diferencial para un lector propio.
$BT/aapt2 dump chunks /tmp/keep/config.xhdpi.apk | sed -n '113,122p'
[RES_TABLE_TYPE_SPEC_TYPE] chunkSize: 3256 headerSize: 16 id: 0x08 types: 2 entry configs: 810
Entry qualifier masks:
#0x19e = 0x4000: layout_dir
[ResTable_type] chunkSize: 924 headerSize: 84 id: 0x08 name: drawable flags: 0x01 (SPARSE) entryCount: 42 entryStart: 252 config: xhdpi
[ResTable_entry] id: 0x0161 name: abc_ab_share_pack_mtrl_alpha keyIndex: 0 size: 8 flags: 0x0000
[Res_value] size: 8 dataType: 0x03 data: 0x0000000b ((file) res/JJ.9.png type=PNG)
[ResTable_entry] id: 0x0166 name: abc_btn_check_to_on_mtrl_000 keyIndex: 1 size: 8 flags: 0x0000
[Res_value] size: 8 dataType: 0x03 data: 0x00000000 ((file) res/-B.png type=PNG)
Obsérvese la máscara del typeSpec: solo la entrada 0x19e de las 810 varía, y varía por
layout_dir. Ese es el índice de la «sección 6 · ResTable_typeSpec» en acción.
15.4 Volcado de chunks propio
El mismo recorrido de «XML binario, sección 8.2»,
descendiendo por headerSize y avanzando por size, aplicado a RES_TABLE_TYPE y
RES_TABLE_PACKAGE_TYPE. Sobre com.termux_1002.apk:
fichero: resources.arsc (771032 bytes)
+0x000000 RES_TABLE_TYPE headerSize=12 size=771032 packageCount=1
+0x00000c RES_STRING_POOL_TYPE headerSize=28 size=220604 stringCount=6338 UTF-8
+0x035dc8 RES_TABLE_PACKAGE_TYPE headerSize=288 size=550416 id=0x7f
name='com.termux' typeStrings=0x120 keyStrings=0x2a0
+0x035ee8 RES_STRING_POOL_TYPE headerSize=28 size=384 stringCount=19 UTF-16
+0x036068 RES_STRING_POOL_TYPE headerSize=28 size=119564 stringCount=3507 UTF-8
+0x053374 RES_TABLE_TYPE_SPEC_TYPE headerSize=16 size=144 id=0x01 entryCount=32
+0x053404 RES_TABLE_TYPE_TYPE headerSize=84 size=724 id=0x01 flags=0x00
entryCount=32 entriesStart=0xd4 config.size=64
…
El fichero completo tiene 260 chunks: 1 tabla, 3 string pools, 1 paquete, 19 typeSpec y 236
ResTable_type. Y el censo de todo el muestrario, con el mismo recorrido, del que salen las cifras
de las secciones «7.2», «7.3», «8.2», «12.1», «12.2» y «12.3»:
| Medida | 58 APK standalone | 505 APK internos de contenedores |
|---|---|---|
ResTable_config.size |
16.028 de 64 bytes, 302 de 56 | 5.917 de 64 bytes, ninguno de 56 |
ResTable_type.flags |
16.330 con 0x00 |
4.941 con 0x00, 976 con FLAG_SPARSE |
FLAG_OFFSET16 |
0 | 0 |
| String pools | 121 UTF-8, 63 UTF-16, 33 con estilos | 1.005 UTF-8, 500 UTF-16, 252 con estilos |
RES_TABLE_LIBRARY_TYPE |
5 | 6 |
RES_TABLE_OVERLAYABLE_TYPE |
0 | 34 |
RES_TABLE_STAGED_ALIAS_TYPE |
0 | 0 |
15.5 apktool
apktool d -f -o salida $MUESTRAS/com.termux_1002.apk
ls salida/res | head -4
head -4 salida/res/values/strings.xml
anim
animator
color
color-night
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="abc_action_bar_home_description">Navigate home</string>
<string name="abc_action_bar_up_description">Navigate up</string>
Con -r, apktool copia resources.arsc en crudo y no decodifica nada:
apktool d -r -f -o salida-r $MUESTRAS/com.termux_1002.apk
I: Copying raw resources.arsc...
I: Baksmaling classes.dex...
I: Copying raw AndroidManifest.xml...
Verificado con apktool 3.0.3 el 13 de agosto de 2026. Dos consecuencias de -r que no son
obvias y que muerden:
- El manifiesto tampoco se decodifica. Lo dice la tercera línea: queda como
AXMLbinario, no como XML de texto, porque resolver sus referencias exige la tabla que-rno ha leído. - El
apktool.ymlsale con datos distintos, y peor. Sin-rdeclaraminSdkVersion: 24; con-r,minSdkVersion: 25. El valor real del manifiesto, contrastado conaapt2 dump xmltree, es 24. En modo-rel dato es sencillamente incorrecto, y quien reconstruya desde ahí obtiene unAPKcon unminSdkVersionque no es el del original.
Fuentes
frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h(etiquetaandroid-17.0.0_r1) — https://android.googlesource.com/platform/frameworks/base/+/refs/tags/android-17.0.0_r1/libs/androidfw/include/androidfw/ResourceTypes.h Consultado el 25 de septiembre de 2026, descargado con?format=TEXTy decodificado. De aquí salen íntegras las tablas de campos de las secciones «3», «5», «6», «7», «8.1», «10», «11», «12.1», «12.2», «12.3» y «12.5», las constantesSPEC_*,FLAG_*,ATTR_*,TYPE_*,PolicyFlags, las macrosRes_MAKEID/Res_GETPACKAGE/Res_GETTYPE/Res_GETENTRY,kResTableTypeMinSize,offset_from16,ResTable_config::copyFromDeviceNoSwap(«sección 8.2 · El problema del size variable») y losstatic_assertde queResTable_configva al final deResTable_typey de que ocupa 64 bytes.frameworks/base/libs/androidfw/ResourceTypes.cpp(etiquetaandroid-17.0.0_r1) — https://android.googlesource.com/platform/frameworks/base/+/refs/tags/android-17.0.0_r1/libs/androidfw/ResourceTypes.cpp Consultado el 25 de septiembre de 2026. De aquí salenunpackLanguageOrRegionypackLanguageOrRegion(«sección 8.3 · language y country: el truco del bit alto»), yResTable_config::match(),isBetterThan(),isLocaleBetterThan()eisMoreSpecificThan()(«sección 9 · Cómo elige Android qué entrada usar»).frameworks/base/libs/androidfw/LoadedArsc.cpp(etiquetaandroid-17.0.0_r1) — https://android.googlesource.com/platform/frameworks/base/+/refs/tags/android-17.0.0_r1/libs/androidfw/LoadedArsc.cpp Consultado el 25 de septiembre de 2026. De aquí salenVerifyResTableType,VerifyResTableEntry(«sección 10.3 · Lo que AOSP valida en cada entrada»),GetEntryOffsetcon la búsqueda binaria sobre las entradas dispersas («sección 7.3 · FLAG_SPARSE y ResTable_sparseTypeEntry»), el límite de 65.535 entradas por tipo («sección 11 · La estructura del resource ID: 0xPPTTEEEE»), las reglas de orden entretypeSpecytypede la «sección 2 · El árbol de chunks» y la carga deRES_TABLE_FLAGGEDyRES_TABLE_FLAG_LIST(«sección 12.5 · RES_TABLE_FLAGGED (0x0207) y RES_TABLE_FLAG_LIST (0x0208)»).frameworks/base/tools/aapt2/Resource.h(etiquetaandroid-17.0.0_r1) — https://android.googlesource.com/platform/frameworks/base/+/refs/tags/android-17.0.0_r1/tools/aapt2/Resource.h Consultado el 25 de septiembre de 2026. De aquí salenkAppPackageId = 0x7f,kFrameworkPackageId = 0x01y la descripción canónica de0xPPTTEEEEde la «sección 11 · La estructura del resource ID: 0xPPTTEEEE».frameworks/base/tools/aapt2/cmd/Link.cpp(etiquetaandroid-17.0.0_r1) — https://android.googlesource.com/platform/frameworks/base/+/refs/tags/android-17.0.0_r1/tools/aapt2/cmd/Link.cpp Consultado el 25 de septiembre de 2026. De aquí salen la validación del rango de--package-id, el0x00que asigna--shared-lib, el rango reservado0x02–0x7ede--allow-reserved-package-idy la explicación del comportamiento previo a Android 8, todo en la «sección 11 · La estructura del resource ID: 0xPPTTEEEE».frameworks/base/tools/aapt2/format/binary/TableFlattener.cppyTableFlattener.h(etiquetaandroid-17.0.0_r1) — https://android.googlesource.com/platform/frameworks/base/+/refs/tags/android-17.0.0_r1/tools/aapt2/format/binary/TableFlattener.cpp Consultado el 25 de septiembre de 2026. De aquí salen las condiciones exactas con queaapt2emiteFLAG_SPARSE,FLAG_OFFSET16yFLAG_COMPACT, y la constantekSparseEncodingThreshold = 60de las secciones «7.2», «7.3» y «10.2», y dónde escribe los chunks de la «sección 12.5 · RES_TABLE_FLAGGED (0x0207) y RES_TABLE_FLAG_LIST (0x0208)». La opción--force-sparse-encodingde la «sección 7.3 · FLAG_SPARSE y ResTable_sparseTypeEntry» está entools/aapt2/cmd/Convert.h, en la misma etiqueta.frameworks/base/include/androidfw/ResourceTypes.hen las etiquetasandroid-4.4_r1,android-5.0.0_r1,android-6.0.0_r1,android-7.1.2_r36,android-9.0.0_r1yandroid-11.0.0_r1— https://android.googlesource.com/platform/frameworks/base/+/refs/tags/android-9.0.0_r1/libs/androidfw/include/androidfw/ResourceTypes.h Consultado el 12 de agosto de 2026, las seis etiquetas. De aquí sale el historial de campos deResTable_configde la tabla de la «sección 8.2 · El problema del size variable».- apktool — parámetros de la CLI — https://apktool.org/docs/cli-parameters Consultado el 12 de agosto de 2026. De aquí sale la sintaxis de la receta 15.5.
- apktool 3.0.3 — instalado y ejecutado el 13 de agosto de 2026 sobre
com.termux_1002.apkdel muestrario, con y sin-r. De aquí salen las dos salidas de la «sección 15.5 · apktool» y la discrepancia medida deminSdkVersionentre los dos modos, contrastada conaapt2 dump xmltree. frameworks/base/libs/androidfw/LoadedArsc.cpp,AssetManager2.cppytools/aapt2/format/binary/TableFlattener.cpp(etiquetaandroid-16.0.0_r1) — https://android.googlesource.com/platform/frameworks/base/+/refs/tags/android-16.0.0_r1/libs/androidfw/AssetManager2.cpp Consultado el 25 de septiembre de 2026. De aquí salen elheaderSizede 284 sintypeIdOffset(«sección 5 · ResTable_package»), el literal?Ncon queaapt2rellena el hueco destyleableymacro(«sección 5 · ResTable_package»), el tratamiento dePP = 0como paquete dinámico y el límite deentryCount ≤ 0xFFFF(«sección 11 · La estructura del resource ID: 0xPPTTEEEE»).FeatureSetMetadata.ktdel Android Gradle Plugin (ramamirror-goog-studio-main) — https://android.googlesource.com/platform/tools/base/+/refs/heads/mirror-goog-studio-main/build-system/gradle-core/src/main/java/com/android/build/gradle/internal/tasks/featuresplit/FeatureSetMetadata.kt Consultado el 25 de septiembre de 2026. De aquí sale la asignación dePPa los módulos de funcionalidad a partir deBASE_ID = 0x7F(«sección 11 · La estructura del resource ID: 0xPPTTEEEE»).- bundletool 1.18.3 —
LanguageResourcesSplitter.java— https://github.com/google/bundletool/blob/master/src/main/java/com/android/tools/build/bundletool/splitters/LanguageResourcesSplitter.java Consultado el 25 de septiembre de 2026. De aquí sale quebundletooldivide los recursos por losConfigValuedelresources.pb(«sección 6 · ResTable_typeSpec»). aapt2 optimize --collapse-resource-namesde lasbuild-tools37.0.0 — ejecutado el 25 de septiembre de 2026 sobre un APK de prueba con dosdrawable. De aquí sale la ruta repetida de la receta de la «sección 14 · Recursos ofuscados».- AOSP —
ResourceTypes.hen dieciséis etiquetas, compilado — https://android.googlesource.com/platform/frameworks/base/+/refs/tags/android-3.2.4_r1/include/utils/ResourceTypes.h y las mismas rutas (include/androidfw/desdeandroid-4.1.1_r1,libs/androidfw/include/androidfw/desdeandroid-8.0.0_r1) hastaandroid-17.0.0_r1. Consultado el 25 de septiembre de 2026. De aquí sale el historial de tamaños de la «sección 8.2 · El problema del size variable», medido conclang++ -fsyntax-onlyparaaarch64yarmv7ade Android.