La tabla de recursos: resources.arsc

referencia técnica · Revisado el 12 de agosto de 2026

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 todo el dominio. No existe ningún escritor de este formato en Rust: arsc está abandonado desde 2022 y resand es un proyecto personal. Sin escritor no hay editor, hay visor, y ya existen cinco visores. Ese es el motivo de que este sea el documento más largo del corpus.

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_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_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 impone y conviene tener presentes:

  • Un RES_TABLE_TYPE_TYPE tiene que venir después del RES_TABLE_TYPE_SPEC_TYPE de su mismo id. LoadedArsc.cpp aborta con RES_TABLE_TYPE_TYPE with ID %02x found without preceding RES_TABLE_TYPE_SPEC_TYPE.
  • Si aparecen dos RES_TABLE_TYPE_SPEC_TYPE con el mismo id, 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 corpus. Un APK normal tiene packageCount = 1 —así 557 de ellos—; el máximo observado es 6, en org.totschnig.myexpenses_858.apk, por sus bibliotecas compartidas dinámicas (sección 12.1).

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 corpus, 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).

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 corpus, 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 que el paquete hereda de otro
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 corpus
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 corpus
typeIdOffset 0x11c 4 uint32_t Desplazamiento a sumar a los id de tipo. 0 en todo el corpus

headerSize vale 288 (0x120), que es exactamente sizeof(ResTable_package). Y en todo el corpus 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) 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 una cadena vacía en el pool, hueco que deja aapt2 cuando un tipo se declara y luego no se usa. 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), 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. Es la información con la que bundletool decide qué entra en config.es.apk y qué se queda en base.apk, y la 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). 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 corpus 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 rama main y aapt2 solo lo emite cuando ya está usando entradas compactas (FLAG_COMPACT, sección 10.2) y los offsets caben en 16 bits. No se ha observado ni una sola vez en el corpus: 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, tres condiciones simultáneas: que se haya pedido con --enable-sparse-encoding; que minSdkVersion >= 32 (SDK_S_V2) o que no haya minSdk ni sdkVersion en el contexto; 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 corpus: 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 rama main:

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 Sin significado definido. Debe ser 0
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
(relleno) 0x3d 3 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: 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:

void ResTable_config::copyFromDeviceNoSwap(const ResTable_config& o) {
    const size_t size = dtohl(o.size);
    if (size >= sizeof(ResTable_config)) {
        *this = o;
    } else {
        memcpy(this, &o, size);
        memset(((uint8_t*)this)+size, 0, sizeof(ResTable_config)-size);
    }
}

Es decir:

Se leen exactamente size bytes y el resto de la estructura se rellena a cero. Si size es 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, derivado de los campos presentes en ResourceTypes.h en cada etiqueta de AOSP:

size Último campo añadido Presente desde
36 screenSizeDp android-4.4_r1
48 localeScript, localeVariant android-5.0.0_r1
52 screenConfig2 android-6.0.0_r1
56 localeScriptWasComputed android-7.1.2_r36
64 localeNumberingSystem android-9.0.0_r1 y posteriores, incluida main

⚠️ sin verificar: los valores de la columna size están calculados a partir de la lista de campos de cada etiqueta y de la regla de alineación a 4 bytes de C, no leídos de un static_assert de la cabecera. Los dos que sí están medidos son 56 y 64, que son los únicos que aparecen en el corpus.

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 bigendian 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) 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, en igualdad, la que se pueda reducir en vez de ampliar: si ni la propia ni la otra coinciden con la pedida, gana la mayor. DENSITY_ANY gana siempre. Si no se pide densidad, se asume DENSITY_MEDIUM (160).
  • Dimensiones en dp. Gana la que minimiza la suma de pedido - candidata en anchura y altura, asumiendo que match() 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 -v31 sustituya al -v21 en 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

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 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 corpus: 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 es inválido
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.536 entradas por tipo, que LoadedArsc.cpp comprueba explícitamente.

Los valores de PP:

PP Quién
0x00 Paquete sin identificador asignado: es lo que aapt2 link --shared-lib emite, y el sistema le asigna uno real al cargar
0x01 kFrameworkPackageId — el framework de Android, android.R
0x020x7e 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
0x800xff Bibliotecas compartidas dinámicas y overlays. 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 0x020x7e y del FeatureSplitSymbolTableDelegate que reescribe identificadores cuando minSdkVersion < 26.

En el corpus 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 bibliotecas dinámicas suyas.

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 corpus.

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 corpus. 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 corpus:

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 corpus, 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 corpus: 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.

13. Qué entender y qué conservar en crudo

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

La lista de la izquierda no es arbitraria: son los cinco tipos que hay que tocar para fusionar dos tablas y para renombrar entradas, que son las dos mutaciones básicas. Todo lo demás se copia.

