杰瑞科技汇

Android Java JNI如何高效调用本地代码?

Of course! Let's dive deep into Android, Java, and JNI. This is a powerful but complex topic, so we'll break it down from the fundamentals to a practical, step-by-step example.

Android Java JNI如何高效调用本地代码?-图1
(图片来源网络,侵删)

Table of Contents

  1. What is JNI? (The "Why")
  2. When to Use JNI (and When NOT To)
  3. The Core Concept: The Bridge
  4. Step-by-Step JNI Implementation
    • Step 1: The Java Side
    • Step 2: Generate the Header File
    • Step 3: Write the C/C++ Native Code
    • Step 4: Create the CMake Build Script
    • Step 5: Tell Android Studio About Your Native Code
    • Step 6: Load and Use the Native Library
  5. Common JNI Tasks & Data Type Mapping
  6. Advanced Topics & Best Practices
  7. Alternatives to JNI

What is JNI? (The "Why")

JNI stands for Java Native Interface. It is a framework that allows Java code running in the Java Virtual Machine (JVM) to interact with applications and libraries written in other languages, such as C, C++, and Assembly.

Think of it as a bridge or a translator. On one side of the bridge is your Java/Kotlin code, and on the other side is your native (C/C++) code. JNI enables them to call each other and exchange data.

The Key Components:

  • Java Code: Your Android app's logic, UI, etc.
  • Native Code (C/C++): Code compiled to machine code that runs directly on the CPU.
  • JNI Library: A set of functions and conventions that both sides use to communicate. The JVM knows how to call JNI functions, and your C/C++ code is written to implement those functions.

When to Use JNI (and When NOT To)

JNI is a powerful tool, but it's not a silver bullet. Misusing it can lead to complex, slow, and hard-to-maintain code.

Android Java JNI如何高效调用本地代码?-图2
(图片来源网络,侵删)

Good Use Cases:

  • Reusing Existing Native Libraries: You have a high-performance, legacy C/C++ library (e.g., a physics engine, a video codec, an encryption library) that you want to use in your Android app without rewriting it in Java/Kotlin.
  • Performance-Critical Code: For specific, CPU-intensive tasks where C/C++ can offer significant speedups over the JVM (e.g., complex mathematical calculations, audio/video processing).
  • Accessing Platform-Specific APIs: When you need to interact with hardware or OS features that are not exposed through the Android SDK (though the NDK is increasingly making this less necessary).
  • Low-Level Hardware Interaction: For direct hardware access, which is typically done in C/C++.

Avoid When Possible:

  • Simple Logic: Don't use JNI just to call a simple add(int a, int b) function. The overhead of the JNI call will likely make it slower than the Java equivalent.
  • UI Operations: NEVER create or manipulate Android UI elements (like View objects) from a native thread. UI must be updated on the main UI thread using runOnUiThread(). This is a common source of crashes.
  • Memory Management: You are now responsible for managing memory in two worlds (the JVM's garbage collector and C/C++'s manual memory management). This is complex and can lead to memory leaks or crashes if not handled carefully.
  • Development Complexity: JNI code is harder to write, debug, and maintain. It breaks the "write once, run anywhere" promise of Java.

The Core Concept: The Bridge

The communication works in two directions:

Java → C/C++ (Calling Native Code)

  • You declare a native method in your Java class using the native keyword.
  • You compile your Java code, and the javah tool (or modern javac -h) generates a C/C++ header file (.h). This header file defines the function signature your C/C++ code must implement.
  • You write the C/C++ implementation for that function.
  • You package your C/C++ code into a shared library (.so file).
  • Your Java code uses System.loadLibrary("mylibrary") to load the library. After loading, the JVM can link your Java method call to the corresponding C/C++ function.

C/C++ → Java (Calling Java Code)

  • From your native C/C++ function, you can call back into the JVM.
  • You use JNI functions (like FindClass, GetMethodID, CallVoidMethod) to get a reference to a Java class, a method, and then invoke that method.
  • This allows you to pass data back to Java or call Java methods to perform actions (like updating the UI).

Step-by-Step JNI Implementation

Let's create a simple example: a Java method that calls a C++ function to add two integers and return the result.

Android Java JNI如何高效调用本地代码?-图3
(图片来源网络,侵删)

Step 1: The Java Side

Create a Java class with a native method. This method has no implementation in Java; its implementation will be in C++.

app/src/main/java/com/example/jnidemo/MyJniClass.java

