The preface
Because the following source code analysis will involve a lot of Java and Native calls to each other. Of course, it has no impact on our code analysis, but the fact that such a black box is in front of us makes us curious about how it works. This article will move from the most basic concepts of JNI to simple code examples and implementation principles.
JNI
JNI (Java Native Interface) is a programming framework that enables Java programs in a Java virtual machine to call local applications or libraries and be called by other programs. Native programs are typically written in other languages C, C++, or assembly, and are compiled to be based on the native hardware and operating system. On the Android platform, in order to make it more convenient for developers to use and enhance its functionality, Android provides NDK to facilitate the development of developers.
Why JNI?
JNI allows programmers to use other programming languages to address situations that are difficult to handle with pure Java code, such as platform-specific features or libraries that are not supported by the Java standard library. It is also used to modify existing programs written in other languages for Java programs to call. Many standard libraries based on JNI provide many functions for programmers to use, such as file I/O and audio-related functions. Of course, there are also high-performance programs, as well as platform-specific API implementations, that allow all Java applications to use these capabilities safely and platform-independent. The Java layer can be used to implement UI functionality, while C++ takes care of computational operations.
The JNI framework allows Native methods to call Java objects just as easily as Java programs access Native objects. Native methods can create Java objects, read them, and call Java objects to perform certain methods. Native methods can also read objects created by Java programs themselves and call methods on those objects.
Hello World
Here, we’ll start with a simple Hello World example to get a sense of the JNI call flow, and then analyze the implementation principles and details.
1. Define native functions in Java files
In this method declaration, the use of the native keyword tells the virtual machine that the function is in a shared library (that is, implemented on the native side).
private native String helloWorld();
Copy the code
2. Use Javah to generate header files
For the naming rules of native methods, function names are constructed according to the following rules:
- Prefix the name with Java_.
- Describes the file paths associated with the top-level source directory.
- Use an underscore instead of a forward slash.
- Delete the.java file extension.
- Append the function name after the last underscore.
Following these rules, this example uses a function named Java_com_example_hellojni_HelloJni_stringFromJNI. This name describe hellojni/SRC/com/example/hellojni/hellojni. In Java, a stringFromJNI Java () function. We want to make it easier to write native functions like Java functions without this step of transformation, so we can do it through Javah.
javah -d ../jni -jni com.chenjensen.myapplication.MainActivity
Copy the code
- D: output directory of header files
- Jni: Generates a JNI file
3. Implement native functions according to the header files generated by Javah
JNIEXPORT jstring JNICALL Java_com_chenjensen_myapplication_MainActivity_helloWorld
(JNIEnv *, jobject);
Copy the code
Native methods defined in our Java files are generated in the header file, and the type conversion is completed. We only need to create a new CPP file to implement the corresponding methods.
4. The CPP file
JNIEXPORT jstring JNICALL Java_com_chenjensen_myapplication_MainActivity_helloWorld
(JNIEnv *env, jobject)
{
char *str = "Hello world";
return (*env).NewStringUTF(str);
}
Copy the code
5. Build file to support the specified platform (ARM, x86, etc.)
ndk {
moduleName "hello"// The name of the generated so file will be used in the code calling the C program abiFilters"armeabi"."armeabi-v7a"."x86"}}Copy the code
After specifying the name of the generated so file, the compilation system will look for the corresponding C/CPP file from the JNI directory to generate the corresponding SO file.
6. Perform
In Java code, before the execution of native methods, the corresponding dynamic library should be loaded in advance, and then the execution can be carried out. Generally, static code blocks are loaded in this class. When the application starts, this function is called to load the.so file.
static {
System.loadLibrary("hello");
}
Copy the code
At this point, calling the corresponding native code in the Java code will take effect.
So how to call Java from a C/C++ file is similar to how we call Java to find a class by reflection. The core functions are as follows.
FindClass(), NewObject(), GetStaticMethodID(),
GetMethodID(), CallStaticObjectMethod(), CallVoidMethod()
Copy the code
Find the corresponding class, the corresponding method, call the corresponding class and method. I won’t give you a code example here. See the link at the end of this article.
How to call
Through the above six steps, we realized Java calling native functions. With the help of the corresponding tools, we could quickly realize the mutual call between them. However, the tools also shielded a lot of implementation details, making the process into a black box without knowing its implementation. In this process, when the JVM calls these functions, it passes a pointer to JNIEnv, a pointer to jobject, and any Java parameters declared in a Java method.
A JNI function looks like this:
JNIEXPORT void JNICALL Java_ClassName_MethodName
(JNIEnv *env, jobject obj)
{
/*Implement Native Method Here*/
}
Copy the code
Java and C++ calls, Java execution needs to be on the JVM, so when calling, the JVM must know which local function to call, and when the local function calls Java, it must know the application object and the specific function.
In JNI, C++ and Java are executed on the same thread, but the thread values are different. JNIEnv is the JNI environment. The JNIEnv object is bound to the thread. When the call is made, a JavaVM pointer is passed as a parameter, and then the JavaVM getEnv function gets the JNIEnv object pointer. Each time a thread is created in Java, a new JNIEnv object is generated.
When analyzing the source code of the system, we can see a lot of Java calls to Native. Through the analysis of the source code, we find that after the system is started, many Service processes will be started. At this time, many of its implementations are realized through Native. Let’s go back to the system startup process. The startup VM is first invoked in the Zygote process.
if(startVm(&mJavaVM, &env, zygote) ! = 0) {return;
}
onVmCreated(env);
if (startReg(env) < 0) {
return;
}
Copy the code
int AndroidRuntime::startReg(JNIEnv* env)
{
if (register_jni_procs(gRegJNI, NELEM(gRegJNI), env) < 0) {
env->PopLocalFrame(NULL);
return- 1; }...return 0;
}
Copy the code
static int register_jni_procs(const RegJNIRec array[], size_t count, JNIEnv* env)
{
for (size_t i = 0; i < count; i++) {
if (array[i].mProc(env) < 0) {
return -1;
}
}
return 0;
}
Copy the code
static const RegJNIRec gRegJNI[] = { REG_JNI(register_com_android_internal_os_RuntimeInit), REG_JNI(register_android_os_SystemClock), REG_JNI(register_android_util_EventLog), REG_JNI(register_android_util_Log), . }Copy the code
Array [I] refers to the gRegJNI array, which has more than 100 members. Each of these members is defined by the REG_JNI macro.
#define REG_JNI(name) { name }
Copy the code
struct RegJNIRec {
int (*mProc)(JNIEnv*);
};
Copy the code
Calling mProc is equivalent to calling the function to which its argument name refers. For example, REG_JNI(register_com_android_internal_os_RuntimeInit). MProc refers to the register_com_android_internal_os_RuntimeInit method. After entering these methods, there is a mapping of some native and Java methods in the class.
int register_com_android_internal_os_RuntimeInit(JNIEnv* env) {
return jniRegisterNativeMethods(env, "com/android/internal/os/RuntimeInit",
gMethods, NELEM(gMethods));
}
Copy the code
Static JNINativeMethod gMethods[] = {{static JNINativeMethod [] = {{"nativeFinishInit"."()V",
(void*) com_android_internal_os_RuntimeInit_nativeFinishInit },
{ "nativeZygoteInit"."()V",
(void*) com_android_internal_os_RuntimeInit_nativeZygoteInit },
{ "nativeSetExitWithoutCleanup"."(Z)V",
(void*) com_android_internal_os_RuntimeInit_nativeSetExitWithoutCleanup },
};
Copy the code
This completes the mapping association between native methods and Java methods.
- Another way to load
JNI methods can be registered in two ways: one is registered during the above startup process, and the other is registered in the program through the System. LoadLibrary. Here, we use System.
public static void loadLibrary(String libname) {
Runtime.getRuntime().loadLibrary0(VMStack.getCallingClassLoader(), libname);
}
Copy the code
public static Runtime getRuntime() {
return currentRuntime;
}
Copy the code
synchronized void load0(Class fromClass, String filename) {
if(! (new File(filename).isAbsolute())) { throw new UnsatisfiedLinkError("Expecting an absolute path of the library: " + filename);
}
if (filename == null) {
throw new NullPointerException("filename == null");
}
String error = doLoad(filename, fromClass.getClassLoader());
if (error != null) {
throw new UnsatisfiedLinkError(error);
}
}
Copy the code
String librarySearchPath = null;
if(loader ! = null && loader instanceof BaseDexClassLoader) { BaseDexClassLoader dexClassLoader = (BaseDexClassLoader) loader; librarySearchPath = dexClassLoader.getLdLibraryPath(); } synchronized (this) {return nativeLoad(name, loader, librarySearchPath);
}
Copy the code
After layer upon layer of invocation, we came to the nativeLoad method. The purpose of this code analysis is to understand how the JVM finds the corresponding native method during the whole JNI registration process and invocation.
The content executed by nativeLoad is passed to the classLoader and eventually converted into system calls to the dlopen and DLSYm functions.
- Call the dlopen function to open an so file and create a handle;
- Call the dlsym() function, look at the JNI_OnLoad() function pointer of the corresponding so file, and execute the corresponding function.
In short, Dlopen and DLSYM provide a mechanism to dynamically load libraries into memory and call methods in the library when needed.
In Java bytecode, the native method is distinguished from the normal method by a flag “ACC_NATIVE”, where the normal method is to put the byte code directly into the code property sheet. When Java performs ordinary method calls, it can find the method table and the corresponding code property table to interpret the execution code.
When loading the dynamic library load into the system, the first step is to execute the JNI_OnLoad method of the dynamic library. We need to declare the association between Native and Java in this method. Since the relevant classes in the system do not provide this method, they need to manually call their respective registration methods. In the demo we wrote, the compiler did it for us and didn’t need us to do it. Once the mapping is written, the registerNativeMethods method is called to register these methods. The specific function mapping and registration is shown in the Runtime.
In the translated Java code, ordinary Java methods will directly point to specific methods in the method table, while for native methods, special marks are made. When the native method is executed, it will find the corresponding method according to the corresponding table of the native method loaded in before execution.
Refer to the article
Java Java JNI implementation principle