3.【干货】火爆全网的《超全NDK精品教程》JNI c调用java和java调用c( Env 和函数签名)

C调用JAVA


目录:

1.JNIENV

2.函数签名

3.c调用java

4.java调用c

5. java和c的对应关系!

1. JNIEnv分析

Env: c中和c++的区别

Env:c 与 java 相互调用的桥梁,是一个线程相关的结构体,该结构体代表了Java在本线程的执行环境

可以理解为我们java中的上下文参数

typedef const struct JNINativeInterface*C_JNIEnv;

#if defined(__cplusplus)

typedef _JNIEnv JNIEnv;

定义一个结构体,结构体指针

 在c中,           使用: JNIEnv *env,  2级指令

在C++中      *env:   1级指令, 只有1级指针才能用---->可以用lamda表达式

在C的定义中,env是一个两级指针,而在C++的定义中,env是个一级指针

举例: c++     return env->NewStringUTF(hello.c_str());

c中:     return (*env--->)NewStringUTF(hello.c_str());

意思就是把c中的字符串传给java

https://blog.csdn.net/zhangmiaoping23/article/details/103855018

原理: 

need-to-insert-img

我们知道 ,JNIEnv是JNINativeInterface_结构体的指针别名 , 在JNINativeInterface_结构体中 , 定义很多操作函数 。例如:

jstring(JNICALL*NewStringUTF)(JNIEnv*env,constchar*utf);jsize(JNICALL*GetStringUTFLength)(JNIEnv*env,jstring str);constchar*(JNICALL*GetStringUTFChars)(JNIEnv*env,jstring str,jboolean*isCopy);void(JNICALL*ReleaseStringUTFChars)(JNIEnv*env,jstring str,constchar*chars);

由上述函数可以看出,每个函数都需要一个JNIEnv指针,但是为什么需要呢 ?

有两点:

第一:函数需要 , 在函数中仍然需要JNINativeInterface_结构体中的函数做处理

第二:区别对待C和C++

JNIEnv类型实际上代表了Java环境,通过JNIEnv*指针就可以对Java端的代码进行操作。比如我们可以使用JNIEnv来创建Java类中的对象,调用Java对象的方法,获取Java对象中的属性等

基本用法:

// 得到jclassjclass jcls=(*env)->GetObjectClass(env,jobj);

// com.zeno.jni_HelloJNI.hJNIEXPORTvoidJNICALL Java_com_zeno_jni_HelloJni_accessJavaStringField(JNIEnv*,jobject);// Hello_JNI.c/*C语言访问java String类型字段*/JNIEXPORTvoidJNICALL Java_com_zeno_jni_HelloJni_accessJavaStringField(JNIEnv*env,jobject jobj){// 得到jclassjclass jcls=(*env)->GetObjectClass(env,jobj);// 得到字段IDjfieldID jfID=(*env)->GetFieldID(env,jcls,"name","Ljava/lang/String;");// 得到字段的值jstring jstr=(*env)->GetObjectField(env,jobj,jfID);// 将jstring类型转换成字符指针char*cstr=(*env)->GetStringUTFChars(env,jstr,JNI_FALSE);//printf("is vaule:%s\n", cstr);// 拼接字符chartext[30]="  xiaojiu and ";strcat(text,cstr);//printf("modify value %s\n", text);// 将字符指针转换成jstring类型jstring new_str=(*env)->NewStringUTF(env,text);// 将jstring类型的变量 , 设置到java 字段中(*env)->SetObjectField(env,jobj,jfID,new_str);}

如果native方法是static, obj就代表native方法的类的class 对象实例(static 方法不需要类实例的,所以就代表这个类的class对象)。

举一个简单的例子:我们在TestJNIBean中创建一个静态方法testStaticCallMethod和非静态方法testCallMethod,我们看在cpp文件中该如何编写?

1public class TestJNIBean{

2    public static final String LOGO = "learn android with aserbao";

3    static {

4        System.loadLibrary("native-lib");

5    }

6    public native String testCallMethod();  //非静态

7

8    public static native String testStaticCallMethod();//静态

9

10    public  String describe(){

11        return LOGO + "非静态方法";

12    }

13

14    public static String staticDescribe(){

15        return LOGO + "静态方法";

16    }

17}

1extern "C"

2JNIEXPORT jstring JNICALL

3Java_com_example_androidndk_TestJNIBean_testCallMethod(JNIEnv *env, jobject instance) {

4    jclass  a_class = env->GetObjectClass(instance);                                  //因为是非静态的,所以要通过GetObjectClass获取对象

5    jmethodID  a_method = env->GetMethodID(a_class,"describe","()Ljava/lang/String;");// 通过GetMethod方法获取方法的methodId.

6    jobject jobj = env->AllocObject(a_class);                                        // 对jclass进行实例,相当于java中的new

7    jstring pring= (jstring)(env)->CallObjectMethod(jobj,a_method);                // 类调用类中的方法

8    char *print=(char*)(env)->GetStringUTFChars(pring,0);                          // 转换格式输出。

9    return env->NewStringUTF(print);

10}

