Understanding JNI
The Java Native Interface (JNI) is a critical component of the Android operating system, enabling managed Java or Kotlin code to interact seamlessly with native C and C++ code. In the context of AOSP, JNI is the essential bridge that connects high-level Android framework services (written in Java) to underlying native daemons and libraries.
The Purpose of JNI
JNI serves two primary use cases in Android:
- Calling Native Code from Java: Utilizing high-performance libraries (like OpenGL or custom C++ audio processors) directly from an Android application or system service.
- Calling Java Code from Native: Allowing background native threads (such as sensors or hardware abstraction layers) to invoke callbacks on Java objects.
Without JNI, the Android Framework would be isolated from the Linux kernel and hardware drivers.
Core JNI Structures: JNIEnv and JavaVM
At the heart of JNI are two essential structures:
JavaVM: Represents the Java Virtual Machine process. There is exactly oneJavaVMper process in Android. You use theJavaVMinterface to attach native threads to the JVM, enabling them to execute JNI calls.JNIEnv: Represents the JNI environment for a specific thread.JNIEnvpointers are strictly thread-local. You cannot pass aJNIEnvfrom one thread to another. It provides the functions needed to interact with Java objects, call Java methods, and throw exceptions.
// Example: Attaching a native thread to the JavaVM
JNIEnv* env;
if ((*jvm)->AttachCurrentThread(jvm, &env, NULL) != JNI_OK) {
// Handle error
}
Static vs. Dynamic JNI Method Registration
Android provides two ways to bind native functions to Java native method declarations.
Static Registration
Static registration relies on strict naming conventions. The JVM resolves the native method at runtime by looking for a specifically named C/C++ function. The tool javah (or javac -h) generates these headers.
package com.aosp.example;
public class NativeBridge {
public native void performAction();
}
// Static JNI function name mapping
JNIEXPORT void JNICALL Java_com_aosp_example_NativeBridge_performAction(JNIEnv *env, jobject thiz) {
// Implementation
}
Dynamic Registration
Dynamic registration is the preferred approach in AOSP. It explicitly maps Java methods to C/C++ function pointers at runtime, typically within the JNI_OnLoad function. This approach is faster, avoids long and fragile function names, and makes code refactoring easier.
// Dynamic Registration Example
#include <jni.h>
void native_performAction(JNIEnv *env, jobject thiz) {
// Implementation
}
static const JNINativeMethod gMethods[] = {
{"performAction", "()V", (void*)native_performAction}
};
jint JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv* env;
if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) {
return JNI_ERR;
}
jclass clazz = env->FindClass("com/aosp/example/NativeBridge");
if (env->RegisterNatives(clazz, gMethods, 1) < 0) {
return JNI_ERR;
}
return JNI_VERSION_1_6;
}
JNI Identifiers: jclass, jmethodID, and jfieldID
To interact with Java objects from native code, JNI uses specific identifiers:
jclass: A reference to a JavaClassobject. Obtained viaenv->FindClass().jmethodID: An opaque identifier for a Java method. Obtained viaenv->GetMethodID()orenv->GetStaticMethodID().jfieldID: An opaque identifier for a Java field. Obtained viaenv->GetFieldID()orenv->GetStaticFieldID().
These identifiers are often cached globally to avoid the heavy performance cost of looking them up on every JNI call.
// Finding a class and method ID
jclass stringClass = env->FindClass("java/lang/String");
jmethodID lengthMethod = env->GetMethodID(stringClass, "length", "()I");
// Calling the method on a jstring instance
jstring myStr = ...; // Obtained elsewhere
jint len = env->CallIntMethod(myStr, lengthMethod);