Skip to content
Rust & NativeRust y nativo

C++ on Android: When and Why to Use ItC++ en Android: cuándo y por qué usarlo

ByPor David Cruz Anaya, Senior Mobile & Applied AI Engineer (Android, Kotlin Multiplatform, product AI)Ingeniero sénior de móvil e IA aplicada (Android, Kotlin Multiplatform, IA de producto)

Updated 20 June 2026Actualizado el 20 de junio de 20269 min read9 min de lectura
AndroidC++NDKJNINative DevelopmentKotlinPerformance
C++ on Android: When and Why to Use It
Share:Compartir:

Most Android apps never need a single line of C++. Kotlin, coroutines, and the Android Runtime handle the vast majority of apps.

That slice is maybe twenty percent of the problems I have run into. Recognize it early, before you have burned a sprint optimizing Kotlin that was never going to be fast enough.

What “C++ on Android” Means

When people say they are “using C++ on Android,” they mean the Android NDK, the Native Development Kit. The NDK is a toolchain that compiles C and C++ into shared libraries, the .so files that ship inside your APK. Your Kotlin code then calls into those libraries through JNI, the Java Native Interface.

So the architecture is always two layers. Kotlin owns the UI, the lifecycle, and the boring glue. C++ owns one focused, performance-critical job underneath. The JNI bridge is the crossing between them.

The NDK ships its own C++ standard library, LLVM’s libc++, which has been the only STL option since NDK r18. You usually build with CMake, though the older ndk-build is still supported, and you ship a separate .so for each CPU architecture you support, usually arm64-v8a, armeabi-v7a, and x86_64 for emulators.

When Should You Reach for C++?

Reach for C++ when the JVM is the bottleneck, not your code. There are five situations where that is reliably true, and they cover almost every legitimate use of the NDK I have seen.

Real-time audio. The Android Runtime’s garbage collector can pause your thread at any moment, and an audio callback that has to deliver samples every five to ten milliseconds cannot survive a pause. Google’s guidance for low-latency audio is to keep the real-time path out of the JVM and in native C++, precisely to dodge those GC pauses. Java has a low-latency AudioTrack mode for less demanding cases, but when you need the floor, native is the answer. A typical unoptimized Java audio path lands somewhere between 100 and 250 milliseconds, while a native path through AAudio and Oboe gets under 20 on devices that support the low-latency path. I wrote a full breakdown of how that works in my modern C++ for Android audio guide, and it is the same reason I built Klarinet, a low-latency audio SDK on top of these APIs.

Game engines and graphics. Anything running a tight render loop, a physics simulation, or direct OpenGL ES and Vulkan calls lives in C++. Unity, Unreal, and Godot all run their cores natively for exactly this reason: you cannot afford a GC pause between frames at 60 or 120 fps.

Heavy compute. Digital signal processing, computer vision pre- and post-processing, encryption, codecs, and the number-crunching glue around an on-device ML model. These are tight loops over big buffers where you want SIMD instructions and predictable memory, not an interpreter deciding when to collect. This is also where RenderScript used to live; Google deprecated it in Android 12 and now points these workloads at the GPU or at native C++ with SIMD, which is part of why they land in the NDK today.

Reusing an existing C or C++ library. If a mature library already exists in C++, FFmpeg, OpenCV, libsodium, a vendor’s C SDK, rewriting it in Kotlin would be reckless. You wrap it instead.

Sharing a core across platforms. A C++ core compiles for Android and iOS and desktop from one codebase. I covered the iOS half of this in how to use C++ code in your iOS and tvOS Xcode project. Write the engine once, expose a thin platform-specific surface on each side, and you maintain one implementation instead of three.

This is not the same as Kotlin Multiplatform. KMP shares your Kotlin business logic across platforms and is the right default for that, but it still runs on a garbage-collected runtime, so it does not replace C++ in the realtime, SIMD, lock-free slice. In practice they work together: a KMP app wraps a C++ core, which is exactly how I built Klarinet.

Why C++ Wins in Those Cases

C++ gives you control the managed runtime deliberately takes away.

There is no garbage collector to pause your thread at the worst possible moment, so a real-time loop runs when you tell it to. You also get direct memory control, which means you can lay data out cache-friendly, avoid allocations in the hot path, and reach for SIMD intrinsics like ARM NEON to process many samples or pixels per instruction. None of that is available to you through the JVM.

Then there is the ecosystem. Decades of mature, well-tested C and C++ libraries exist for audio, video, math, and cryptography, and the NDK lets you use them directly. The same DSP code that powers your Android audio engine also runs on iOS, so you verify it once and fix it once.

The costs