11

12extern "C"

13JNIEXPORT jstring JNICALL

14Java_com_example_androidndk_TestJNIBean_testStaticCallMethod(JNIEnv *env, jclass type) {

15    jmethodID  a_method = env->GetMethodID(type,"describe","()Ljava/lang/String;"); // 通过GetMethod方法获取方法的methodId.

16    jobject jobj = env->AllocObject(type);                                          // 对jclass进行实例,相当于java中的new

17    jstring pring= (jstring)(env)->CallObjectMethod(jobj,a_method);                // 类调用类中的方法

18    char *print=(char*)(env)->GetStringUTFChars(pring,0);                          // 转换格式输出。

19    return env->NewStringUTF(print);

20}

上面的两个方法最大的区别就是静态方法会直接传入jclass,从而我们可以省去获取jclass这一步,而非静态方法传入的是当前类

谈谈你对JNIEnv 和JavaVM 理解?

2.JNIEnv

JNIEnv 表示Java 调用native 语言的环境,是一个封装了几乎全部JNI 方法的指针。

JNIEnv 只在创建它的线程生效,不能跨线程传递,不同线程的JNIEnv 彼此独立。

native 环境中创建的线程,如果需要访问JNI,必须要调用AttachCurrentThread关联,并使用DetachCurrentThread 解除链接。

C++子线程调用Java方法

我们通常都是在C++主线程中调用java方法,很简单,但是在子线程中调用java方法却不能采用在主线程中调用的方式,

因为调用java方法是要用JNIEnv去调用的,但是JNIEnv是线程相关的,子线程中不能直接使用创建线程的JNIEnv,

所以需要JVM进程相关的,可以通过JVM来获取当前线程的JNIEnv,然后就可以调用java方法了。


通过JVM获取JniEnv:JNIEnv*env=jvm->AttachCurrentThread(&env,0);

jvm->DetachCurrentThread();

JavaVM:JavaVM是Java虚拟机在JNI层的代表,JNI全局仅仅有一个

JNIEnv:JavaVM 在线程中的代码,每个线程都有一个,JNI可能有非常多个JNIEnv;

JavaVM 是虚拟机在JNI 层的代表,一个进程只有一个JavaVM,所有的线程共用一个JavaVM。

什么时候使用?动态注册

————————————————

主要步骤就是,通过运行时函数JNI_OnLoad初始化JavaVM,再通过JavaVM调用AttachCurrentThread函数初始化当前线程的JNIEnv,

此时这个JNIEnv就可以调用java方法了,调用完后要调用JavaVM的DetachCurrentThread函数来释放JavaVM引用。

--------------------------------------------------------------------------------

2.为什么JNI中突然多出了一个概念叫"签名"?----(因为java支持重载,c不支持,但是c++支持)

因为Java是支持函数重载的,也就是说,可以定义相同方法名,但是不同参数的方法,然后Java根据其不同的参数,

找到其对应的实现的方法。这样是很好,所以说JNI肯定要支持的,那JNI要怎么支持那,如果仅仅是根据函数名,

没有办法找到重载的函数的,所以为了解决这个问题,JNI就衍生了一个概念——"签名",即将参数类型和返回值类型的组合。

如果拥有一个该函数的签名信息和这个函数的函数名,我们就可以顺序的找到对应的Java层中的函数了。

3. 方法的签名是如何定义的?

方法签名具体方法:

获取方法的签名比较麻烦一些,通过下面的方法也可以拿到属性的签名。

打开命令行,输入javap

注意:

类描述符开头的 'L' 与结尾的 ';' 必须要有

数组描述符,开头的 '[' 必须要有

方法描述符规则: "(各参数描述符)返回值描述符",其中参数描述符间没有任何分隔符号

从上表可以看出, 基本数据类型的签名基本都是单词的首字母大写, 但是boolean和long除外因为B已经被byte占用, 而long也被Java类签名的占用.

对象和数组的签名稍微复杂一些.

对象的签名就是对象所属的类签名, 比如String对象, 它的签名为Ljava/lang/String; .

