第1章 JNI基础:Java与C/C++的桥梁

各位同学,欢迎来到这门课的第一章。

做Android底层开发,JNI是绕不开的一道坎。说白了,JNI就是Java和C/C++之间的翻译官。Java那边说「我要调一个本地方法」,C/C++这边就得听懂并执行。我刚开始接触JNI时,觉得这东西不就是个接口嘛,后来踩了不少坑才明白——细节决定成败。

1.1 为什么需要JNI?

Android应用层用Java/Kotlin写,但底层库、驱动、算法优化,很多都是C/C++的天下。比如音视频编解码、OpenGL渲染、加密算法,这些场景用Java写性能跟不上,或者根本没法直接操作硬件。

JNI就是用来打通这个壁垒的。它让Java代码能调用C/C++函数,反过来C/C++也能回调Java方法。嗯,这里要注意——JNI不是Android独有的,它是Java标准的一部分。Android只是把它用到了极致。

核心理解:JNI是一套协议,定义了Java虚拟机如何与本地代码交互。没有它,Java和C/C++就是两个世界的人。

1.2 JNI函数命名规则

这个规则,我当年第一次看时觉得挺繁琐,但用久了发现——它其实很机械,完全可预测。

JNI函数的命名格式如下:

Java_包名_类名_方法名

举个例子。假设你在Java中有一个类:

package com.example.ndk;
public class NativeLib {
    public static native String stringFromJNI();
}

那么对应的C/C++函数名就是:

Java_com_example_ndk_NativeLib_stringFromJNI

规则很简单:

  • Java_ 开头
  • 包名中的点 . 替换为下划线 _
  • 类名和方法名直接拼接
  • 如果方法名中有特殊字符(比如Unicode),需要用编码转义——不过这种情况很少见

我曾经踩过的坑:包名或类名里带了数字或下划线,结果生成的函数名和预期对不上。比如包名 com.my_app,下划线会被当作分隔符处理,导致JNI找不到函数。解决办法是避免在包名中使用下划线,或者用 javah 工具自动生成头文件,别手写。

1.3 JNI数据类型映射

Java和C/C++的数据类型不一样,JNI定义了一套映射规则。我习惯把它分成两类:基本类型和引用类型。

基本类型映射

Java类型 JNI类型 C/C++类型 说明
boolean jboolean unsigned char 0表示false,非0表示true
byte jbyte signed char 8位有符号
char jchar unsigned short 16位Unicode
short jshort short 16位有符号
int jint int 32位有符号
long jlong long long 64位有符号
float jfloat float 32位浮点
double jdouble double 64位浮点

你看,基本类型的映射很直接。但引用类型就复杂一些了。

引用类型映射

Java类型 JNI类型 说明
String jstring Java字符串,需要转换才能用
Object[] jobjectArray 对象数组
byte[] jbyteArray 字节数组
Class jclass Java类对象
Throwable jthrowable 异常对象

引用类型在C/C++中都是不透明指针。你不能直接操作它们的内容,必须通过JNI提供的函数来读写。比如 jstring 要用 GetStringUTFChars 才能拿到C字符串。

个人建议:处理字符串时,记得调用 ReleaseStringUTFChars 释放内存。我见过不少新手只拿不还,导致内存泄漏。JNI的内存管理,后面章节会详细讲。

1.4 javah工具的使用

javah是JDK自带的一个工具,用来从Java类生成JNI头文件。有了它,你就不用自己手写函数声明了——省心又防错。

用法很简单。假设你的Java文件是 NativeLib.java

// 先编译Java文件
javac com/example/ndk/NativeLib.java

// 然后运行javah
javah -jni com.example.ndk.NativeLib

执行后,会生成一个 com_example_ndk_NativeLib.h 文件。内容大概是这样:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_example_ndk_NativeLib */

#ifndef _Included_com_example_ndk_NativeLib
#define _Included_com_example_ndk_NativeLib
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_example_ndk_NativeLib
 * Method:    stringFromJNI
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_example_ndk_NativeLib_stringFromJNI
  (JNIEnv *, jclass);

#ifdef __cplusplus
}
#endif
#endif

注意看,函数签名里多了两个参数:JNIEnv*jclass(静态方法)或 jobject(实例方法)。这两个参数是JNI框架自动传入的,你不用自己传。

重要提醒:javah在JDK 10以后被标记为废弃,推荐用 javac -h 替代。比如:

javac -h . com/example/ndk/NativeLib.java

效果一样,但更现代。我个人现在都用 javac -h,省得每次还要单独敲javah。

1.5 简单JNI示例

光说不练假把式。我们来写一个完整的JNI示例,从Java到C再到编译运行。

第一步:写Java类

// NativeLib.java
package com.example.ndk;

public class NativeLib {
    static {
        System.loadLibrary("native-lib");
    }

    public static native String stringFromJNI();
    public static native int add(int a, int b);
}

第二步:生成头文件

javac -h . com/example/ndk/NativeLib.java

第三步:实现C/C++代码

// native-lib.cpp
#include <jni.h>
#include <string>

extern "C" JNIEXPORT jstring JNICALL
Java_com_example_ndk_NativeLib_stringFromJNI(
    JNIEnv* env,
    jclass /* clazz */) {
    std::string hello = "Hello from C++!";
    return env->NewStringUTF(hello.c_str());
}

extern "C" JNIEXPORT jint JNICALL
Java_com_example_ndk_NativeLib_add(
    JNIEnv* env,
    jclass /* clazz */,
    jint a,
    jint b) {
    return a + b;
}

第四步:编写CMakeLists.txt

cmake_minimum_required(VERSION 3.10)
project("native-lib")

add_library(
    native-lib
    SHARED
    native-lib.cpp
)

find_library(log-lib log)
target_link_libraries(native-lib ${log-lib})

第五步:在Android中调用

// MainActivity.java
public class MainActivity extends AppCompatActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        String msg = NativeLib.stringFromJNI();
        int sum = NativeLib.add(3, 5);

        Log.d("JNI", "msg: " + msg + ", sum: " + sum);
    }
}

运行后,你会看到日志输出:msg: Hello from C++!, sum: 8。嗯,就是这么简单。

避坑指南:我曾经在 System.loadLibrary 时写错了库名,结果运行时直接崩溃。库名要和CMake中 add_library 指定的名字一致,而且不要加 lib 前缀和 .so 后缀。

1.6 知识体系总览

为了让你对本章内容有个整体印象,我画了一张图。它展示了JNI在Java和C/C++之间的位置,以及本章涉及的核心知识点。

JNI知识体系总览 Java层 声明 native 方法 System.loadLibrary() 调用本地方法 JNI桥梁 函数命名规则 · 数据类型映射 javah工具 · JNIEnv指针 C/C++层 实现JNI函数 操作jstring/jint等类型 编译为.so动态库 函数命名规则 Java_包名_类名_方法名 数据类型映射 jint → int jstring → String javah工具 自动生成头文件 javac -h 替代 JNI示例 完整流程 Java→C→编译→运行

这张图把本章的知识点串起来了。Java层声明native方法,通过JNI桥梁找到C/C++层的实现。函数命名规则和数据类型映射,就是这座桥的「交通规则」。javah工具帮你自动生成路标,而示例代码则是一次完整的通车演练。

好了,第一章就到这里。JNI的基础概念、命名规则、数据类型、javah用法,以及一个完整的示例,你都过了一遍。这些东西看着简单,但后面每一章都会用到它们。尤其是数据类型映射和函数命名,记牢了,后面写代码会顺手很多。


公众号:蓝海资料掘金营,微信deep3321