NDK 基础知识–JNI
开发环境: Android studio v3.6.1
(3.6.0都支持kotlin与c/c++互相调用,是该学学NDK了,不能再找理由了)
NDK 可以让我们Android 应用中使用C、C++代码。以前Android 都是使用java,NDK中包含JNI (java本地接口)可以使用java 调用c、c++等。如今kotlin被Android 官方宣布第一开发语言。kotlin与java是100%兼容的(我认为kotlin、java都依靠jvm,他们都要编译成java字节码,kotlin只是利用它的编译器特性,简化了java语法。这应该就是以后编程语言发展趋势吧,让我们少做点,电脑多做的)
好了,废话不说了,正片开始
文章目录
- NDK 基础知识--JNI
- 1.native方法
- 2.获取java中的属性和方法(类似java反射)
- 3. JNI_OnLoad、动态注册
1.native方法
在java 文件中声明一个native方法
//Test.java
package com.wkk.ndkdemo;
public class Test {
//声明native方法,不用实现,方法实现代码在c或c++中
native void test();
}
如果是kotlin 则是external
关键词
external fun test()
c++ 文件
#include <jni.h>
extern "C"
JNIEXPORT void JNICALL
Java_com_wkk_ndkdemo_Test_test(JNIEnv *env, jobject thiz) {
}
这里c++的方法名特么长Java_com_wkk_ndkdemo_Test_test
这个名字是有固定语法的
方法的第一个参数是JNIEnv
指针, JNIEnv 是一个结构体,定义了许多与java的方法。
方法的第二个参数jobject
表示调用test()方法的对象。test()方法是成员方法,如果是个静态方法,则第二个参数就是jclass
表示当前方法的class ,因为静态方法属于类。
上面看到jobject 类型对应java中Object ,jni定义一写类型和java类型对应起来,如下
//都是java基本类型前面加个j
/* Primitive types that match up with Java equivalents. */
typedef uint8_t jboolean; /* unsigned 8 bits */ //---->java 中boolean
typedef int8_t jbyte; /* signed 8 bits */ //---->java byte
typedef uint16_t jchar; /* unsigned 16 bits */
typedef int16_t jshort; /* signed 16 bits */
typedef int32_t jint; /* signed 32 bits */
typedef int64_t jlong; /* signed 64 bits */
typedef float jfloat; /* 32-bit IEEE 754 */
typedef double jdouble; /* 64-bit IEEE 754 */
typedef _jobject* jobject; // 对应java中的类对象
typedef _jclass* jclass; // 对应java中的clas
typedef _jstring* jstring;//对应java中的string
typedef _jobjectArray* jobjectArray;
typedef _jbooleanArray* jbooleanArray;
…… 省略更多请看jni.h文件中的定义
类如在java中参数为Int类型的方法
native void test1(int number);
则对应的c++
extern "C"
JNIEXPORT void JNICALL
Java_com_wkk_ndkdemo_Test_test1(JNIEnv *env, jobject thiz, jint number) {
}
可以看到在java中int类型参数,在C++中对应的是jint方法
2.获取java中的属性和方法(类似java反射)
public class User {
private String name="Jack";
private int age=20;
public String getName() {
Log.i("User","who在调用我");
return name;
}
public void setName(String name) {
Log.i("User","啊!我被人在调用了");
this.name = name;
}
public native void test();
}
c++代码
extern "C"
JNIEXPORT void JNICALL
Java_com_wkk_ndkdemo_User_test(JNIEnv *env, jobject user) {
//通过对象获取类jclass
jclass userCls = env->GetObjectClass(user);
//———————————————获取属性值———————————————————
//获取属性id 0️⃣
jfieldID ageId = env->GetFieldID(userCls, "age", "I");
//通过属性id获取属性值
jint ageValue = env->GetIntField(user, ageId);
//要想在logcat看到值需要引入android/log.h头文件,使用下面方法打印
__android_log_print(ANDROID_LOG_INFO,"User","c++,ageValue%d",ageValue);
//-----------------------
//同理获取name属性值 1️⃣
jfieldID nameId = env->GetFieldID(userCls, "name", "Ljava/lang/String;");
//通过属性id获取属性值 String属性
jstring nameValue = static_cast<jstring>(env->GetObjectField(user, nameId));
//———————————————调用方法,无参数———————————————————
//获取方法id /2️⃣3️⃣4️⃣5️⃣6️⃣
jmethodID getNameId = env->GetMethodID(userCls, "getName", "()Ljava/lang/String;");
//调用getName方法
jobject callGetName = env->CallObjectMethod(user, getNameId);
//———————————————调用方法,有参数———————————————————
//获取方法id3️⃣
jmethodID setNameId = env->GetMethodID(userCls, "setName", "(Ljava/lang/String;)V");
//调用getName方法4️⃣
env->CallVoidMethod(user, setNameId, env->NewStringUTF("abc"));
//———————————————创建新对象———————————————————
//获取构造方法id5️⃣
jmethodID initID = env->GetMethodID(userCls, "<init>", "()V");
//通过c++代码创建user对象
jobject newUser = env->NewObject(userCls, initID);
//c/c++没有垃圾回收机制,需要自己释放内存
env->DeleteLocalRef(newUser);
}
在0️⃣ 使用env->GetFieldID方法获取属性id,此方法需要三个参数,第一个是jclass,第二个属性名,第三个参数写了个"I"
表示int类型签名,对应关系如下表所示,最新版的Android studio 已经有自动补全功能,当你填写第二个属性时,编辑器会自动补全第三个参数类型签名
java | 签名 |
int | I |
char | C |
byte | B |
long | L |
float | F |
boolean | Z |
String(类) | Ljava/lang/String; |
自己写的类 | L类的全路径; |
例如com.wkk.ndkdemo包下面有Data.java 则它的签名为Lcom/wkk/ndkdemo/Data;
在2️⃣处使用env->GetMethodID获取某个方法的id,最后一个参数为方法签名,就是把原方法用类型签名表示,如上面的String getName()
无参数,返回数据类型为String,所以方法签名是()Ljava/lang/String;
在3️⃣出 setName 有参数,参数是String类型,无返回值,所以方法签名为(Ljava/lang/String;)V
返回值为viod 用V表示。
在4️⃣ ,通过CallVoidMethod方法调用setName方法,调用java方法都是CallXXXMethod ,xxx表示方法的返回值类型。此类方法前两个参数分别是jobject,jmethodID,最后一个为可以变参数。被调用的Java方法参数,setName的参数是String类型,因为是Java方法,要通过NewStringUTF把字符串转换为java可以使用的类型。
我们在c++中可以创建java对象,在5️⃣,调用类的构造方法,每个类的构造方法名都是“"” 并不是像java那样和类名一样。通过NewObject创建对象,最后一个参数也是可变参数,添加构造方法的参数值
3. JNI_OnLoad、动态注册
上面忘记说了要想使用c++需要加载C++库
在静态代码块中调用
static {
System.loadLibrary("native-lib")
}
如果是kotlin
companion object{
init {
System.loadLibrary("native-lib")
}
}
当调用System.loadLibrary()
·方法加载库时,如果库中有jint JNI_OnLoad(JavaVM* vm, void* reserved);
方法就会先执行这个方法
上面介绍java方法与C++方法关联的方式一般称为静态注册。还有一种动态注册,就是利用JNI_OnLoad 实现。
java 代码
package com.wkk.jnidemo;
public class Data {
public native int test();
public native int test2(int a);
}
c++代码
#include <jni.h>
//如果用不到JNIEnv jobject 两个参数可以省略不写
jint test(JNIEnv *env, jobject data) {
return 10;
}
jint test2(JNIEnv *env, jobject data, jint a) {
return 1 + a;
}
static const char *CLASS_NAME = "com/wkk/jnidemo/Data";
//JNINativeMethod 是一个结构体
static const JNINativeMethod methods[] = {
{
"test",//java中的方法名
"()I",//对应的方法签名
(void *) test//c++中对应的函数指针 转化为void 指针
},
{
"test2",
"(I)I",
(void *) test2
}
};
jint JNI_OnLoad(JavaVM *vm, void *reserved) {
JNIEnv *env = NULL;
//获取JNIEnv指针
jint status = vm->GetEnv((void **) &env, JNI_VERSION_1_6);
if (status != JNI_OK) {
//如果获取失败 return -1 结束
return -1;
}
//通过env的FindClass方法通过类名获取jclass
jclass dataClass = env->FindClass(CLASS_NAME);
//注册方法,把java native方法与c++方法关联
//第一个参数为对应的jclass
//第二个参数是JNINativeMethod 数据组
//第三个参数表示注册方法的个数
jint registerNativesStatus=env->RegisterNatives(dataClass,methods, sizeof(methods)/ sizeof(JNINativeMethod));
if (registerNativesStatus != JNI_OK) {
return -1;
}
return JNI_VERSION_1_6;
}
以上就是动态注册的简单过程。
文章如有解释不对的地方,望大佬指点