Alineación y zipalign
1. Qué es y por qué existe
Un APK instalado no se desempaqueta: se queda como está en /data/app/…, y el sistema lee
de él durante toda la vida de la aplicación. Eso abre una optimización grande: en vez de
copiar resources.arsc o una biblioteca nativa a RAM, el sistema puede mapearlos en memoria
con mmap(2) directamente desde el fichero. La página de memoria virtual apunta al APK en
disco, sin copia intermedia y sin coste al arrancar.
Para que eso funcione hacen falta dos condiciones a la vez:
- La entrada tiene que estar sin comprimir (
stored). Un flujo deflate hay que descomprimirlo a algún sitio; no hay nada que mapear. - El inicio de sus datos tiene que caer en un múltiplo del tamaño exigido.
mmapmapea desde un offset alineado; si los datos empiezan a mitad de página, el mapeo no puede apuntar directamente al contenido.
La documentación oficial de zipalign —la herramienta que lo consigue,
glosario— lo formula así:
«
zipalignis a zip archive alignment tool that helps ensure that all uncompressed files in the archive are aligned relative to the start of the file. This lets the files be accessed directly viammap(2), removing the need to copy this data in RAM and reducing your app's memory usage.»
Qué se beneficia y qué no:
| Entrada | Método | ¿Se alinea? | Motivo |
|---|---|---|---|
resources.arsc |
stored |
Sí, a 4 bytes | Se mapea. Obligatorio desde targetSdk 30 |
lib/<abi>/*.so con extractNativeLibs=false |
stored |
Sí, a 4 o 16 KiB | Se carga desde el APK, sin extraer |
classes*.dex |
deflate |
No | Comprimido: no se mapea desde el APK |
res/*.xml, assets/* comprimidos |
deflate |
No | Ídem |
.png, .webp, .ogg ya comprimidos |
stored |
Sí, a 4 bytes | Van stored, así que entran en la regla general |
Las entradas comprimidas no se alinean nunca, y no por descuido: alinear un flujo deflate
no aporta nada porque de todos modos hay que descomprimirlo. zipalign las copia tal cual y
las marca (OK - compressed) al comprobar.
Cómo se consigue la alineación está descrito en la sección 8.1 de
Contenedor ZIP y no se repite aquí. En una frase:
el único grado de libertad para mover el inicio de los datos es el extra field del
Local File Header, porque
offset_datos = offset_LFH + 30 + file_name_length + extra_field_length
y zipalign engorda ese extra field con ceros hasta que offset_datos cae donde debe. Ni
reordena entradas ni recomprime nada.
2. La alineación de 4 bytes
Es la regla general y la que aplica zipalign 4: toda entrada stored empieza en un
múltiplo de 4 bytes. El motivo es el acceso alineado a la palabra: resources.arsc es una
jerarquía de chunks cuyas cabeceras se leen como uint32, y leerlas desde una dirección no
alineada es, en el mejor caso, más lento, y en algunas arquitecturas, un fallo.
Desde targetSdkVersion 30 dejó de ser una optimización para ser un requisito de instalación:
«Apps that target Android 11 (API level 30) or higher can't be installed if they contain a compressed
resources.arscfile or if this file is not aligned on a 4-byte boundary.»
El incumplimiento produce INSTALL_PARSE_FAILED_RESOURCES_ARSC_COMPRESSED, que es el fallo de
reempaquetado más frecuente del ecosistema.
Medido, sobre org.fossify.calculator_1.4.0.apk, entrada assets/dexopt/baseline.prof:
LFH = 325 file_name_length = 27 extra_field_length (LFH) = 2 → datos en 384
325 + 30 + 27 + 2 = 384 = 4 × 96
Dos bytes de relleno, los mínimos para llegar al siguiente múltiplo de 4. Y la entrada
equivalente del Central Directory declara extra field length = 0: el relleno vive solo
en el Local File Header, que es la asimetría que hace que zipinfo no lo enseñe.
3. La regla de las páginas de 16 KB
3.1 Qué es
Los dispositivos Android han usado siempre páginas de memoria de 4 KiB. Los nuevos
dispositivos de 64 bits pueden usar páginas de 16 KiB, lo que mejora el rendimiento pero
rompe el supuesto de todo binario nativo compilado dando por hecho los 4 KiB. Para que una
biblioteca .so se pueda mapear en un dispositivo de páginas de 16 KiB hacen falta dos cosas
distintas que se confunden:
| Requisito | Dónde vive | Cómo se comprueba |
|---|---|---|
Alineación dentro del APK |
Offset de los datos de la entrada en el ZIP |
zipalign -c -P 16 -v 4 |
Alineación de los segmentos LOAD del ELF |
Dentro del propio .so |
Inspección del ELF: los LOAD no deben tener alineación menor que 2**14 |
zipalign solo comprueba el primero. El segundo es competencia de
Código nativo y ELF, y la documentación de Google es
explícita: «16 KB devices require the shared libraries' ELF segments to be aligned properly
using 16 KB ELF alignment in order for your app to run».
Un APK puede pasar zipalign -c -P 16 y seguir siendo incompatible con 16 KB, porque
sus .so tengan segmentos LOAD alineados a 2**12. Los dos controles son necesarios y
ninguno basta.
3.2 Desde cuándo y a qué afecta
De developer.android.com, literal:
«To ensure your app works correctly on the latest versions of Android, all apps targeting Android 15 (API level 35) and higher must support 16 KB memory page sizes on 64-bit devices on Google Play. Starting February 1, 2027, if your app updates don't support 16 KB memory page sizes, you won't be able to release these updates.»
Resumido:
| A qué apps afecta | targetSdkVersion ≥ 35, en dispositivos de 64 bits |
| Quién lo exige | Google Play, no la plataforma |
| Desde cuándo | 1 de febrero de 2027 para las actualizaciones |
| Qué pasa si no se cumple | No se puede publicar la actualización en Play |
La distinción «Play, no la plataforma» importa para el análisis: un APK que no cumpla se
instala igual con adb install o desde F-Droid, y puede fallar en tiempo de ejecución sobre
un dispositivo de 16 KiB, pero no lo bloquea el instalador. Es una regla de publicación.
⚠️ La fecha y el alcance están tomados literalmente de developer.android.com/guide/practices/page-sizes
el 12 de agosto de 2026. Lo que no se ha podido confirmar es qué ocurre exactamente con las
aplicaciones ya publicadas que no se actualicen después de esa fecha; la redacción habla solo
de «app updates».
3.3 El comando
«If your APK contains shared libraries (
.sofiles), use-P 16to ensure that they're aligned to a 16KiB page boundary suitable formmap(2)in both 16KiB and 4KiB devices.»
El flag -P sustituye al antiguo -p, que fijaba 4 KiB. La ayuda de la herramienta local:
Usage: zipalign [-f] [-p] [-P <pagesize_kb>] [-v] [-z] <align> infile.zip outfile.zip
zipalign -c [-p] [-P <pagesize_kb>] [-v] <align> infile.zip
<align>: alignment in bytes, e.g. '4' provides 32-bit alignment
-c: check alignment only (does not modify file)
-f: overwrite existing outfile.zip
-p: 4kb page-align uncompressed .so files
-v: verbose output
-z: recompress using Zopfli
-P <pagesize_kb>: Align uncompressed .so files to the specified
page size. Valid values for <pagesize_kb> are 4, 16
and 64. '-P' cannot be used in combination with '-p'.
El argumento posicional 4 sigue siendo obligatorio: es la alineación general de la sección 2.
-P 16 añade una regla más estricta solo para los .so sin comprimir. Y el valor de -P
está en kilobytes, no en bytes: -P 16 significa 16.384.
4. El orden importa: alinear ANTES de firmar
Es la regla número uno del empaquetado y la fuente número uno de APK rotos. La formulación
oficial:
«Caution: If you sign your APK using
apksignerand make further changes to the APK, the APK's signature is invalidated. If you usezipalignto align your APK, use it before signing the APK.»
4.1 Por qué exactamente
Dos motivos que actúan en cascada, y los dos son consecuencia directa de que v2 firma bytes, no entradas:
- Alinear mueve bytes. El relleno del
extra fielddesplaza los datos de esa entrada y, con ellos, todo lo que va detrás: el resto de entradas, elCentral Directoryy elEOCD. El digest de v2 se calcula sobre el tramo 1 —del offset 0 al inicio delAPK Signing Block—, elCentral Directoryy elEOCD(sección 6.1 de APK Signing Block). Cambiar el offset de cualquier entrada cambia el digest. zipalignreconstruye elZIPy tira elAPK Signing Blockpor el camino. No lo preserva ni lo intenta: copia entrada a entrada a un fichero nuevo, escribe unCentral Directorynuevo y unEOCDnuevo. El hueco entre datos yCentral Directorysimplemente no se reproduce.
El segundo motivo hace que el primero ni llegue a notarse: no es que la firma quede inválida,
es que desaparece. Y ahí entra la protección contra el downgrade de la sección 5.2 de
Firma v4 y verificación: el .SF de v1, que sí sobrevive
porque es una entrada del ZIP normal, sigue declarando X-Android-APK-Signed: 2, 3. El
verificador ve esa declaración, no encuentra los bloques, y rechaza el fichero acusando de
manipulación.
4.2 La demostración
Sobre copias en el scratchpad. firmado.apk es la copia de org.fossify.calculator_1.4.0.apk
firmada con v1+v2+v3 en la sección 10.2 de
Firma v4 y verificación, y ya está alineada:
BT=~/Library/Android/sdk/build-tools/37.0.0
SCR=/tmp/scratch/firma
$BT/zipalign -c -v 4 $SCR/firmado.apk | tail -2
6995207 META-INF/MANIFEST.MF (OK - compressed)
Verification successful
Se le pasa zipalign de todos modos, que es el error:
$BT/zipalign -f -p 4 $SCR/firmado.apk $SCR/firmado_alineado.apk
ls -la $SCR/firmado.apk $SCR/firmado_alineado.apk
$BT/apksigner verify --verbose $SCR/firmado_alineado.apk
-rw-r--r--@ 1 jj wheel 7170968 12 ago. 12:26 firmado_alineado.apk
-rw-r--r--@ 1 jj wheel 7176562 12 ago. 12:25 firmado.apk
DOES NOT VERIFY
ERROR: JAR signer PRUEBAS.RSA: JAR signature META-INF/PRUEBAS.SF indicates the APK is signed
using APK Signature Scheme v2 but no such signature was found. Signature stripped?
ERROR: JAR signer PRUEBAS.RSA: JAR signature META-INF/PRUEBAS.SF indicates the APK is signed
using APK Signature Scheme v3 but no such signature was found. Signature stripped?
zipalign devolvió 0: para él la operación fue un éxito. El APK resultante mide 5.594 bytes
menos, de los cuales 4.096 son el APK Signing Block que ha desaparecido y el resto, relleno
de alineación recalculado. El volcador de la sección 9 de
APK Signing Block lo confirma:
python3 dump_sigblock.py $SCR/firmado.apk $SCR/firmado_alineado.apk
== firmado.apk
inicio del bloque : 7057408 (multiplo de 4096: True)
offset del CD : 7061504 total del bloque: 4096
0x7109871a 1536 bytes v2 (APK_SIGNATURE_SCHEME_V2_BLOCK_ID)
0xf05368c0 1536 bytes v3 (APK_SIGNATURE_SCHEME_V3_BLOCK_ID)
0x42726577 956 bytes padding (VERITY_PADDING_BLOCK_ID)
firmado_alineado.apk: SIN APK Signing Block (CD en 7055910)
Los mensajes de error mienten sobre la causa. «Signature stripped?» suena a ataque; aquí es
un zipalign ejecutado un paso tarde. Es el diagnóstico erróneo más común del ecosistema.
4.3 El orden correcto
$BT/zipalign -f -p 4 $SCR/copia.apk $SCR/alineado.apk
$BT/apksigner sign --ks $SCR/pruebas.jks --ks-key-alias pruebas \
--ks-pass pass:pruebas --key-pass pass:pruebas \
--out $SCR/alineado_firmado.apk $SCR/alineado.apk
$BT/apksigner verify --verbose $SCR/alineado_firmado.apk | head -4
$BT/zipalign -c -v 4 $SCR/alineado_firmado.apk | tail -1
$BT/zipalign -c -P 16 4 $SCR/alineado_firmado.apk; echo "exit=$?"
Verifies
Verified using v1 scheme (JAR signing): false
Verified using v2 scheme (APK Signature Scheme v2): true
Verified using v3 scheme (APK Signature Scheme v3): true
Verification successful
exit=0
Alineado y firmado, y las dos comprobaciones pasan. El resultado mide exactamente lo mismo que
firmado.apk, 7.176.562 bytes: la cadena es determinista.
Regla operativa: zipalign es lo último antes de firmar, y apksigner sign es lo último
del todo. Ninguna herramienta toca el APK después.
5. zipalign -c como verificador
-c comprueba sin modificar. Devuelve 0 si todo cumple y 1 si algo no, lo que lo hace
utilizable en un script. Con -v imprime una línea por entrada con el offset de los
datos, no el del Local File Header.
| Comando | Qué comprueba | Qué reporta |
|---|---|---|
zipalign -c -v 4 |
Toda entrada stored en múltiplo de 4 |
(OK), (OK - compressed) o (BAD - <resto>) |
zipalign -c -P 16 -v 4 |
Lo anterior más los .so stored en múltiplo de 16.384 |
Ídem |
El (BAD - n) no es un código de error: es el resto de la división del offset entre la
alineación exigida, o sea, cuántos bytes sobran.
5.1 Un caso que no cumple
Buscado en todo el corpus: los 58 APK standalone y los 659 internos de los contenedores.
CORPUS=~/corpus-apk
SCR=/tmp/scratch
# 1) los 58 standalone
for f in $CORPUS/*.apk; do
$BT/zipalign -c -P 16 4 "$f" >/dev/null 2>&1 || echo "FALLA16 $(basename $f)"
done
# 2) los 659 internos, extraidos antes a $SCR/inner/<contenedor>/
find $SCR/inner -name '*.apk' | while read f; do
$BT/zipalign -c -P 16 4 "$f" >/dev/null 2>&1 || echo "FALLA16 $f"
done
Los 58 standalone pasan, con -c 4 y con -c -P 16 4. Los internos, no: seis splits del
contenedor SAI de Netflix fallan la comprobación de 16 KB, y son precisamente los de
arquitectura.
FALLA16 inner/com.netflix.mediaclient.sai.zip/split_config.armeabi_v7a.apk
FALLA16 inner/com.netflix.mediaclient.sai.zip/split_config.x86_64.apk
FALLA16 inner/com.netflix.mediaclient.sai.zip/split_voip.config.armeabi_v7a.apk
FALLA16 inner/com.netflix.mediaclient.sai.zip/split_config.x86.apk
FALLA16 inner/com.netflix.mediaclient.sai.zip/split_voip.config.arm64_v8a.apk
FALLA16 inner/com.netflix.mediaclient.sai.zip/split_config.arm64_v8a.apk
El detalle, con -v:
$BT/zipalign -c -P 16 -v 4 inner/com.netflix.mediaclient.sai.zip/split_config.arm64_v8a.apk
Verifying alignment of …/split_config.arm64_v8a.apk (4)...
49 AndroidManifest.xml (OK - compressed)
4096 lib/arm64-v8a/libavif_android.so (BAD - 4096)
851968 lib/arm64-v8a/libbugsnag-ndk.so (OK)
2039808 lib/arm64-v8a/libbugsnag-plugin-android-anr.so (BAD - 8192)
2056192 lib/arm64-v8a/libbugsnag-root-detection.so (BAD - 8192)
2064384 lib/arm64-v8a/libcronet.101.0.4951.41.so (OK)
6946816 lib/arm64-v8a/libe5e7.so (OK)
7974912 lib/arm64-v8a/libtensorflowlite_jni_gms_client.so (BAD - 12288)
8523287 stamp-cert-sha256 (OK - compressed)
8523372 META-INF/BNDLTOOL.SF (OK - compressed)
8523912 META-INF/BNDLTOOL.RSA (OK - compressed)
8524623 META-INF/MANIFEST.MF (OK - compressed)
Verification FAILED
Código de salida: 1.
Y el mismo fichero, con la alineación general de 4 bytes:
$BT/zipalign -c -v 4 inner/com.netflix.mediaclient.sai.zip/split_config.arm64_v8a.apk
Verifying alignment of …/split_config.arm64_v8a.apk (4)...
49 AndroidManifest.xml (OK - compressed)
4096 lib/arm64-v8a/libavif_android.so (OK)
851968 lib/arm64-v8a/libbugsnag-ndk.so (OK)
2039808 lib/arm64-v8a/libbugsnag-plugin-android-anr.so (OK)
…
Código de salida: 0. Las mismas entradas, dos veredictos opuestos. Este APK está alineado
a 4 KiB —libavif_android.so empieza en 4096 exacto— y eso era lo correcto cuando se
construyó; lo que no cumple es la regla nueva de 16 KiB.
Los offsets, medidos directamente sobre el ZIP, enseñan de dónde salen los (BAD - n):
| Entrada | Offset de datos | mód 4096 | mód 16384 |
|---|---|---|---|
libavif_android.so |
4.096 | 0 | 4.096 |
libbugsnag-ndk.so |
851.968 | 0 | 0 |
libbugsnag-plugin-android-anr.so |
2.039.808 | 0 | 8.192 |
libbugsnag-root-detection.so |
2.056.192 | 0 | 8.192 |
libtensorflowlite_jni_gms_client.so |
7.974.912 | 0 | 12.288 |
El número entre paréntesis del (BAD - n) es exactamente la última columna.
5.2 La trampa de interpretar el resultado
com.termux_1002.apk, cuyas .so van comprimidas, también da Verification successful
con -P 16. No es que cumpla la regla de 16 KB: es que no tiene ninguna entrada a la que la
regla se aplique.
113795036 META-INF/84D3000E.RSA (OK - compressed)
113796166 META-INF/MANIFEST.MF (OK - compressed)
Verification successful
zipalign -c no responde «¿cumple este APK la regla de 16 KB?». Responde «¿están alineadas
las entradas que debían estarlo?». Son preguntas distintas y la segunda es más débil: un
APK con extractNativeLibs=true pasa siempre, porque no tiene .so sin comprimir, y aun así
puede ser incompatible con dispositivos de 16 KiB por la alineación de sus segmentos ELF
(sección 3.1).
Fuentes
- zipalign — Android Studio — https://developer.android.com/tools/zipalign
Consultado el 12 de agosto de 2026. De aquí salen la cita sobre
mmap(2)de la sección 1, la recomendación de-P 16de la sección 3.3 y la advertencia sobre el orden respecto aapksignerde la sección 4. - Support 16 KB page sizes — Android Developers — https://developer.android.com/guide/practices/page-sizes
Consultado el 12 de agosto de 2026. De aquí salen la fecha del 1 de febrero de 2027, el
umbral de
targetSdkVersion35, la exigencia de alineación de los segmentos ELF y el comandozipalign -c -P 16 -v 4de la sección 3. - Behavior changes: Apps targeting Android 11 — Compressed resource files — https://developer.android.com/about/versions/11/behavior-changes-11
Consultado el 12 de agosto de 2026. De aquí sale citada la obligación de que
resources.arscvaya sin comprimir y alineado a 4 bytes de la sección 2. - apksigner — Android Studio — https://developer.android.com/tools/apksigner
Consultado el 12 de agosto de 2026. De aquí sale la advertencia «If you use
zipalignto align your APK, use it before signing the APK» de la sección 4. - AOSP —
build/tools/zipalign/ZipAlign.cpp— https://github.com/aosp-mirror/platform_build/blob/master/tools/zipalign/ZipAlign.cpp Consultado el 12 de agosto de 2026. De aquí sale la confirmación de que la alineación se decide por extensión de fichero y de que las entradas comprimidas se copian sin alinear, citada en la sección 1. zipaligndebuild-tools37.0.0 — ejecutado el 12 de agosto de 2026 en esta máquina. De aquí sale el texto de ayuda de la sección 3.3 y todas las salidas de las secciones 4 y 5.- Corpus de verificación —
~/corpus-apkMedido el 12 de agosto de 2026 sobre los 58APKstandalone y los 659 internos de los 28 contenedores. De aquí salen las mediciones de offsets de las secciones 2 y 5.1, el barrido completo dezipalign -cy los seis splits de Netflix que no cumplen.