package com.example.jnidemo;
public class MyJniClass {
    // Load the native library when the class is loaded.
    // The library name "myjni-lib" corresponds to the "libmyjni-lib.so" file.
    static {
        System.loadLibrary("myjni-lib");
    }
    // Declare a native method. The implementation is in C++.
    // Note the native method signature. We'll map types later.
    public native int add(int a, int b);
    // Another example: passing a String from Java to C++
    public native String sayHello(String name);
}

Step 2: Generate the Header File

You need to tell the C++ compiler what function signature to implement. The easiest way is to use Android Studio's built-in tools.

  1. Make sure your Java file is saved.
  2. Click on the add method in MyJniClass.java.
  3. Press Alt + Enter (or Option + Enter on Mac).
  4. Select Add JNI stubs.

Android Studio will automatically create the C++ source and header files in app/src/main/cpp/:

  • myjni-lib.cpp (the C++ implementation file)
  • myjni-lib.h (the C++ header file)

The generated myjni-lib.h will look something like this. You should not edit this file by hand.

app/src/main/cpp/myjni-lib.h

#ifndef MYJNILIB_H
#define MYJNILIB_H
#include <jni.h>
#include <string>
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_example_jnidemo_MyJniClass
 * Method:    add
 * Signature: (II)I
 */