The JNI boundary is not free. Every call across JNI marshals data between the managed and native worlds, and that has a cost. Cross the boundary rarely, and move a lot each time, typically through a direct ByteBuffer that both sides share without copying. Chatty JNI, thousands of tiny calls, will erase the very performance you went native to get.

The build gets heavier. You now own a CMake configuration, a JNI bridge, and a toolchain that is fussier than Gradle alone. If you are still wrestling with the Kotlin build, see my Groovy-to-Kotlin DSL Gradle guide, because native builds assume you are already comfortable there.

Debugging is harder. A native crash does not give you a friendly Kotlin stack trace. You learn ndk-stack to symbolicate native crash dumps, and you get familiar with signals like SIGSEGV instead of exceptions.

You take on memory-safety risk. Kotlin and the JVM are memory-safe by construction. C++ hands back buffer overflows, use-after-free, and integer overflow, and on Android those are not just crashes, they are exploitable security vulnerabilities. Bundled native libraries like FFmpeg or OpenSSL also make their CVEs your patching problem. My Rust toolchain setup guide walks the same NDK pipeline with cargo-ndk instead of CMake.

Your APK grows. You ship a .so per architecture, so supporting arm64-v8a, armeabi-v7a, and x86_64 triples that slice of a universal build. App Bundles, mandatory on Google Play, split by ABI so each device downloads only its own .so, which puts the real on-device cost closer to one architecture than three. The native payload is still bigger than zero.

The 16 KB page-size rule now applies to you. Since November 1, 2025, new apps and updates on Google Play that target Android 15 (API 35) or higher must support 16 KB memory pages on 64-bit devices, and Google extended the Play Console enforcement deadline to May 31, 2026. Pure Kotlin and Java apps already comply for free. The moment you ship native code, you inherit this requirement and must rebuild your .so files with proper alignment. NDK r28 and later do this by default; on r27 you have to opt in with explicit linker flags. It is usually just a rebuild, but watch the transitive case: a stale prebuilt .so buried in a third-party AAR can fail the check even when your own code is clean, so treat it as a dependency audit, not just a flag flip.

Maintenance. Two languages, two mental models, two sets of gotchas. Every engineer who touches that part of the codebase needs to be comfortable in both.

When to stay in Kotlin

Do not use C++ for a normal app. If you are building CRUD screens, talking to a REST API, rendering lists, and handling forms, Kotlin with coroutines on the Android Runtime is the right tool. The runtime has had years of optimization.

Be especially wary of premature optimization. “It might be faster in C++” is not a reason; it is a hypothesis, and an expensive one to test. Profile first. Most performance problems on Android are a bad database query, an image loaded on the main thread, or an over-rendering Compose tree, none of which native code fixes. If you have not measured, you are not optimizing, you are guessing in a harder language.

How Do You Decide If Code Belongs in C++?

When I am deciding whether a piece of work belongs in C++, I run it through four questions. If the answer to all four is yes, go native. If any is no, stay in Kotlin and revisit only when the data forces you to.

  1. Is the JVM itself the bottleneck? Real-time constraints, GC pauses, or raw compute that the runtime cannot meet, not just code you have not optimized yet.
  2. Have you profiled and confirmed it? You have numbers, not a hunch, showing managed code cannot hit the target.
  3. Does a mature native solution already exist, or must you share this core with iOS? Reuse and cross-platform sharing justify the bridge on their own.
  4. Can you afford the lifetime cost? The build complexity, the debugging, and the maintenance, for as long as the app lives.

Resources

La mayoría de las aplicaciones Android no necesitan nunca ni una línea de C++. Kotlin, las corrutinas y el Android Runtime se ocupan de la inmensa mayoría de las aplicaciones.

Esa franja es quizá el veinte por ciento de los problemas con los que me he encontrado. Reconócela pronto, antes de haberte quemado un sprint optimizando un Kotlin que nunca iba a ser lo bastante rápido.

Qué significa «C++ en Android»

Cuando alguien dice que está «usando C++ en Android», se refiere al NDK de Android, el Native Development Kit. El NDK es una cadena de herramientas que compila C y C++ a bibliotecas compartidas, los archivos .so que viajan dentro de tu APK. Tu código Kotlin llama después a esas bibliotecas a través de JNI, la Java Native Interface.

Así que la arquitectura son siempre dos capas. Kotlin lleva la interfaz, el ciclo de vida y el pegamento aburrido. C++, por debajo, lleva un trabajo concreto y crítico para el rendimiento. El puente JNI es el cruce entre las dos capas.