Dos matices que el reparto no debe ocultar:

  • Conservar en crudo no es ignorar. Si merge reasigna el PP de un paquete, los ResTable_ref que viven dentro de un RES_TABLE_OVERLAYABLE_POLICY_TYPE dejan 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.

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 keyno 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 corpus, 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; el renombrado de claves, en ninguna de las ocho aplicaciones medidas, incluidas las que tienen el DEX ofuscado al 99,9 % o pasan por DexGuard. Es un hallazgo con consecuencias directas: 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. Reconstruirla es por tanto deofuscación determinista y sin riesgo, al mismo nivel que aplicar un mapping.txt.

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 corpus no contiene ninguna muestra con keyStrings ofuscado, así que no se ha podido observar qué aspecto tiene ni comprobar si hay señales que permitan detectarlo con fiabilidad. La cifra más alta medida es el 0,5 % de claves de tres caracteres o menos de F-Droid y Termux, que corresponde a nombres legítimamente cortos (id, tag, top).

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
CORPUS=~/corpus-apk
$BT/aapt2 dump resources $CORPUS/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 $CORPUS/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 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 corpus, 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 app.apk
# los recursos decodificados quedan en salida/res/, en XML de texto
apktool d -r -f -o salida app.apk
# -r deja resources.arsc intacto y no decodifica nada de res/

⚠️ no ejecutado localmente: apktool no está instalado en la máquina de referencia. La sintaxis procede de la documentación oficial de parámetros de la CLI (fuente 8).

Fuentes

  1. frameworks/base/libs/androidfw/include/androidfw/ResourceTypes.h (rama main) — https://android.googlesource.com/platform/frameworks/base/+/refs/heads/main/libs/androidfw/include/androidfw/ResourceTypes.h Consultado el 12 de agosto de 2026, descargado con ?format=TEXT y decodificado. De aquí salen íntegras las tablas de campos de las secciones 3, 5, 6, 7, 8.1, 10, 11, 12.1, 12.2 y 12.3, las constantes SPEC_*, FLAG_*, ATTR_*, TYPE_*, PolicyFlags, las macros Res_MAKEID/Res_GETPACKAGE/Res_GETTYPE/Res_GETENTRY, kResTableTypeMinSize, offset_from16 y el static_assert de que ResTable_config va al final de ResTable_type.
  2. frameworks/base/libs/androidfw/ResourceTypes.cpp (rama main) — https://android.googlesource.com/platform/frameworks/base/+/refs/heads/main/libs/androidfw/ResourceTypes.cpp Consultado el 12 de agosto de 2026. De aquí salen ResTable_config::copyFromDeviceNoSwap (sección 8.2), unpackLanguageOrRegion y packLanguageOrRegion (sección 8.3), y ResTable_config::match(), isBetterThan(), isLocaleBetterThan() e isMoreSpecificThan() (sección 9).
  3. frameworks/base/libs/androidfw/LoadedArsc.cpp (rama main) — https://android.googlesource.com/platform/frameworks/base/+/refs/heads/main/libs/androidfw/LoadedArsc.cpp Consultado el 12 de agosto de 2026. De aquí salen VerifyResTableType, VerifyResTableEntry (sección 10.3), GetEntryOffset con la búsqueda binaria sobre las entradas dispersas (sección 7.3), el límite de 65.536 entradas por tipo (sección 11) y las reglas de orden entre typeSpec y type de la sección 2.
  4. frameworks/base/tools/aapt2/Resource.h (rama main) — https://android.googlesource.com/platform/frameworks/base/+/refs/heads/main/tools/aapt2/Resource.h Consultado el 12 de agosto de 2026. De aquí salen kAppPackageId = 0x7f, kFrameworkPackageId = 0x01 y la descripción canónica de 0xPPTTEEEE de la sección 11.
  5. frameworks/base/tools/aapt2/cmd/Link.cpp (rama main) — https://android.googlesource.com/platform/frameworks/base/+/refs/heads/main/tools/aapt2/cmd/Link.cpp Consultado el 12 de agosto de 2026. De aquí salen la validación del rango de --package-id, el 0x00 que asigna --shared-lib, el rango reservado 0x020x7e de --allow-reserved-package-id y la explicación del comportamiento previo a Android 8, todo en la sección 11.
  6. frameworks/base/tools/aapt2/format/binary/TableFlattener.cpp y TableFlattener.h (rama main) — https://android.googlesource.com/platform/frameworks/base/+/refs/heads/main/tools/aapt2/format/binary/TableFlattener.cpp Consultado el 12 de agosto de 2026. De aquí salen las condiciones exactas con que aapt2 emite FLAG_SPARSE, FLAG_OFFSET16 y FLAG_COMPACT, y la constante kSparseEncodingThreshold = 60 de las secciones 7.2, 7.3 y 10.2.
  7. frameworks/base/include/androidfw/ResourceTypes.h en las etiquetas android-4.4_r1, android-5.0.0_r1, android-6.0.0_r1, android-7.1.2_r36, android-9.0.0_r1 y android-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 de ResTable_config de la tabla de la sección 8.2.
  8. 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, que no se ha podido ejecutar.
  9. Corpus de verificación — 5,9 GB, 86 contenedores y 717 APK: 58 sueltos de F-Droid y 659 dentro de contenedores de splits. Medido el 12 de agosto de 2026 con aapt2 2.20-15087165 y un volcador propio en python3 sobre los 58 APK standalone y los 505 APK internos con resources.arsc. De aquí salen todas las cifras y volcados de las secciones 3, 4, 5, 6, 7.1 a 7.3, 8.2, 8.3, 10.1, 10.2, 11, 12, 14 y 15.