JNI 编程是高级/专家 Android 开发的必备技能之一,接下来咱们就一步一步把握 JNI 编程的方方面面。
本文示例代码能够在 github.com/yuandaimaah… 这儿下载到
1. 根本概念
JNI(Java Native Interface,JAVA 原生接口)。JNI 是本地编程接口,它使得在 Java 虚拟机 (VM) 内部运行的 Java 代码能够与用其它编程言语(如 C、C++ 和汇编言语)编写的运用程序和库进行互操作。浅显一点讲就是在 Java 代码里调用 C/C++ 等言语的代码或 C/C++ 代码调用 Java 代码。
JNI 技能在 Android 范畴有大量的运用:
- Java 程序能够通过 JNI 操作硬件
- 音视频处理,数学运算,实时烘托等范畴相关的库根本都运用 C/C++ 编写,咱们能够运用 JNI 技能来调用这些库,而不必运用 Java 来重写
- 比较 C/C++,Java 更容易被反编译,一些和安全相关的代码,咱们能够运用 C/C++ 来编写,让后运用 JNI 技能调用
JNI 技能的运用当然不止以上罗列的比如,更多的运用办法等待咱们的进一步探索。
2. HelloWorld 实战
接下来咱们通过一个简单的示例程序,快速地把握 JNI 的根本运用。
首要编译一个 Java 文件: HelloJNI.java
public class HelloJNI {
static {
System.loadLibrary("hello");
}
private native void sayHello();
public static void main(String[] args) {
new HelloJNI().sayHello();
}
}
接着生成 C/C++ 头文件 HelloJNI.h
javac -h . HelloJNI.java
该指令会生成一个 HelloJNI.h,这个头文件描述了咱们需求完结的函数。
/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HelloJNI */
#ifndef _Included_HelloJNI
#define _Included_HelloJNI
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: HelloJNI
* Method: sayHello
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
-
生成的函数中有两个参数:
-
JNIEnv:JNIEnv 内部供给了许多函数,便利咱们进行 JNI 编程。
C 代码中,JNIEnv 是指向 JNINativeInterface 结构的指针,为了拜访任何一个 JNI 函数,该指针需求首要被解引证。因为 C 代码中的 JNI 函数不了解当时的 JNI 环境, JNIEnv 实例应该作为第一个参数传递给每一个 JNI 函数调用调用者,调用格式如下:
(*env)->NewStringUTF(env,"Hello from JNI !");在 C++ 代码中,JNIEnv 实际上是 C++ 类实例,JNI 函数以成员函数的形式存在,因而 JNI 函数调用不要求 JNIEnv 实例作参数。在 C++ 中,完结同样功用的调用代码格式如下:
env->NewstringUTF ( "Hello from JNI ! ");
-
-
jobject: 指向 “this” 的 Java 对象
-
假如 java 中的 native 函数是 static 的,那第二个参数是 jclass,代表了 java 中的 Class 类。
-
extern “C” 告诉 C++ 编译器以 C 的办法来编译这个函数,以便利其他 C 程序链接和拜访该函数。C 和 C++ 有着不同的命名协议,因为 C++ 支持函数重载,用了不同的命名协议来处理重载的函数。在 C 中函数是通过函数名来辨认的,而在 C++ 中,因为存在函数的重载问题,函数的辨认办法通过函数名,函数的回来类型,函数参数列表三者组合来完结的。因而两个相同的函数,经过C,C++编绎后会产生完全不同的姓名。所以,假如把一个用 C 编绎器编绎的目标代码和一个用 C++ 编绎器编绎的目标代码进行链接,就会出现链接失败的过错。
-
JNIEXPORT、JNICALL 两个宏在 linux 渠道的界说如下:
//该声明的作用是确保在本动态库中声明的办法 , 能够在其他项目中能够被调用 #define JNIEXPORT __attribute__ ((visibility ("default"))) //一个空界说 #define JNICALL
接着咱们来完结详细的 C 程序 HelloJNI.c
#include "HelloJNI.h"
#include <stdio.h>
#include <jni.h>
//办法名要和 Java 层包名对应上
JNIEXPORT jstring JNICALL Java_HelloJNI_sayHello(JNIEnv *env, jobject obj)
{
return (*env)->NewStringUTF(env,"Hello from JNI !");
}
编译和履行(需求装备好 JAVA_HOME 环境变量):
gcc -fpic -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" -shared -o libhello.so HelloJNI.c
java -Djava.library.path=. HelloJNI
至此,一个简单的 demo 就完结了。
3. 动态注册
以上运用 JNI 的办法称为静态注册,还有一种办法叫动态注册,咱们接下来看个动态注册的比如吧
java层: com/example/ndk/NativeTest.java
package com.example.ndk;
public class NativeTest {
static {
System.loadLibrary("nativetest");
}
public native void init();
public native void init(int age);
public native boolean init(String name);
public native void update();
}
C 层的完结首要有三步:
- 完结 java 层本地办法
- 构建一个 JNINativeMethod 类型的数组
- 注册本地函数
NativeTest.c :
#include <jni.h>
#include <stdio.h>
#ifdef __cplusplus
extern "C" {
#endif
//1 完结 java 层本地办法
JNIEXPORT void JNICALL
c_init1(JNIEnv *env, jobject thiz) {
printf("c_init1\n");
}
JNIEXPORT void JNICALL
c_init2(JNIEnv *env, jobject thiz, jint age) {
printf("c_init2\n");
}
JNIEXPORT jboolean JNICALL
c_init3(JNIEnv *env, jobject thiz, jstring name) {
printf("c_init3\n");
}
JNIEXPORT void JNICALL
c_update(JNIEnv *env, jobject thiz) {
printf("c_update\n");
}
#ifdef __cplusplus
}
#endif
// typedef struct {
// //Java层native办法称号
// const char* name;
// //办法签名
// const char* signature;
// //native层办法指针
// void* fnPtr;
// } JNINativeMethod;
//2 构建 JNINativeMethod 数组
//中心的办法签名看上去有点奇怪,后边咱们来讲它的命名规矩
static JNINativeMethod methods[] = {
{"init", "()V", (void *)c_init1},
{"init", "(I)V", (void *)c_init2},
{"init", "(Ljava/lang/String;)Z", (void *)c_init3},
{"update", "()V", (void *)c_update},
};
/**
* 3 完结动态注册的进口函数
* 其内容根本固定
*/
JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env = NULL;
jint result = -1;
// 获取JNI env变量
if (vm->GetEnv((void**) &env, JNI_VERSION_1_6) != JNI_OK) {
// 失败回来-1
return result;
}
// 获取native办法地点类
const char* className = "com/example/ndk/NativeTest";
jclass clazz = env->FindClass(className);
if (clazz == NULL) {
return result;
}
// 动态注册native办法
if (env->RegisterNatives(clazz, methods, sizeof(methods) / sizeof(methods[0])) < 0) {
return result;
}
// 回来成功
result = JNI_VERSION_1_6;
return result;
}
JNINativeMethod 第二个成员变量是办法签名,它的组成规矩为:
(参数类型标识1参数类型标识2…参数类型标识n)回来值类型标识
其间的类型标识如下图所示:
| 类型标识 | Java数据类型 |
|---|---|
| Z | boolean |
| B | byte |
| C | char |
| S | short |
| I | int |
| J | long |
| F | float |
| D | double |
| L包名/类名; | 各种引证类型 |
| V | void |
编译和履行:
cd com/example/ndk
javac NativeTest.java
#回到项目根目录
cd -
g++ -fpic -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" -shared -o libnativetest.so NativeTest.c
java -Djava.library.path=. com.example.ndk.NativeTest
参考资料
- JNI/NDK入门攻略之正确姿态了解JNI和NDK
- JNI 简明教程之手把手教你入门
- cross-compiling-for-android-with-the-ndk
- Android 官方cmake文档
- JNI/NDK开发攻略
- 关于 C++ 中的 extern “C”
关于
我叫阿豪,2015 年本科毕业于国防科技大学指挥自动化专业,毕业后,从事信息化装备的研制作业。首要研究方向为 Android Framework 与 Linux Kernel,2023年春节后开端做 Android Framework 相关的技能共享。
假如你对 Framework 感兴趣或许正在学习 Framework,能够参考我总结的Android Framework 学习道路攻略,也可重视我的微信大众号,我会在大众号上继续共享我的经验,协助正在学习的你少走一些弯路。学习过程中假如你有疑问或许你的经验想要共享给咱们能够增加我的微信,我拉你进技能交流群。

