Android NDK with Kotlin: High-Performance Native Code
Some Android workloads require more control over CPU-intensive processing, native libraries, memory, or platform-level algorithms.
The Android NDK allows Kotlin applications to use C and C++ while Kotlin remains responsible for application logic and UI.
Suitable Workloads
Native code can be useful for:
- Image processing
- Video processing
- Signal processing
- Physics simulations
- Large numerical algorithms
- Existing C/C++ libraries
- Performance-sensitive operations
Native code is not automatically faster. JNI overhead and memory copies can eliminate the expected benefit.
Architecture
Android UI
|
ViewModel
|
Native Repository
|
JNI API
|
C++ Engine
|
Optimized Algorithm
Kotlin API
Expose a focused native API:
class ImageProcessor {
companion object {
init {
System.loadLibrary("native_engine")
}
}
external fun process(
input: ByteArray,
width: Int,
height: Int
): ByteArray
}
The expensive operation should happen inside native code rather than repeatedly crossing JNI.
CMake
A minimal native library:
cmake_minimum_required(VERSION 3.22.1)
project("native_engine")
add_library(
native_engine
SHARED
native_engine.cpp
)
Additional native libraries can be linked as required.
Reducing Memory Copies
A naive pipeline might look like:
Camera
↓
Bitmap
↓
ByteArray
↓
JNI
↓
C++ buffer
Every conversion may increase memory usage and latency.
Performance-sensitive systems should minimize unnecessary allocations and copies.
Reusable Native Buffers
For repeated processing, reuse buffers:
class ProcessingBuffer {
public:
std::vector<uint8_t> data;
void resize(size_t size) {
data.resize(size);
}
};
Avoid allocating large temporary buffers for every frame when reuse is possible.
Parallel Processing
A workload can be partitioned:
Input
↓
├── Worker 1
├── Worker 2
├── Worker 3
└── Worker 4
↓
Result
Use suitable thread pools rather than repeatedly creating threads for short tasks.
ABI Considerations
Android native libraries may target architectures such as:
arm64-v8a
armeabi-v7a
x86_64
Package only the architectures required by your supported devices when appropriate to reduce application size.
Measuring Performance
Measure rather than guess.
Track:
- Execution time
- Memory allocation
- CPU usage
- Frame latency
- Battery consumption
A simple native benchmark can use std::chrono:
auto start = std::chrono::steady_clock::now();
process();
auto end = std::chrono::steady_clock::now();
Use Android profiling tools for deeper analysis.
Kotlin Coroutines
Keep expensive work away from the main thread:
viewModelScope.launch {
val result = withContext(Dispatchers.Default) {
processor.process(input, width, height)
}
updateUi(result)
}
Native Crash Prevention
Common native failures include:
- Invalid pointers
- Buffer overflows
- Use-after-free
- Race conditions
- Incorrect JNI usage
Use sanitizers and native debugging during development.
Production Checklist
Before shipping:
- Test supported ABIs
- Validate memory ownership
- Profile JNI overhead
- Test low-memory devices
- Test lifecycle transitions
- Stress test native code
- Inspect native crash reports
Conclusion
The NDK is most valuable when Kotlin needs native libraries or performance-sensitive algorithms.
A strong architecture keeps Kotlin responsible for application behavior and uses C/C++ as a focused computational engine with a small, measurable interface.
Useful Links
SDK Flutter: https://github.com/v-modal/vmodal_sdk_flutter
SDK Android: https://github.com/v-modal/vmodal_sdk_android
Discord: https://discord.gg/K72z28KUx
Top comments (0)