1. 对 JNI 的了解
java因其跨平台的特性导致其本地交互能力不够,为了便于 Java 与本地代码(C、C++)交互,所以提供了JNI( Java Native Interface)。故名思意,JNI提供的就是将Java层的需求(native 方法)转变成C语言中的接口类,然后由 C 去实现具体方法的功能。
NDK (Native Develop Kit),是 Android 提供的本地开发工具的集合。其优点有:
- 提高代码安全性(so 库反编译困难)
- 方便目前已有的C/C++库
- 便于平台移植
- 提高某些情形下的程序执行效率
2. JNI 函数的注册方法
静态方法:
- 创建Java类,声明 native 方法
- javah 生成头文件 .h文件的作用
- 创建 C/C++ 文件,实现对应的native方法
如何连接 Java 层方法和 native 层方法的:
Java方法被调用时,JVM会生成对应的 native 方法名,例如 com.example.StrHelper.getStr() ,JVM会在JNI库中查找 Java_com_example_StrHelper_getStr 函数,如果找到了,就会保存一个该 JNI 函数的指针,直接调用该指针。如果没找到就会报错。
上代码:
1.准备工作,
Android Studio
中安装好NDK、 CMAK、 LLDB
工具
2.起一个新的项目,在选择Activity
类型的时候直接选择最后的Native C++
类型一路next
下去,这样你就不用配置gradle
等文件了,最简单。
3.创建一个java文件,用来声明native方法
package com.cn.jnitest;
public class NativeHelper {
static {
System.loadLibrary("native-lib");//一定要确保在调用native方法前加载了so库
}
public static native String getAppKeys();
}
4.
javac xxx.java
或者build
生成class
文件
5.将.class
生成.h
文件,javah -cp
。如果有问题-jni com.cn.jnitest.NativeHelper
/* DO NOT EDIT THIS FILE - it is machine generated */
#include
/* Header for class com_cn_jnitest_NativeHelper */
#ifndef _Included_com_cn_jnitest_NativeHelper
#define _Included_com_cn_jnitest_NativeHelper
#ifdef __cplusplus
extern "C" {
#endif
JNIEXPORT jstring JNICALL Java_com_cn_jnitest_NativeHelper_getAppKeys
(JNIEnv *, jclass);
#ifdef __cplusplus
}
#endif
#endif
6.最后就是实现native方法了,创建一个C++文件,最好保持名字和.h文件一致
#include
#include "com_cn_jnitest_NativeHelper.h"
JNIEXPORT jstring JNICALL Java_com_cn_jnitest_NativeHelper_getAppKeys(JNIEnv *env, jclass type)
{
char* app_key = "5465465416948";
return env->NewStringUTF(app_key);
}
再在CMakeLists.txt中加入
add_library(
native-lib
SHARED
native-lib.cpp
com_cn_jnitest_NativeHelper.cpp)//这个.cpp文件
就大功告成了,直接运行,可以调用native方法来获取string了
你也可以在原有项目上添加JNI代码,只不过需要改一下gradle 配置和添加CMakeLists.txt 文件
如下
//build.gradle 文件中最少指定 CMakeLists.txt 的位置
externalNativeBuild {
cmake{
path "src/main/jni/CMakeLists.txt"
}
}
//CMakeLists.txt 指定cpp文件即可
add_library(
main
SHARED
hun.cpp)
然后就可以了,其实还有一些其他的设置项,日后再出一篇来讲解。
弊端:
- 编写不方便,JNI方法名字遵循规则,很长
- 编写过程步骤太多,每个声明 native 方法的类都要生成一个 .h 头文件。
- 初次调用需要在JNI 层根据函数名查找建立对应关系,耗时
动态注册:
- 创建Java类,声明native方法
- 创建对应 C++ 类,在该类中实现
JNI_OnLoad
方法,定义JNINativeMethod
列表,以及Java native方法具体实现(此时,方法的名称可以是任意的)
通过System.LoadLibrary()
加载so库的时候,JVM会调用JNI_OnLoad
方法,而我们可以通过在该方法中调用JNIEnv->RegisterNatives()
方法将我们的native方法声明注册到JNI中,那是如何将native方法与Java方法联系起来的呢,就是通过JNINativeMethod
结构体将两者联系起来的。
上代码:
typedef struct {
const char* name;//这个是java层函数的名字
const char* signature;//这个是Java层函数的签名,其他两个很好理解,这个函数签名是个啥???下面会讲
void* fnPtr;//这个是native层函数的名字
} JNINativeMethod;//这就是结构体的主要内容,然后我们怎么写呢
TestJni.java
package com.cn.mydynamic;
public class TestJni {
static {
System.loadLibrary("main");
}
public native String sayHello();//定义了一个native方法
}
上面的JNINativeMethod结构体中的第二项,Java层函数签名,就是按照一定的规则,将java层函数的参数返回值进行转化,为啥整出个这玩意儿?因为java支持函数重载,仅凭函数名称是找不对对应函数的,所以就用参数和返回值结合函数名称来找。
规则如下:
当参数的类型是引用类型时,其格式是" L包名;",其中包名中的"." 换成"/"。
很容易写错,但是可以通过javap -s -p xxx.class
直接生成转换好的签名,上述的TestJni转换后为
>javap -s -p TestJni.class
Compiled from "TestJni.java"
public class com.cn.mydynamic.TestJni {
public com.cn.mydynamic.TestJni();
descriptor: ()V
public native java.lang.String sayHello();
descriptor: ()Ljava/lang/String;
static {};
descriptor: ()V
}
有了签名有了Java 函数,有了native函数,就可以放进结构体里了
//建立Java层函数与native层函数的对应关系
static const JNINativeMethod getMethod[] = {
{"sayHello",
"()Ljava/lang/String;",
(void*)sya_hello
}
};
JNIEXPORT jstring JNICALL sya_hello
(JNIEnv *env, jobject job)
{
char* app_key = "不要回答!!!不要回答!!!";
return env->NewStringUTF(app_key);
}
接下啦就要把对应关系注册上
#define NELEM(m) (sizeof(m) / sizeof((m)[0]))
jint JNI_OnLoad(JavaVM* vm, void* reserved) {
JNIEnv* env = NULL;
if (vm ->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
return -1;
}
assert(env != NULL);
jclass clazz;
clazz = env->FindClass("com/cn/mydynamic/TestJni");
if (clazz == NULL) {
return -1;
}
if (env->RegisterNatives(clazz, getMethods, NELEM(getMethods)) < 0) {
return -1;
}
return JNI_VERSION_1_4;
};
运行结果