数组的签名为[+类型签名, 例如byte数组. 其类型为byte, 而byte的签名为B, 所以byte数组的签名就是[B.同理可以得到如下的签名对应关系:

第八: java创建c层对象

Native 构建对象如何与 Java 层对应

一般返回一个地址: 

4. C调用java的方法, 变量, 构造函数, 对象

第七: c创建java对象 ;   如何对属性进行赋值(源码就是parcelbale) 

解决办法:  使用函数NewObject可以用来创建JAVA对象;

原理使用了6: java的构造方法! 

创建一个native对象,然后得到一个对象的地址

JNIEXPORT void JNICALL Java_com_example_jni_1test_sayHello(JNIEnv * evn, jobject obj)

{

    //获取java的Class

    jclass my_class=evn->FindClass("com/example/Person");

    //获取java的Person构造方法id---构造函数的函数名为,返回值为void

    jmethodID init_id=evn->GetMethodID(my_class,"","(Ljava/lang/String;I)V");//(类,属性名.签名)    


    //创建Person对象--使用NewObject方法

    jobject person=evn->NewObject(my_class,init_id, (evn)->NewStringUTF("mike"),20);


    //获取Person的Desc方法id

     jmethodID desc_id=evn->GetMethodID(my_class,"Desc","()V");


     //调用创建的person里的desc方法

     evn->CallVoidMethod(person,desc_id)

}

第六. .间接访问Java类的父类的方法。(java是做不到的)

但是通过底层C的方式可以间接访问到父类Human的方法,跳过子类的实现,甚至你可以直接哪个父类(如果父类有多个的话),这是Java做不到的!

必须知道父类的类名! 

jclass cls = (*env)->GetObjectClass(env, jobj);

//获取man属性(对象)

jfieldID fid = (*env)->GetFieldID(env, cls, "human", "Lcom/haocai/jni/Human;");

//获取

jobject human_obj = (*env)->GetObjectField(env, jobj, fid);

//执行sayHi方法

jclass human_cls = (*env)->FindClass(env, "com/haocai/jni/Human");

jmethodID mid = (*env)->GetMethodID(env, human_cls, "sayHi", "()V");

//执行Java相关的子类方法

(*env)->CallObjectMethod(env, human_obj, mid);

//执行Java相关的父类方法

(*env)->CallNonvirtualObjectMethod(env, human_obj, human_cls, mid);

第五. .访问Java类的构造方法。

和之前的区别: FindClass, NewObject, 构造方法用

jclasscls = (*env)->FindClass(env, "java/util/Date");

//jmethodID

jmethodIDconstructor_mid= (*env)->GetMethodID(env, cls,"","()V");

//实例化一个Date对象(可以在constructor_mid后加参)

jobjectdate_obj =  (*env)->NewObject(env, cls, constructor_mid);

//调用getTime方法

jmethodIDmid = (*env)->GetMethodID(env, cls, "getTime", "()J");

jlongtime = (*env)->CallLongMethod(env, date_obj, mid);

第四. 获取java的静态方法

总结: 访问静态方法和非静态方法的区别: 调用的方法不一样! 

共同点: 流程都一样. 要获取类, 获取方法的Id

//Jclass

jclass cls = (*env)->GetObjectClass(env, jobj);

//JmethodID

jfieldID mFid = (*env)->GetStaticMethodID(env, cls, "getUUID", "()Ljava/lang/String;");

//调用

//CallStaticMethod

jstring uuid = (*env)->CallStaticObjectMethod(env, jobj, mFid);

第三. 获取java的方法

3步走: 类,方法签名,Call方法

//Jclass

jclass cls = (*env)->GetObjectClass(env, jobj);

//JmethodID

jfieldID mFid = (*env)->GetMethodID(env, cls, "genRandomInt", "(I)I");

//调用

//CallMethod

jint random = (*env)->CallIntMethod(env, jobj, mFid, 200);

总结: 访问静态属性和非静态属性的区别: 调用的方法不一样! 

共同点: 流程都一样. 要获取类, 获取属性的Id

第二. 获取java的static属性

//访问静态属性

//jclass

jclass cls = (*env)->GetObjectClass(env, jobj);

//jfieldID

jfieldID fid =(*env)->GetStaticFieldID(env, cls, "count", "I");

//GetStaticField

jint count = (*env)->GetStaticIntField(env, cls, fid);

第一. 获取java的属性

1. 获取class

2. 得到属性字段的id ,通过属性的签名得到

3.得到java的属性

jclass cls = env->GetObjectClass(jobj);//obj是传入的参数,得到的是B类的实例

// 获取java的属性

char* name="";

jfieldID jfieldId = env->GetFieldID(cls, name, "Ljava/lang/String");

env->GetObjectField(jobj, jfieldId)

2.Java调用C函数

    在Java中声明Native方法(即需要调用的本地方法)

编译上述 Java源文件javac(得到 .class文件) 3。 通过 javah 命令导出JNI的头文件(.h文件)

使用 Java需要交互的本地代码 实现在 Java中声明的Native方法

编译.so库文件

通过Java命令执行 Java程序,最终实现Java调用本地代码

CMake:一个跨平台的编译构建工具,替代 Android.mk

LLDB:一个高效的 C/C++ 的调试工具

NDK:即我们需要下载的工具,会生成到 SDK 根目录下的 ndk-bundle 目录下

java调用C:

在JNI 中,java调用C的流程步骤:(个人的理解整理,如有误,请包容也请指正)

总结:

1.ndk,会先加载so库

2.ndk开发工具包提供调用java的方法直接调用。(我们可以通过静态注册和动态注册)

————————————————

静态注册和动态注册?

他们的好处如何?

如何实现动态注册

 如何加载NDK 库 ?如何在JNI 中注册Native 函数,有几种注册方法 ?

当执行一个 Java 的 native 方法时,虚拟机是怎么知道该调用 so 中的哪个方法呢?

这就需要用到注册的概念了,通过注册,将指定的 native 方法和 so 中对应的方法绑定起来(函数映射表),这样就能够找到相应的方法了。

参考回答:

public class JniTest{

//加载NDK 库

static{

System.loadLirary("jni-test");

}

注册JNI 函数的两种方法

o静态方法

. 缺点

必须遵循注册规则

名字过长

运行时去找效率不高

动态注册:

无需向静态注册一样,遵循特定的方法命名格式。

原因:

我们在调用 System.loadLibrary的时候,会在C/C++文件中回调一个名为 JNI_OnLoad ()的函数,在这个函数中一般是做一些初始化相关操作, 我们可以在这个方法里面注册函数, 注册整体流程如下:

通过 RegisterNatives 方法手动完成 native 方法和 so 中的方法的绑定,这样虚拟机就可以通过这个函数映射表直接找到相应的方法了。

 b. 通常我们在 JNI_OnLoad 方法中完成动态注册,native-lib.cpp 如下:

JNI_OnLoad入口:然后分别调用下面

//注册函数 registerNatives(把类写好) ->registerNativeMethods(添加方法,把什么的类传来) ->env->RegisterNatives

// jni头文件 #include#include#include#includeusingnamespacestd;//native 方法实现jintget_random_num(){returnrand();}/*需要注册的函数列表,放在JNINativeMethod 类型的数组中,

以后如果需要增加函数,只需在这里添加就行了

参数:

1.java中用native关键字声明的函数名

2.签名(传进来参数类型和返回值类型的说明)

3.C/C++中对应函数的函数名(地址)

*/staticJNINativeMethod getMethods[]={{"getRandomNum","()I",(void*)get_random_num},};//此函数通过调用RegisterNatives方法来注册我们的函数staticintregisterNativeMethods(JNIEnv*env,constchar*className,JNINativeMethod*getMethods,intmethodsNum){jclass clazz;//找到声明native方法的类clazz=env->FindClass(className);if(clazz==NULL){returnJNI_FALSE;}//注册函数 参数:java类 所要注册的函数数组 注册函数的个数if(env->RegisterNatives(clazz,getMethods,methodsNum)<0){returnJNI_FALSE;}returnJNI_TRUE;}staticintregisterNatives(JNIEnv*env){//指定类的路径,通过FindClass 方法来找到对应的类constchar*className="com/example/wenzhe/myjni/JniTest";returnregisterNativeMethods(env,className,getMethods,sizeof(getMethods)/sizeof(getMethods[0]));}//回调函数JNIEXPORT jint JNICALLJNI_OnLoad(JavaVM*vm,void*reserved){JNIEnv*env=NULL;//获取JNIEnvif(vm->GetEnv(reinterpret_cast(&env),JNI_VERSION_1_6)!=JNI_OK){return-1;}assert(env!=NULL);//注册函数 registerNatives ->registerNativeMethods ->env->RegisterNativesif(!registerNatives(env)){return-1;}//返回jni 的版本 returnJNI_VERSION_1_6;}

来源:https://www.jianshu.com/p/1d6ec5068d05

4. Java中类型和native中类型映射关系。

类型转换

JavaNativeSignature

bytejbyteB

charjcharC

doublejdoubleD

floatjfloatF

intjintI

shortjshortS

longjlongJ

booleanjbooleanZ

voidvoidV

对象jobjectL+classsname+;

ClassjclassLjava/lang/Class;

StringjstringLjava/lang/String;

ThrowablejthrowableLjava/lang/Throwable;

Object[]jobjectArray[L+classname+;

byte[]jbyteArray[B

char[]jcharArray[C

double[]jdoubleArray[D

float[]jfloatArray[F

int[]jintArray[I

short[]jshortArray[S

long[]jlongArray[J

booleanjbooleanArray[Z

总结: java调用c和C调用java

你可能感兴趣的:(3.【干货】火爆全网的《超全NDK精品教程》JNI c调用java和java调用c( Env 和函数签名))