El NDK trae su propia biblioteca estándar de C++, la libc++ de LLVM, que es la única opción de STL desde el NDK r18. Lo normal es compilar con CMake, aunque el ndk-build más antiguo sigue estando soportado, y publicas un .so distinto por cada arquitectura de CPU a la que das soporte, normalmente arm64-v8a, armeabi-v7a y x86_64 para los emuladores.

¿Cuándo deberías echar mano de C++?

Echa mano de C++ cuando el cuello de botella es la JVM, no tu código. Hay cinco situaciones en las que eso es cierto de forma fiable, y cubren casi todos los usos legítimos del NDK que he visto.

Audio en tiempo real. El recolector de basura del Android Runtime puede pausar tu hilo en cualquier momento, y un callback de audio que tiene que entregar muestras cada cinco o diez milisegundos no sobrevive a una pausa. La guía de Google para el audio de baja latencia es mantener la ruta de tiempo real fuera de la JVM y en C++ nativo, precisamente para esquivar esas pausas del GC. Java tiene un modo de baja latencia de AudioTrack para los casos menos exigentes, pero cuando necesitas el suelo de latencia, la respuesta es nativo. Una ruta de audio típica en Java sin optimizar cae en algún punto entre 100 y 250 milisegundos, mientras que una ruta nativa a través de AAudio y Oboe baja de 20 en los dispositivos que soportan la ruta de baja latencia. Escribí un desglose completo de cómo funciona en mi guía de C++ moderno para audio en Android, y es la misma razón por la que construí Klarinet, un SDK de audio de baja latencia encima de estas API.

Motores de juegos y gráficos. Cualquier cosa que corra un bucle de render cerrado, una simulación de física o llamadas directas a OpenGL ES y Vulkan vive en C++. Unity, Unreal y Godot ejecutan sus núcleos en nativo exactamente por esto: no puedes permitirte una pausa del GC entre fotogramas a 60 o 120 fps.

Cálculo pesado. Procesado digital de señal, preprocesado y posprocesado de visión artificial, cifrado, códecs y el pegamento de cálculo intensivo alrededor de un modelo de ML en el dispositivo. Son bucles cerrados sobre búferes grandes en los que quieres instrucciones SIMD y memoria predecible, no un intérprete decidiendo cuándo recolectar. Aquí es también donde vivía RenderScript; Google lo marcó como obsoleto en Android 12 y ahora apunta estas cargas a la GPU o a C++ nativo con SIMD, y esa es parte de la razón por la que hoy acaban en el NDK.

Reutilizar una biblioteca de C o C++ que ya existe. Si ya existe una biblioteca madura en C++, como FFmpeg, OpenCV, libsodium o el SDK en C de un proveedor, reescribirla en Kotlin sería una temeridad. La envuelves.

Compartir un núcleo entre plataformas. Un núcleo en C++ compila para Android y para iOS y para escritorio desde un solo código. La mitad de iOS la cubrí en cómo usar código C++ en tu proyecto de Xcode para iOS y tvOS. Escribes el motor una vez, expones una superficie fina y específica de cada plataforma en cada lado, y mantienes una implementación en lugar de tres.

Esto no es lo mismo que Kotlin Multiplatform. KMP comparte tu lógica de negocio en Kotlin entre plataformas y es el valor por defecto correcto para eso, pero sigue corriendo sobre un runtime con recolección de basura, así que no sustituye a C++ en la franja de tiempo real, SIMD y libre de bloqueos. En la práctica trabajan juntos: una aplicación KMP envuelve un núcleo en C++, que es exactamente cómo construí Klarinet.

Por qué gana C++ en esos casos

C++ te da un control que el runtime gestionado te quita a propósito.

No hay un recolector de basura que pause tu hilo en el peor momento posible, así que un bucle de tiempo real corre cuando tú se lo dices. También tienes control directo de la memoria, lo que significa que puedes disponer los datos de forma favorable a la caché, evitar asignaciones en la ruta caliente y echar mano de intrínsecos SIMD como ARM NEON para procesar muchas muestras o píxeles por instrucción. Nada de eso está a tu alcance a través de la JVM.

Luego está el ecosistema. Hay décadas de bibliotecas maduras y muy probadas de C y C++ para audio, vídeo, matemáticas y criptografía, y el NDK te deja usarlas directamente. El mismo código de DSP que mueve tu motor de audio en Android también corre en iOS, así que lo verificas una vez y lo arreglas una vez.

Los costes

La frontera JNI no es gratis. Cada llamada a través de JNI hace marshalling de los datos entre el mundo gestionado y el nativo, y eso tiene un coste. Cruza la frontera pocas veces y mueve mucho en cada cruce, normalmente a través de un ByteBuffer directo que los dos lados comparten sin copiar. Un JNI parlanchín, miles de llamadas diminutas, borrará justo el rendimiento por el que te fuiste a nativo.

