第一个JNI程序:Java与C++互相调用

好,咱们直接动手。第一个JNI程序,说白了就是让Java和C++能互相喊话。我刚开始学JNI时,觉得这玩意儿挺玄乎——两个完全不同的语言,怎么就能在Android上无缝配合呢?其实底层原理没那么复杂,今天咱们一步步把它拆开。

JNI函数命名规则

先说说命名。JNI函数的命名,有一套严格的规则。你想想看,Java层的方法到了C++层,总得有个统一的寻址方式吧?

规则是这样的:Java_包名_类名_方法名。包名里的点号要换成下划线。举个例子:

// Java层
package com.example.myapp;
public class NativeLib {
    public static native String helloFromCpp();
}

// C++层对应的函数名
JNIEXPORT jstring JNICALL
Java_com_example_myapp_NativeLib_helloFromCpp(JNIEnv *env, jclass clazz) {
    // ...
}

嗯,这里要注意一点。如果包名或类名里本身就有下划线,那JNI会用_1来转义。我在项目中遇到过有人因为包名带下划线,结果JNI死活找不到函数,排查了半天才发现是命名问题。所以,我个人建议包名和类名里尽量别用下划线,省得给自己挖坑。

javah与javac的使用

接下来是工具链。javac和javah,这两个命令是JNI开发的入门工具。

javac 负责把Java源码编译成.class文件。这个大家应该都熟,不多说。

javah 的作用是自动生成JNI头文件。它会根据Java代码中的native方法声明,生成对应的C/C++函数声明。说白了,就是帮你把命名规则那套繁琐的东西自动化了。

具体用法:

// 1. 编译Java文件
javac com/example/myapp/NativeLib.java

// 2. 生成JNI头文件
javah -jni com.example.myapp.NativeLib

执行完第二步,你会得到一个com_example_myapp_NativeLib.h文件。打开看看,里面就是JNI要求的函数声明。你只需要在C++文件里实现这些函数就行。

我的小技巧: 如果你用Android Studio,其实可以跳过javah这一步。直接在native方法上Alt+Enter,IDE会自动帮你生成JNI函数签名。不过,理解底层原理还是很有必要的——万一哪天IDE抽风了呢?

生成so库

最后一步,把C++代码编译成so库。在Android NDK环境下,我们通常用CMake或ndk-build来管理编译。

这里我以CMake为例,展示一个最简单的CMakeLists.txt

cmake_minimum_required(VERSION 3.4.1)

project("mynativelib")

add_library(
    mynativelib
    SHARED
    native-lib.cpp
)

find_library(
    log-lib
    log
)

target_link_libraries(
    mynativelib
    ${log-lib}
)

然后在build.gradle里配置一下:

android {
    ...
    defaultConfig {
        ...
        externalNativeBuild {
            cmake {
                cppFlags "-std=c++11"
            }
        }
    }
    externalNativeBuild {
        cmake {
            path "CMakeLists.txt"
        }
    }
}

编译完成后,so文件会出现在app/build/intermediates/cmake目录下。不同CPU架构(armeabi-v7a、arm64-v8a、x86等)会生成对应的so文件。

我曾经踩过的坑: 有一次我忘了在CMakeLists.txt里链接log库,结果在C++代码里调用__android_log_print时,编译没问题,但运行时直接崩溃。排查了半天才发现是链接问题。所以,记得把用到的库都显式链接上。

完整示例:Java与C++互相调用

光说不练假把式。咱们写一个完整的例子,Java调用C++,C++再回调Java。

Java层:

package com.example.myapp;

public class NativeLib {
    static {
        System.loadLibrary("mynativelib");
    }

    // C++实现的方法
    public static native String helloFromCpp();

    // C++会回调这个方法
    public static void callbackFromCpp(String message) {
        Log.d("NativeLib", "C++回调: " + message);
    }
}

C++层(native-lib.cpp):

#include <jni.h>
#include <string>
#include <android/log.h>

#define LOG_TAG "NativeLib"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)

extern "C" JNIEXPORT jstring JNICALL
Java_com_example_myapp_NativeLib_helloFromCpp(
    JNIEnv *env,
    jclass clazz) {

    // 获取Java类的jclass
    jclass nativeLibClass = env->FindClass("com/example/myapp/NativeLib");
    if (nativeLibClass == nullptr) {
        LOGD("找不到NativeLib类");
        return env->NewStringUTF("error");
    }

    // 获取callbackFromCpp方法的jmethodID
    jmethodID callbackMethod = env->GetStaticMethodID(
        nativeLibClass,
        "callbackFromCpp",
        "(Ljava/lang/String;)V"
    );
    if (callbackMethod == nullptr) {
        LOGD("找不到callbackFromCpp方法");
        return env->NewStringUTF("error");
    }

    // 调用Java方法
    jstring message = env->NewStringUTF("Hello from C++!");
    env->CallStaticVoidMethod(nativeLibClass, callbackMethod, message);

    // 返回结果给Java
    return env->NewStringUTF("C++处理完成,已回调Java");
}

这个例子展示了完整的双向调用流程。Java调用C++的helloFromCpp,C++内部又通过JNI回调了Java的callbackFromCpp方法。

核心要点:

  • JNI函数命名必须严格遵循规则,否则找不到
  • javah可以自动生成头文件,减少手动编写的工作量
  • CMake是Android NDK推荐使用的构建工具
  • JNIEnv指针是C++与Java交互的桥梁,所有JNI函数都通过它调用
  • 记得处理异常和空指针,JNI代码一旦崩溃,整个应用都会挂掉

好了,第一个JNI程序就到这里。说白了,JNI就是一层协议,让Java和C++能互相理解对方在说什么。你只要掌握了命名规则、工具链和基本的调用流程,后面再复杂的场景都是这些基础知识的组合。

下一章,咱们会深入JNI的数据类型转换——Java的String怎么变成C++的std::string?Java的数组怎么传给C++?这些坑,我当年都踩过,到时候一一告诉你。