Alineación y zipalign

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

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:

  1. La entrada tiene que estar sin comprimir (stored). Un flujo deflate hay que descomprimirlo a algún sitio; no hay nada que mapear.
  2. El inicio de sus datos tiene que caer en un múltiplo del tamaño exigido. mmap mapea 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í:

«zipalign is 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 via mmap(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.arsc file 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 (.so files), use -P 16 to ensure that they're aligned to a 16KiB page boundary suitable for mmap(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 apksigner and make further changes to the APK, the APK's signature is invalidated. If you use zipalign to 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:

  1. Alinear mueve bytes. El relleno del extra field desplaza los datos de esa entrada y, con ellos, todo lo que va detrás: el resto de entradas, el Central Directory y el EOCD. El digest de v2 se calcula sobre el tramo 1 —del offset 0 al inicio del APK Signing Block—, el Central Directory y el EOCD (sección 6.1 de APK Signing Block). Cambiar el offset de cualquier entrada cambia el digest.
  2. zipalign reconstruye el ZIP y tira el APK Signing Block por el camino. No lo preserva ni lo intenta: copia entrada a entrada a un fichero nuevo, escribe un Central Directory nuevo y un EOCD nuevo. El hueco entre datos y Central Directory simplemente 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

  1. 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 16 de la sección 3.3 y la advertencia sobre el orden respecto a apksigner de la sección 4.
  2. 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 targetSdkVersion 35, la exigencia de alineación de los segmentos ELF y el comando zipalign -c -P 16 -v 4 de la sección 3.
  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.arsc vaya sin comprimir y alineado a 4 bytes de la sección 2.
  4. apksigner — Android Studio — https://developer.android.com/tools/apksigner Consultado el 12 de agosto de 2026. De aquí sale la advertencia «If you use zipalign to align your APK, use it before signing the APK» de la sección 4.
  5. 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.
  6. zipalign de build-tools 37.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.
  7. Corpus de verificación~/corpus-apk Medido el 12 de agosto de 2026 sobre los 58 APK standalone 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 de zipalign -c y los seis splits de Netflix que no cumplen.