JNIEXPORT jint JNICALL Java_com_example_jnidemo_MyJniClass_add(JNIEnv *, jobject, jint, jint);
/*
 * Class:     com_example_jnidemo_MyJniClass
 * Method:    sayHello
 * Signature: (Ljava/lang/String;)Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_example_jnidemo_MyJniClass_sayHello(JNIEnv *, jobject, jstring);
#ifdef __cplusplus
}
#endif
#endif // MYJNILIB_H

Key points in the header:

  • JNIEXPORT, JNICALL: Macros that ensure the function is exported correctly.
  • Java_com_example_jnidemo_MyJniClass_add: This is the mangled function name. It's derived from ClassPackagePathClassName_MethodName. The JVM uses this name to find the function.
  • JNIEnv *: A pointer to the JNI environment. It's your gateway to all JNI functions (e.g., creating objects, calling methods).
  • jobject: A reference to the Java object instance this method belongs to (equivalent to this in Java). For static methods, this would be jobject.
  • (II)I: This is the method signature. It describes the argument types and the return type.
    • starts the argument list.
    • I is a JNI type for int.
    • ends the argument list.
    • The final I is the return type (int).

Step 3: Write the C/C++ Native Code

Now, implement the functions defined in the header file.

app/src/main/cpp/myjni-lib.cpp

#include "myjni-lib.h"
// Implementation of the add method
JNIEXPORT jint JNICALL Java_com_example_jnidemo_MyJniClass_add(JNIEnv *env, jobject thiz, jint a, jint b) {
    // Simply add the two integers and return the result
    return a + b;
}
// Implementation of the sayHello method
JNIEXPORT jstring JNICALL Java_com_example_jnidemo_MyJniClass_sayHello(JNIEnv *env, jobject thiz, jstring name) {
    // 1. Get the C-style string from the jstring
    const char *c_str = env->GetStringUTFChars(name, nullptr);
    if (c_str == nullptr) {
        // OutOfMemoryError thrown
        return nullptr;
    }
    // 2. Create a new C++ string
    std::string result = "Hello from C++, ";
    result += c_str;
    // 3. Release the memory for the C-style string
    env->ReleaseStringUTFChars(name, c_str);
    // 4. Create a new jstring from the C++ string and return it
    return env->NewStringUTF(result.c_str());
}

Step 4: Create the CMake Build Script

CMake is the standard build system for native code in Android Studio. You need to tell it how to compile your C++ files into a shared library.

Create a file named CMakeLists.txt in the app directory (same level as build.gradle).

app/CMakeLists.txt

# Sets the minimum version of CMake required.
cmake_minimum_required(VERSION 3.18.1)
# Define the project name.
project("myjni-lib")
# Add your native library. The name here MUST match the name in System.loadLibrary.
add_library(
        myjni-lib # Library name
        SHARED    # Type: Shared Library (.so)
        # List of source files to compile
        src/main/cpp/myjni-lib.cpp
)
# Find and link the JNI library.
# This is essential for your code to access JNI functions like JNIEnv.
find_library(
        log-lib
        log)
# Link your library against the log library (optional, for logging).
target_link_libraries(
        myjni-lib
        ${log-lib})

Step 5: Tell Android Studio About Your Native Code

Finally, update your app-level build.gradle file to point to your CMake configuration.

app/build.gradle

android {
    // ... other configurations
    defaultConfig {
        // ... other settings
        externalNativeBuild {
            cmake {
                // Pass arguments to CMake.
                // For example, to enable C++17: arguments "-DANDROID_STL=c++_17"
                cppFlags ""
            }
        }
    }
    // This is the crucial part
    externalNativeBuild {
        cmake {
            path "src/main/cpp/CMakeLists.txt" // Path to your CMakeLists.txt
            version "3.18.1"                   // Use a CMake version compatible with your AGP
        }
    }
}
// ... other configurations like dependencies

After saving this file, Android Studio will automatically sync your project and build the native library. You can see the generated .so files in app/build/intermediates/cmake/debug/lib/.

Step 6: Load and Use the Native Library

Now you can use your MyJniClass in your Activity or Fragment.

MainActivity.java

package com.example.jnidemo;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        TextView tv = findViewById(R.id.sample_text);
        // Create an instance of our JNI class
        MyJniClass jniHelper = new MyJniClass();
        // Call the native add method
        int sum = jniHelper.add(10, 25);
        String message = "10 + 25 = " + sum;
        // Call the native sayHello method
        String greeting = jniHelper.sayHello("Android Developer");
        message += "\n\n" + greeting;
        tv.setText(message);
    }
}

Common JNI Tasks & Data Type Mapping

This is one of the trickiest parts. You constantly convert between Java types and native types.

Java Type JNI Type Description
byte jbyte 8-bit signed integer
short jshort 16-bit signed integer
int jint 32-bit signed integer
long jlong 64-bit signed integer
float jfloat 32-bit floating-point
double jdouble 64-bit floating-point
char jchar 16-bit Unicode character
boolean jboolean 8-bit (true=1, false=0)
Object jobject Any non-primitive object
String jstring A Java String object
Class jclass A Java Class object
Throwable jthrowable A Java Throwable object
Object[] jobjectArray Any object array
byte[] jbyteArray Array of bytes
int[] jintArray Array of ints
... ... ...

Key JNI Functions for Data Conversion:

  • Strings:
    • GetStringUTFChars(): Converts jstring to const char*.
    • NewStringUTF(): Converts const char* to jstring.
    • Remember to ReleaseStringUTFChars() when you're done!
  • Arrays:
    • GetIntArrayElements(): Gets a pointer to the jintArray's elements.
    • ReleaseIntArrayElements(): Releases the array.
    • GetIntArrayLength(): Gets the array's length.

Advanced Topics & Best Practices

  • Threading: By default, native code runs on the same thread that called it. If you call a long-running native function from the UI thread, your app will freeze. Offload heavy native work to a background thread using java.lang.Thread or java.util.concurrent.Executor.
  • Exception Handling: JNI does not automatically propagate Java exceptions to your native code. If a Java method called from native code throws an exception, the JNI function will return an error code or NULL. You must explicitly check for exceptions using ExceptionCheck() and handle them with ExceptionDescribe() and ExceptionClear().
  • Memory Management: Be careful with NewGlobalRef and DeleteGlobalRef. When you get a reference to a Java object in native code (e.g., via NewObject), you must manage its lifecycle. Global references prevent the JVM's garbage collector from reclaiming the object, so you must explicitly delete them when you're done to avoid memory leaks.
  • JNI_OnLoad: When you load a library, you can provide a JNI_OnLoad function. This function is called by the JVM when the library is first loaded. It's used to perform one-time setup, like registering your native methods with the JVM, which can be faster than the default lookup mechanism.

Alternatives to JNI

While JNI is the standard, it's not the only way to use native code in Android.

  • Android NDK (Native Development Kit): This is not an alternative to JNI, but rather the toolkit that provides the compiler, debugger, and build system (like CMake) for creating the native libraries that JNI uses. You cannot use the NDK without using JNI under the hood.
  • Kotlin/Native: This is a different approach from JetBrains. It compiles Kotlin code (including its interop with C/Objective-C) directly to a native binary, without a JVM. It avoids the overhead of JNI but is a different ecosystem and may not be suitable for all projects.
  • P/Invoke for .NET (MAUI): If you are using .NET Multi-platform App UI (MAUI), you use Platform Invocation Services (P/Invoke) to call native libraries, which has a different (and often simpler) syntax than JNI.
分享:
扫描分享到社交APP
上一篇
下一篇