美文网首页
NDK-012: jni: JNIEnv的实现原理

NDK-012: jni: JNIEnv的实现原理

作者: xqiiitan | 来源:发表于2025-01-02 15:44 被阅读0次

基于Eclipse +VisualStudio的 JNI开发,java调用C语言功能。
C语言打包生成.dll动态库,给Eclipse调用。

1.JNI的开发流程

  1. 获取JNI层的秘钥。
  2. 写好本地的native方法;
public class NdkSimple {
    public static void main(String[] args) {
        NdkSimple ndkSimple = new NdkSimple();
        String signaturePwd = ndkSimple.getSingnaturePassword();
        System.out.println("密码:" + signaturePwd); // 打印出JNI层设置的密码。密码:940223
    }
    // 定义本地的native方法
    public native String getSingnaturePassword();
}
  1. 使用命令,生成头文件.h; Eclipse项目
    cd 到src的目录,命令:javah能看到命令集合。
    javah -d ../jni -jni com.tom.myjni.mds.jni.NdkSimple
    javah -d ../jni -jni com.darren.ndk12.NdkSimple
  2. 生成.h文件
/* DO NOT EDIT THIS FILE - it is machine generated */
#include "jni.h"// "" 引入自己工程的头文件 <> 引入系统的头文件
/* Header for class com_darren_ndk12_NdkSimple */
// 用来打一个标记,c在编译的时候会把头文件 copy 到你引入的地方,不管是重复引用还是相互引用都只会 copy 一次
// 避免互相引用文件 死循环。 如果没定义就定义。只拷贝一次。
#ifndef _Included_com_darren_ndk12_NdkSimple
#define _Included_com_darren_ndk12_NdkSimple
#ifdef __cplusplus // 相当于 if 语句 c++ 
// 不管是 c 还是 c++ 统一都是采用 c 的编译方式,因为在c里面是不允许函数重载的,但是在 c++ 里面可以
// 在jni开发的时候,找到函数,要确保函数是唯一的。不允许函数的重载。
extern "C" {
#endif
/* 方法的声明:
 * Class:     com_darren_ndk12_NdkSimple
 * Method:    getSingnaturePassword
 * Signature: ()Ljava/lang/String;
 */
JNIEXPORT jstring JNICALL Java_com_darren_ndk12_NdkSimple_getSingnaturePassword
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif
  1. 将.h文件拷贝到VS项目头文件中; 将jni.h(jdk\include\win32), jni_md.h(jdk\include)
    也拷贝进来。先拷贝到工程目录,然后头文件--添加--现有项--选择jni.h文件。
    把include 的头文件引用(jni.h, jni_md.h),改成""引入。 依赖进来。
  2. 在Simple.c 中写native的实现类。实现头文件的native方法。
// 实现native方法。
// jni函数的实现,JNIEXPORT JNI的关键字,不能少,编译能通过,标记为该方法可以被外部调用。
// jstring: java中的String,返回值
// JNICALL: 也是关键字,可以省略。代表是jni调用的。
// JNIEnv: C 和Java 相互调用的桥梁。所有Function要高清。
// jobject: Java传下来的对象,就是本项目中的JniSimple.java 对象。
// jclass: java传递下来的 class对象,就是本项目中的 JniSimple.class对象。针对static native方法。
JNIEXPORT jstring JNICALL Java_com_darren_ndk12_NdkSimple_getSingnaturePassword
(JNIEnv * env, jobject jobj){
    // JNIEnv * 其实已经是一个二级指针了,所以 -> 调用的情况下必须是一级指针 *取值 
    return (*env)->NewStringUTF(env,"940223");
}
  1. 生成.dll动态库。
    项目属性页:配置属性--常规--常规属性--配置类型(动态库.dll)--确定。
    生成--配置管理器 -- 活动解决方案平台--x64--确定。
    生成--生成解决方案。
    进入项目目录--x64--Debug--,已生成NDK_Day12.dll 动态库文件。

7.进入java使用,引入加载我们的动态库。

public class NdkSimple {
    //...
    // 定义本地的native方法
    public native String getSingnaturePassword();
    static {
        // System.loadLibrary: Android加载apk中的libs目录下的.so库;
        // System.load: 加载一个具体路径上的.so库(服务器上下载的so库)。
        String filePath = "C:/Users/NDK/NDK_DAY12/x64/Debug/DK_Day12.dll";
        System.load(filePath);
    }
}

2.详解.h文件和实现文件

见上面代码中的详细注释。

3.JNIEnv的实现原理

JNIEXPORT jstring JNICALL Java_com_darren_ndk12_NdkSimple_getSingnaturePassword
(JNIEnv * env, jobject jobj){
    return (*env)->NewStringUTF(env,"940223"); // 二级指针
}

加一个* 代表把指针类型,变成 JNIEnv。
结构体不能用箭头,只有指针才能使用箭头。

typedef const struct JNINativeInterface_ *JNIEnv;

// JNIEnv 是什么:结构体 指针的别名,JNIEnv 其实是 JNINativeInterface_ 的指针别名。
其实就是一个 一级指针了。
JNIEnv * env; // 其实已经是一个二级指针了。箭头调用只能在一级指针下才能调用(->)
(env) 在二级指针上用,取到一级指针。只有就能调用箭头。

方法指针: jint (JNICALL *GetVersion)(JNIEnv * env);

java调用C语言,
生成JNIEnv对象,建立【桥梁】,才能把JNIEnv对象,传入到方法里面。
调用方法后,会还有个返回值,返回值也会先进入桥梁,再返回给调用者。

JNIEnv原理流程。Java调用C的桥梁。

模拟JNIEnv对象是怎么构建的,

#include <stdlib.h>
#include <stdio.h>
// 定义一个结构体指针的别名
typedef const struct JNINativeInterface *JNIEnv;
// 模拟一个结构体
struct JNINativeInterface{
    // 结构体的方法指针定义:方法指针的名字-NewStringUTF,里面并没有方法的实现(只有方法的指针)。
    char* (*NewStringUTF)(JNIEnv*, char*);
};
// 定义方法的实现:c_str -> c_str
char* NewStringUTF(JNIEnv* env, char* c_str){
    // c_str -> jstring
    return c_str;
}
char* Java_com_darren_getSingnaturePassword(JNIEnv * env){
    // JNIEnv * 其实已经是一个二级指针了,所以 -> 调用的情况下必须是一级指针 *取值 
    return (*env)->NewStringUTF(env, "940223");
}

void main() {
    // 构建 JNIEnv* 对象
    struct JNINativeInterface nativeInterface;
    // 给结构方法指针进行赋值 (指定方法的实现)
    nativeInterface.NewStringUTF = NewStringUTF;

    // 传给 Java_com_darren_ndk12_NdkSimple_getSingnaturePassword 的参数是 JNIEnv*
    JNIEnv env = &nativeInterface;// 一级指针
    JNIEnv* jniEnv = &env;// 二级指针(一级指针再取地址),建立好的桥梁(JNIEnv),二级指针传递给方法

    // 把 jniEnv 对象传给 Java_com_darren_ndk12_NdkSimple_getSingnaturePassword
    char* jstring = Java_com_darren_getSingnaturePassword(jniEnv);
    // jstring 通过 JNIEnv桥梁 可以再传给 java 层,这里直接打印出来。
    printf("jstring = %s", jstring);
    getchar();
}

4.C中访问和修改Java属性和方法

签名,授权加密。

package com.darren.ndk12;
import java.util.UUID;
public class NdkSimple1 {
    public String name = "Darren"; // name => Jack
    public static int age = 24;// 24 + 12
    
    public int add(int number1, int number2){ // 定义Java方法
        return number1 +number2;
    }
    // 小的思考:静态获取 uuid 的方法,然后再 c 调用这个方法获取uuid(Java来实现更简单)
    public static String getUUID(){
        return UUID.randomUUID().toString();
    }
    
    public static void main(String[] args) {
        NdkSimple1 ndSimple1 = new NdkSimple1();
        System.out.println("修改前:"+ndSimple1.name);
        ndSimple1.changeName(); // ndk方法修改name
        System.out.println("修改后:"+ndSimple1.name);

        System.out.println("修改前:"+NdkSimple1.age);
        changeAge(); // ndk方法修改age
        System.out.println("修改后:"+NdkSimple1.age);
        
        ndSimple1.callAddMathod(); // 调用java的方法
    }
    // 改变我们属性: 名称、年龄
    public native void changeName(); 
    public static native void changeAge(); // 静态方法
    // c 调用 java 对象方法
    public native void callAddMathod();
    
    static{
        // 引入加载我们的动态库 
        // System.loadLibrary :android 加载apk中的libs目录下 .so 库
        // System.load : 加载一个具体路径上的 .so 库,去服务器上下载再进行加载(data/data)
        System.load("C:/Users/hcDarren/Desktop/android/NDK/NDK_Day12/x64/Debug/NDK_Day12.dll");
    }
}

Eclipse+cmd命令:生成头文件.h(com_darren_ndk12_NdkSimple1.h)
拷贝.h文件到VisualStudio中,添加现有项 NdkSimple1.h
在Simple.c 中,写实现方法。

// 实现我们的 native 方法
#include "com_darren_ndk12_NdkSimple.h"
#include "com_darren_ndk12_NdkSimple1.h" // 添加新的.h文件

// JNIEXPORT JNI 一个关键字,不能少(编译能通过),标记为该方法可以被外部调用
// jstring : 代表 java 中的 String 
// JNICALL: 也是一个关键字,可以少的 jni call
// JNIEnv: 这个是 c 和 java 相互调用的桥梁,所有 function 搞清
// jobject: java传递下来的对象,就是本项目中 JniSimple java 对象
// jclass: java传递下来的 class 对象,就是本项目中的 JniSimple.class 
JNIEXPORT jstring JNICALL Java_com_darren_ndk12_NdkSimple_getSingnaturePassword
(JNIEnv * env, jobject jobj){
    // JNIEnv * 其实已经是一个二级指针了,所以 -> 调用的情况下必须是一级指针 *取值 
    return (*env)->NewStringUTF(env,"940223");
}

// C中访问和修改Java属性1:修改java的属性
JNIEXPORT void JNICALL Java_com_darren_ndk12_NdkSimple1_changeName
(JNIEnv *env, jobject jobj) {
    // 获取 name 属性然后修改为 Jack

    // 3.获取 jclass 
    jclass j_clz = (*env)->GetObjectClass(env, jobj);
    // 获取 jfieldId (JNIEnv *env, jclass clazz, const char *name, const char *sig)
    // name 获取哪个属性的属性名 
    // 2.sig 属性的签名
    jfieldID j_fid = (*env)->GetFieldID(env, j_clz, "name", "Ljava/lang/String;");
    // 1.获取 name 属性的值
    jstring j_str = (*env)->GetObjectField(env, jobj, j_fid);

    // 打印字符串 jstring -> c_str
    char* c_str = (*env)->GetStringUTFChars(env,j_str,NULL);
    printf("name is %s", c_str);

    // 修改成 jack
    jstring jackName = (*env)->NewStringUTF(env,"Jack");
    (*env)->SetObjectField(env, jobj, j_fid, jackName);
}
// C中访问和修改Java属性2,静态方法第二个参数是jclass
JNIEXPORT void JNICALL Java_com_darren_ndk12_NdkSimple1_changeAge
(JNIEnv * env, jclass jcls){
    // 1.首先获取原来的值
    jfieldID j_fid = (*env)->GetStaticFieldID(env,jcls,"age","I");
    // Static 获取静态的
    jint age = (*env)->GetStaticIntField(env, jcls, j_fid);

    // jint -> int。不需要转换
    age += 12;
    // 2.设置新的 age 给参数
    (*env)->SetStaticIntField(env,jcls,j_fid,age);
}

// C中访问Java的对象方法, 对象调用
JNIEXPORT void JNICALL Java_com_darren_ndk12_NdkSimple1_callAddMathod
(JNIEnv *env, jobject jobj){
    
    jclass j_clz = (*env)->GetObjectClass(env,jobj);
    // 获取 methodid,方法的名字 add, 方法签名 (II)I
    jmethodID j_mid = (*env)->GetMethodID(env, j_clz, "add", "(II)I");
    // 去调用 java 的方法,拿到返回值,传入加数 和 被加数
    jint sum = (*env)->CallIntMethod(env, jobj, j_mid,2,3);
    printf("sum = %d",sum); // 拿到结果,并打印:5
}

通过cmd 命令,拿到方法的签名。
......NDK_Day12\src> cd ..
......NDK_Day12> cd bin
......NDK_Day12\bin> javap -p -s com.darren.ndk12.NdkSimple1
进而得到所有方法和属性的签名。

Compiled from "NdkSimple1.java"
public class com.darren.ndk12.NdkSimple1 {
  public java.lang.String name;
    descriptor: Ljava/lang/String;  //属性name的签名
  public static int age;
    descriptor: I        //属性age的签名
  static {};
    descriptor: ()V

  public com.darren.ndk12.NdkSimple1();
    descriptor: ()V
  public int add(int, int);
    descriptor: (II)I   //方法的签名
  public static java.lang.String getUUID();
    descriptor: ()Ljava/lang/String;

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
  public native void changeName();
    descriptor: ()V
  public static native void changeAge();
    descriptor: ()V
  public native void callAddMathod();
    descriptor: ()V
}

4.C中访问 java的static方法。

在JNI中调用Java的静态方法,你需要使用JNIEnv提供的FindClass、GetStaticMethodID和CallStaticVoidMethod
(或其他对应的CallStaticXXXMethod)函数。
以下是一个简单的例子,假设我们有一个Java类TestClass,它有一个静态方法staticMethod,我们将在C中调用它。
这个例子中,Java_com_example_yourapp_YourActivity_callStaticMethod是JNI层的函数,
它会被Java代码(比如一个Activity)调用,从而触发对Java静态方法的调用。
记得在实际的应用中,你需要使用正确的包名和类名来替换com/example/yourapp和TestClass。

public class TestClass {
    public static void staticMethod(String message) {
        System.out.println("Static method called with: " + message);
    }
}

#include <jni.h>
#include <stdio.h>
JNIEXPORT void JNICALL
Java_com_example_yourapp_YourActivity_callStaticMethod(JNIEnv *env, jobject obj) {
    jclass clazz = (*env)->FindClass(env, "com/example/yourapp/TestClass");
    if (clazz == NULL) {
        return; // class not found
    }
    jmethodID methodID = (*env)->GetStaticMethodID(env, clazz, "staticMethod", "(Ljava/lang/String;)V");
    if (methodID == NULL) {
        return; // method not found
    }
 
    jstring message = (*env)->NewStringUTF(env, "Hello from JNI!");
    (*env)->CallStaticVoidMethod(env, clazz, methodID, message);
    (*env)->DeleteLocalRef(env, clazz);
    (*env)->DeleteLocalRef(env, message);
}

不怕没有工作,不怕工资低。多花时间在技术上,多思考。


相关文章

网友评论

      本文标题:NDK-012: jni: JNIEnv的实现原理

      本文链接:https://www.haomeiwen.com/subject/jkmtyjtx.html