从 JVM 角度,存在两种类型的代码:“Java”和“native”, native 一般指的是 c/c++,为了使 java 和 native 端能够进行交互,java 设计了 JNI(java native interface)。 JNI 允许java虚拟机(VM)内运行的java代码与C++、C++和汇编等其他编程语言编写的应用程序和库进行互操作。
虽然大部分情况下我们的软件完全可以由 java 来实现,但是某些场景下使用 native 代码更加适合,比如:
- 代码效率:使用 native 代码的性能更高
- 跨平台特性:标准Java类库不支持应用程序所需的依赖于平台的特性,或者希望用较低级别的语言(如汇编语言)实现一小部分时间关键型代码。
native 层使用 JNI 主要可以做到:
- 创建、检查和更新Java对象(包括数组和字符串)。
- 调用Java方法。
- 加载类并获取类信息。
创建 android ndk 项目
使用 as 创建一个 native c++ 项目
文件结构如下:
可以看到生成了一个 cpp 文件夹,里面有 CMakeLists.txt, native-lib.cpp,CMakeLists后面再讲,这里先来看一下 native-lib.cpp 和 java 代码。
public class MainActivity extends AppCompatActivity {
static {
System.loadLibrary(“native-lib”);
}
…
public native String stringFromJNI();
}
#include <jni.h>
#include
extern “C” JNIEXPORT jstring JNICALL
Java_com_wangzhen_jnitutorial_MainActivity_stringFromJNI(JNIEnv* env, jobject thiz) {
std::string hello = “Hello from C++”;
return env->NewStringUTF(hello.c_str());
}
可以看到在 MainActivity 中先定义了一个 native 方法,然后编译器在 cpp 文件中创建一个一个对应的方法Java_com_wangzhen_jnitutorial_MainActivity_stringFromJNI
。 它的命名规则就是 Java_packageName_methodName。
接下来我们详细的解读一下 cpp 中的代码。
native 代码解读
extern “C”
在 c++ 中使用 c 代码
JNIEXPORT
宏定义:#define JNIEXPORT __attribute__ ((visibility ("default")))
在 Linux/Unix/Mac os/Android 这种类 Unix 系统中,定义为__attribute__ ((visibility ("default")))
GCC 有个visibility属性, 该属性是说, 启用这个属性:
- 当-fvisibility=hidden时,动态库中的函数默认是被隐藏的即 hidden。
- 当-fvisibility=default时,动态库中的函数默认是可见的。
JNICALL
宏定义,在 Linux/Unix/Mac os/Android 这种类 Unix 系统中,它是个空的宏定义: #define JNICALL
,所以在 android 上删除它也可以。 快捷生成 .h 代码
JNIEnv
- JNIEnv类型实际上代表了Java环境,通过这个 JNIEnv* 指针,就可以对 Java 端的代码进行操作:
- 调用 Java 函数
- 操作 Java 对象
- JNIEnv 的本质是一个与线程相关的结构体,里面存放了大量的 JNI 函数指针:
struct _JNIEnv {
/**
- 定义了很多的函数指针
*/
const struct JNINativeInterface functions;
#if defined(__cplusplus)
/// 通过类的名称(类的全名,这时候包名不是用.号,而是用/来区分的)来获取jclass
jclass FindClass(const char* name) { return functions->FindClass(this, name); }
…
}
JNIEnv 的结构图如下:
JavaVM
-
JavaVM : JavaVM 是 Java虚拟机在 JNI 层的代表, JNI 全局只有一个
-
JNIEnv : JavaVM 在线程中的代表, 每个线程都有一个, JNI 中可能有很多个 JNIEnv,同时 JNIEnv 具有线程相关性,也就是 B 线程无法使用 A 线程的 JNIEnv
JVM 的结构图如下:
jobject thiz
这个 object 指向该 native 方法的 this 实例,比如我们在 MainActivity 调用的下面的 native 函数中打印一下 thiz 的 className:
#define LOGE(…) __android_log_print(ANDROID_LOG_ERROR,“JNI”,VA_ARGS);
extern “C” JNIEXPORT jstring JNICALL
Java_com_wangzhen_jnitutorial_MainActivity_stringFromJNI(JNIEnv *env, jobject thiz) {
std::string hello = “Hello from C++”;
// 1. 获取 thiz 的 class,也就是 java 中的 Class 信息
jclass thisclazz = env->GetObjectClass(thiz);
// 2. 根据 Class 获取 getClass 方法的 methodID,第三个参数是签名(params)return
jmethodID mid_getClass = env->GetMethodID(thisclazz, “getClass”, “()Ljava/lang/Class;”);
// 3. 执行 getClass 方法,获得 Class 对象
jobject clazz_instance = env->CallObjectMethod(thiz, mid_getClass);
// 4. 获取 Class 实例
jclass clazz = env->GetObjectClass(clazz_instance);
// 5. 根据 class 的 methodID
jmethodID mid_getName = env->GetMethodID(clazz, “getName”, “()Ljava/lang/String;”);
// 6. 调用 getName 方法
jstring name = static_cast(env->CallObjectMethod(clazz_instance, mid_getName));
LOGE(“class name:%s”, env->GetStringUTFChars(name, 0));
return env->NewStringUTF(hello.c_str());
}
打印结果如下: