What is native development?
Native development on Android involves writing application logic in C or C++ instead of Java or Kotlin. Your native code is compiled into shared libraries (.so files) that are packaged with your APK and called from Java/Kotlin code through the Java Native Interface (JNI).
When to use the NDK
The NDK is not suitable for all Android applications. Consider using native code when:Performance-critical computation
Performance-critical computation
Native code can provide significant performance improvements for:
- Signal processing and audio/video codecs
- Physics simulations and game engines
- Image processing and computer vision
- Cryptographic operations
- Mathematical computations with tight loops
Reusing existing C/C++ libraries
Reusing existing C/C++ libraries
If you have existing native codebases:
- Game engines (Unreal, Unity native plugins)
- Cross-platform libraries
- Scientific computing libraries
- Third-party SDKs distributed as native libraries
Low-level system access
Low-level system access
Native code provides access to:
- POSIX APIs and system calls
- Hardware-specific features
- OpenGL ES, Vulkan graphics APIs
- OpenSL ES, AAudio for low-latency audio
Benefits of native development
Performance advantages
Native code can offer performance benefits in specific scenarios:- No JIT overhead: Compiled directly to machine code without JVM interpretation
- SIMD instructions: Access to ARM NEON and x86 SSE/AVX vector instructions
- Manual memory management: Fine-grained control over allocations and cache behavior
- Lower latency: Reduced overhead for tight computational loops
Modern Android Runtime (ART) includes an ahead-of-time (AOT) compiler that optimizes Java/Kotlin code extensively. The performance gap between managed and native code has narrowed significantly.
Code reuse
Leverage existing native codebases:- Share core logic between Android, iOS, and desktop platforms
- Integrate mature C/C++ libraries without reimplementation
- Maintain a single codebase for cross-platform features
Platform capabilities
Access capabilities not exposed through Android framework:- Graphics APIs (Vulkan, OpenGL ES 3.x)
- Audio APIs (AAudio, OpenSL ES)
- Neural Networks API (NNAPI)
- Camera2 NDK API
Tradeoffs and considerations
Increased complexity
- Development complexity
- Debugging challenges
- Maintenance burden
Native development introduces multiple layers of complexity:
- Language expertise: Requires C/C++ knowledge and understanding of memory management
- Build systems: Must configure ndk-build or CMake in addition to Gradle
- JNI overhead: Need to write and maintain JNI bindings and type conversions
- Multiple toolchains: Different compilers and tools for each ABI
Not always faster
Common misconceptions about native code performance:JNI call overhead: Calling native methods from Java/Kotlin has overhead. Frequent small native calls can be slower than pure Java/Kotlin code. Native code should do substantial work to amortize the JNI crossing cost.
Security considerations
Native code requires additional security attention:- Memory safety: Buffer overflows, use-after-free, and other memory corruption vulnerabilities
- No language-level protection: Unlike Java/Kotlin, C/C++ has no built-in bounds checking
- Third-party dependencies: Responsibility for security updates in native libraries
- Attack surface: Native vulnerabilities can compromise the entire application
Development workflow
Typical NDK development follows this workflow:- Write native code: Implement functionality in C/C++ source files
- Configure build: Set up Android.mk, CMakeLists.txt, or Gradle build configuration
- Define JNI interface: Create native method declarations and implementations
- Build shared libraries: Compile for target ABIs (arm64-v8a, armeabi-v7a, x86_64, x86)
- Load libraries: Call
System.loadLibrary()from Java/Kotlin - Test and debug: Use Android Studio debugger with native debugging enabled
Best practices
Minimize JNI crossings
Reduce the number of calls between Java/Kotlin and native code:- Batch operations when possible
- Keep frequently called hot paths on one side of the boundary
- Transfer data in bulk rather than individual items
Use appropriate data structures
Choose JNI data access methods based on usage patterns:Handle errors properly
Always check for JNI exceptions and NULL returns:Profile before optimizing
Use Android Studio’s profiling tools to identify actual bottlenecks. Many performance issues can be resolved in Java/Kotlin without native code.
Alternative approaches
Before committing to NDK development, consider these alternatives:- Renderscript/Compute shaders: For parallel computation on GPU
- Kotlin Native: For Kotlin-based cross-platform code
- WebAssembly: For portable computation in WebView
- Pure Java/Kotlin optimization: Modern JIT compilers are highly effective
Getting started
To begin NDK development:- Install the NDK through Android Studio SDK Manager
- Review the JNI overview to understand the interface
- Learn about ABIs to configure your build for target architectures
- Understand bionic differences from standard C libraries
Start with a simple “hello world” native library to familiarize yourself with the build process before tackling complex native functionality.