La compilación se vuelve más pesada. Ahora te toca mantener una configuración de CMake, un puente JNI y una cadena de herramientas más quisquillosa que Gradle a solas. Si todavía te estás peleando con la compilación de Kotlin, mira mi guía de Gradle de Groovy al DSL de Kotlin, porque las compilaciones nativas dan por hecho que ahí ya te mueves con soltura.

Depurar es más difícil. Un crash nativo no te da una traza de pila amable de Kotlin. Aprendes ndk-stack para simbolizar los volcados de crashes nativos, y te familiarizas con señales como SIGSEGV en lugar de excepciones.

Asumes riesgo de seguridad de memoria. Kotlin y la JVM son seguras en memoria por construcción. C++ te devuelve desbordamientos de búfer, use-after-free y desbordamiento de enteros, y en Android eso no son solo crashes: son vulnerabilidades de seguridad explotables. Las bibliotecas nativas empaquetadas, como FFmpeg u OpenSSL, convierten además sus CVE en tu problema de parcheo. Mi guía de configuración de la cadena de herramientas de Rust recorre el mismo proceso del NDK con cargo-ndk en lugar de CMake.

Tu APK crece. Publicas un .so por arquitectura, así que dar soporte a arm64-v8a, armeabi-v7a y x86_64 triplica esa franja de una compilación universal. Los App Bundles, obligatorios en Google Play, se parten por ABI, así que cada dispositivo descarga solo su propio .so, y eso deja el coste real en el dispositivo más cerca de una arquitectura que de tres. La carga nativa sigue siendo mayor que cero.

La regla de las páginas de 16 KB ahora te afecta. Desde el 1 de noviembre de 2025, las aplicaciones nuevas y las actualizaciones en Google Play que apunten a Android 15 (API 35) o superior deben soportar páginas de memoria de 16 KB en dispositivos de 64 bits, y Google amplió el plazo de aplicación en Play Console hasta el 31 de mayo de 2026. Las aplicaciones solo de Kotlin y de Java ya cumplen sin hacer nada. En el momento en que publicas código nativo, heredas este requisito y tienes que recompilar tus archivos .so con la alineación correcta. El NDK r28 y posteriores lo hacen por defecto; en r27 tienes que activarlo a mano con opciones explícitas del enlazador. Normalmente es solo una recompilación, pero ojo con el caso transitivo: un .so precompilado y viejo enterrado en un AAR de terceros puede fallar la comprobación aunque tu propio código esté limpio, así que trátalo como una auditoría de dependencias, no como darle la vuelta a una opción.

Mantenimiento. Dos lenguajes, dos modelos mentales, dos juegos de trampas. Cada ingeniero que toca esa parte del código tiene que sentirse cómodo en los dos.

Cuándo quedarte en Kotlin

No uses C++ para una aplicación normal. Si estás construyendo pantallas CRUD, hablando con una API REST, pintando listas y gestionando formularios, Kotlin con corrutinas sobre el Android Runtime es la herramienta correcta. El runtime lleva años de optimización.

Desconfía especialmente de la optimización prematura. «Podría ser más rápido en C++» no es una razón; es una hipótesis, y cara de comprobar. Perfila primero. La mayoría de los problemas de rendimiento en Android son una consulta mala a la base de datos, una imagen cargada en el hilo principal o un árbol de Compose que renderiza de más, y el código nativo no arregla ninguno. Si no has medido, no estás optimizando: estás adivinando en un lenguaje más difícil.

¿Cómo decides si un código pertenece a C++?

Cuando decido si un trozo de trabajo pertenece a C++, lo paso por cuatro preguntas. Si la respuesta a las cuatro es sí, pasa a nativo. Si alguna es no, quédate en Kotlin y vuelve solo cuando los datos te obliguen.

  1. ¿El muro es la propia JVM? Restricciones de tiempo real, pausas del GC o cálculo bruto que el runtime no puede cumplir, no solo código que todavía no has optimizado.
  2. ¿Lo has perfilado y confirmado? Tienes números, no una corazonada, que muestran que el código gestionado no puede llegar al objetivo.
  3. ¿Ya existe una solución nativa madura, o tienes que compartir este núcleo con iOS? La reutilización y compartir entre plataformas justifican el puente por sí solas.
  4. ¿Puedes permitirte el coste de por vida? La complejidad de la compilación, la depuración y el mantenimiento, durante todo el tiempo que viva la aplicación.

Recursos

Share:Compartir:

CommentsComentarios

Loading comments…Cargando comentarios…