NDK 基础知识–JNI java与c++

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语法。这应该就是以后编程语言发展趋势吧,让我们少做点,电脑多做的)

好了,废话不说了,正片开始

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;
}

以上就是动态注册的简单过程。

文章如有解释不对的地方,往大佬指点

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章