美文网首页
Android JNI学习手册

Android JNI学习手册

作者: cvmars | 来源:发表于2021-01-19 10:44 被阅读0次

    一、JNI基础学习-JNI调用java原生方法

    
    class MainActivity : AppCompatActivity() {
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
            sample_text.setOnClickListener {
    
                callMethod("lilei", 18)
            }
        }
    
        external fun callMethod(name: String, age: Int)
    
        companion object {
    
            // Used to load the 'native-lib' library on application startup.
            init {
                System.loadLibrary("native-lib")
            }
        }
    }
    
    
    package com.microtechmd.jnidemo;
    
    public class Student {
    
        private String name;
        private int age;
    
        public Student() {
        }
    
        public Student(String name, int age) {
            this.name = name;
            this.age = age;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public int getAge() {
            return age;
        }
    
        public void setAge(int age) {
            this.age = age;
        }
    
        @Override
        public String toString() {
            return "Student{name='" + name + '\'' +", age=" + age +'}';
        }
    }
    
    
    package com.microtechmd.jnidemo;
    
    public class Person {
    
        private void setStudent(Student student){
            Log.d("dsh", "setStudent: "+student.toString());
        }
    
        public static String logcat(){
    
            Log.d("dsh", "log : ");
        }
    }
    
    
    
    extern "C" JNIEXPORT void JNICALL
    Java_com_microtechmd_jnidemo_MainActivity_callMethod(
            JNIEnv *env,
            jobject jo /* this */, jstring name, jint age) {
    
        //创建Student对象
        const char *student_class_str = "com/microtechmd/jnidemo/Student";
    
        //获取student class
        jclass student_class = env->FindClass(student_class_str); 
        //根据class获取student对象
        jobject student_obj = env->AllocObject(student_class);
        //获取setName 方法iD
        jmethodID setName_ID = env->GetMethodID(student_class, "setName", "(Ljava/lang/String;)V");
        //执行setName方法
        env->CallVoidMethod(student_obj, setName_ID, name);
    
        jmethodID setAge_ID = env->GetMethodID(student_class, "setAge", "(I)V");
        env->CallVoidMethod(student_obj, setAge_ID, age);
    
        const char *person_class_str = "com/microtechmd/jnidemo/Person";
        jclass person_class = env->FindClass(person_class_str);
        jobject person_object = env->AllocObject(person_class);
        jmethodID setStudent_ID = env->GetMethodID(person_class, "setStudent",
                                                   ("(Lcom/microtechmd/jnidemo/Student;)V"));
    
          /执行普通方法 需要对象和方法id、参数。总结类比java静态方法和普通方法的调用
        env->CallVoidMethod(person_object, setStudent_ID, student_obj);
    
       //获取静态方法 ,不需要person对象
        jmethodID log_ID = env->GetStaticMethodID(person_class, "logcat",
                                                   ("()V"));
        //执行静态方法                                            
        jstring string_obj = static_cast<jstring> ( env->CallStaticVoidMethod(person_class,log_ID));
        const char *stringChar = env->GetStringUTFChars(string_obj,0);
        env->ReleaseStringUTFChars(string_obj,stringChar); //回收对象
    
        //JIN调用接口,有点类比Java发射
    }
    
    

    JNI调用java原生方法有四个重要的东西

    一、class 类信息

    二、method 方法信息

    三、sign 方法签名 ,里面包括了方法的参数类型信息 和返回信息,如(Ljava/lang/String;)V 代表的就是 void xxx(String)方法;其中构造方法用 ,多个参数的方法这样表示 (Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String; 代表 String xxxx(String , String )

    四、实例对象

    二、JNI基础学习-String的处理

    java传给c一个string,javah生成了方法名后,

    发现传递来的是一个jstring(因为在c里,是没有string的),

    public class Jni {
        static {
            System.loadLibrary("native-lib");
        }
        public native String study_string(String str);
    }
    
    //生成的头文件
    JNIEXPORT jstring JNICALL Java_jni_study_com_cvmars_Jni_study_1string
      (JNIEnv *, jobject, jstring);
    
    

    传递来的是一个jstring(因为在c里,是没有string的), jstring其实是void*(任意类型)

    我们需要调用一个方法,把jstring转为C语言的char*类型,先看下这个工具方法:

    #include <stdlib.h>
    
    /**
     * 把一个jstring转换成一个c语言的char* 类型.
     */
    char* _JString2CStr(JNIEnv* env, jstring jstr) {
        char* rtn = NULL;
        jclass clsstring = (*env)->FindClass(env, "java/lang/String");
        jstring strencode = (*env)->NewStringUTF(env,"GB2312");
        jmethodID mid = (*env)->GetMethodID(env, clsstring, "getBytes", "(Ljava/lang/String;)[B");
        jbyteArray barr = (jbyteArray)(*env)->CallObjectMethod(env, jstr, mid, strencode); // String .getByte("GB2312");
        jsize alen = (*env)->GetArrayLength(env, barr);
        jbyte* ba = (*env)->GetByteArrayElements(env, barr, JNI_FALSE);
        if(alen > 0) {
            rtn = (char*)malloc(alen+1); //"\0"
            memcpy(rtn, ba, alen);
            rtn[alen]=0;
        }
        (*env)->ReleaseByteArrayElements(env, barr, ba,0);
        return rtn;
    }
    
    

    实现native方法

    JNIEXPORT jstring JNICALL Java_jni_study_com_jnibsetpractice_Jni_transe_1string
            (JNIEnv *env, jobject instance, jstring jstr) {
    
        //把一个jstring转换成一个c语言的char* 类型
        char *cStr = _JString2CStr(env, jstr);
        //c语言拼接字符串
        char *cNewStr = strcat(cStr, "简单加密一下哈哈哈!!!");
        // 把c语言里的char* 字符串转成java认识的字符串
        return (*env)->NewStringUTF(env, cNewStr);
    }
    
    

    三、JNI基础学习-在C里输出log的办法

    1. 在C 里输入
    image
    1. 在Android.mk里输入
    image
    1. 使用log
    image
    LOGD("length = %d", length);
    
    

    四、JNI基础学习- 数据类型和签名机制

    由于Java语言与C/C++语言数据类型的不匹配,需要单独定义一系列的数据类型转换关系来完成两者之间的对等(或者说是映射)。下面给出jni与Java数据类型对应表(jni类型均被定义在jni.h头文件中),如下表1和表2,在jni函数中,需要使用以下jni类型来等价与Java语言对应的类型。

    1.基本类型对照表

    <style> td {white-space:pre-wrap;border:1px solid #dee0e3;}</style> <byte-sheet-html-origin data-id="Oe3MXsb6ys-1611024151055" data-version="1" data-is-embed="true"><colgroup><col width="249"><col width="258"><col width="270"></colgroup>
    | Java类型 | JNI类型 | 描述 |
    | boolean | Jboolean | 无符号8位 |
    | byte | Jbyte | 无符号8位 |
    | char | Jchar | 无符号16位 |
    | short | Jshort | 有符号16位 |
    | int | Jint | 有符号32位 |
    | long | Jlong | 有符号64位 |
    | float | Jfloat | 有符号32位 |
    | double | Jdouble | 有符号64位 |</byte-sheet-html-origin>

    2.引用类型对照表

    <style> td {white-space:pre-wrap;border:1px solid #dee0e3;}</style> <byte-sheet-html-origin data-id="qhTSRvgq9c-1611024151061" data-version="1" data-is-embed="true"><colgroup><col width="341"><col width="440"></colgroup>
    | Java引用类型 | JNI类型 |
    | boolean[] | jbooleanArray |
    | byte[] | jbyteArray |
    | char[] | jcharArray |
    | short[] | jshortArray |
    | int[] | jintArray |
    | long[] | jlongArray |
    | float[] | jfloatArray |
    | double[] | jdoubleArray |
    | All objects | jobject |
    | java.lang.Class | jclass |
    | java.lang.String | jstring |
    | Object[] | jobjectArray |
    | java.lang.Throwable | jthrowable |</byte-sheet-html-origin>

    1 深入理解JNIEnv

    上面列出了JNI自定义类型,而为了操作这些类型,尤其是引用类型,就需要JNIEnv来协助完成。那么,什么是JNIEnv呢?实际上,JNIEnv的实体是一个名为JNINativeInterface的结构体,而这个结构体又是什么呢?JNINativeInterface结构体定义在头文件jni.h中,是一个复杂的函数指针集合,每一个函数指针又会指向一个本地实现函数,来完成特定的功能。诸如常见的New StringUTF,FindClass都定义在其中,如下列出了部分内容:

    /*  jni.h */#if defined(__cplusplus)typedef _JNIEnv JNIEnv;     // C++typedef _JavaVM JavaVM;#elsetypedef const struct JNINativeInterface* JNIEnv;     // Ctypedef const struct JNIInvokeInterface* JavaVM;#endifstruct JNINativeInterface {
        …
        jclass      (*FindClass)(JNIEnv*, const char*);
        …
        jstring     (*NewString)(JNIEnv*, const jchar*, jsize);
       …
       void        (*SetCharArrayRegion)(JNIEnv*, jcharArray,
                            jsize, jsize, const jchar*);
      …
        jint    (*RegisterNatives)(JNIEnv*, jclass, const JNINativeMethod*,
                            jint);
      …
        jint        (*GetJavaVM)(JNIEnv*, JavaVM**);
      …./* added in JNI 1.6 */// … 表示省略了部分内容
     };
    
    

    下图来帮助理解这个复杂的指向关系:

    image

    有了JNIEnv*指针,就可以使用函数指针调用特定的实现函数,来完成特定需求的功能。需要注意的是,env变量是线程线程相关的,不可从一个线程传递env变量到另外一个线程。

    那么又是如何使线程获得这个JNIEnv结构体指针的呢?这里涉及到一个重要的函数JNI_OnLoad(JavaVM* vm,void* reserved),当通过System. loadLibrary()方法来加载我们指定的动态库(如.so库)时,Java虚拟机会检测库中是否实现了JNI_OnLoad函数,如果实现了则这个函数就会被调用,并且一个代表JVM的对象vm被作为参数传递进来,这个对象一个进程只有一份,可以通过它的AttachCurrentThread方法来获得JNIEnv*对象,当我们的线程完成特定任务退出之前,应该调用vm的DetachCurrentThread来释放资源。

    上述方法均被定义在jni.h,如下:

    /* jni.h */#if defined(__cplusplus)
    typedef _JNIEnv JNIEnv;
    typedef _JavaVM JavaVM;#else
    typedef const struct JNINativeInterface* JNIEnv;
    typedef const struct JNIInvokeInterface* JavaVM;#endif/*
     * JNI invocation interface.
     */struct JNIInvokeInterface {  // C// ....
        jint        (*DestroyJavaVM)(JavaVM*);
        jint        (*AttachCurrentThread)(JavaVM*, JNIEnv**, void*);
        jint        (*DetachCurrentThread)(JavaVM*);
        jint        (*GetEnv)(JavaVM*, void**, jint);
        jint        (*AttachCurrentThreadAsDaemon)(JavaVM*, JNIEnv**, void*);
    };
    /*
     * C++ version.
     */struct _JavaVM {    // C++const struct JNIInvokeInterface* functions;
    #if defined(__cplusplus)
        jint DestroyJavaVM()
        { return functions->DestroyJavaVM(this); }
        jint AttachCurrentThread(JNIEnv** p_env, void* thr_args)
        { return functions->AttachCurrentThread(this, p_env, thr_args); }
        jint DetachCurrentThread()
        { return functions->DetachCurrentThread(this); }
        jint GetEnv(void** env, jint version)
        { return functions->GetEnv(this, env, version); }
        jint AttachCurrentThreadAsDaemon(JNIEnv** p_env, void* thr_args)
        { return functions->AttachCurrentThreadAsDaemon(this, p_env, thr_args); }#endif /*__cplusplus*/
    };
    /*
     * Prototypes for functions exported by loadable shared libs.  These are
     * called by JNI, not provided by JNI.
     */
    JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void* reserved);
    JNIEXPORT void JNICALL JNI_OnUnload(JavaVM* vm, void* reserved);// ....
    
    

    在JNI_OnLoad()函数中,也可以通过vm->GetEnv((void*)&env来获得JNIEnv指针。JNI_OnLoad()函数基本功能是确定并返回Java虚拟机支持的JNI版本,我们还可以用作其他用途(诸如做一些初始化工作),一个重要的用途是实现JNI函数的动态注册。

    与JNI_OnLoad()函数正好相反,当共享库被卸载时,会调用JNI_OnUnload()函数,我们可以做一些收尾的工作。

    2 JNI函数的注册过程

    在前面讲解了JNI函数,并没有深入探究Java层函数与jni函数的对应关系的建立,那么这种关联是怎样建立的,或者说当发起java native方法的调用时,是如何找到与之对应的jni函数的呢?这个过程可以分别用静态注册和动态注册的方式来完成。其实前面已经讲过了静态注册的原理。没错!就是命名规范,按照前面说的来命名jni函数,就可以实现,这里就不再赘述了。接下来,介绍JNI函数的动态注册过程。

    何为动态注册呢?说的直白点就是手动的参与它的注册过程,让JNI函数在一加载完.so动态库后就完成它的注册过程(使之与对应java native函数关联起来),而不是等到调用时再来进行注册,以提高调用效率,并且我们也不用遵守前面的命名规范了,可以给jni函数取自己认为合适的名字。

    要完成这个动态注册过程,就需要使用在上面提到过的JNI_OnLoad函数,它是在.so动态库加载后就会被调用的,而这又早于JNI函数的调用时机,因此在这个函数里实现注册过程是很合理的。

    要完成动态注册,方法一可以选择使用AndroidRuntime类的registerNativeMethods方法来完成注册,这个方法原型如下:

    /*
     * Register native methods using JNI.
     */
    static int AndroidRuntime::registerNativeMethods(JNIEnv* env,
        const char* className, const JNINativeMethod* gMethods, int numMethods)
    {
        return jniRegisterNativeMethods(env, className, gMethods, numMethods);
    }
    
    

    使用这个函数需要提供包含进行注册的jni函数的类的全路径(如项目中的OkayOps 类,全路径为com/yu/ops/OkayOps),要进行注册的方法信息结构体数组(JNINativeMethod)及方法个数。JNINativeMethod是一个C结构体,用于存储Java native方法与JNI函数的一一对应关系,包含的信息有native方法名、函数签名、函数指针。它的定义如下所示:

    typedef struct {
    const char* name; // Java层声明的native函数的名字,不需要带路径  。
    const char* signature; // Java层声明的native函数签名信息,用字符串表示
    void*  fnPtr;   //JNI 层对应函数的函数指针,它的类型void*
    } JNINativeMethod;
    
    

    上面涉及一个新概念函数签名,现在只需知道它是用来标识匹配哪个java的native方法即可,为了分析注册过程的条理清晰,将在下一节详细介绍。在registerNativeMethods方法的最后又调用了jniRegisterNativeMethods方法来完成注册,这个函数是在JNIHelp.h中声明(Android提供的帮助类来方便使用jni,路径android/libnativehelper/include/nativehelper/JNIHelp.h,实现在JNIHelp.cpp),可以先来看看这个方法:

    /* JNIHelp.cpp */
    extern "C" int jniRegisterNativeMethods(C_JNIEnv* env, const char* className,
        const JNINativeMethod* gMethods, int numMethods)
    {
        JNIEnv* e = reinterpret_cast<JNIEnv*>(env);
    
        ALOGV("Registering %s's %d native methods...", className, numMethods);
        // 获取指定类名的Class对象,并存储在局部引用中
        scoped_local_ref<jclass> c(env, findClass(env, className));
        if (c.get() == NULL) {  // 获取class对象为NULL
            char* tmp;
            const char* msg;
            if (asprintf(&tmp,
                         "Native registration unable to find class '%s'; aborting...",
                         className) == -1) {
                // Allocation failed, print default warning.
                msg = "Native registration unable to find class; aborting...";
            } else {
                msg = tmp;
            }
            e->FatalError(msg);
        }
         // 调用JNIEnv的RegisterNatives来完成注册
        if ((*env)->RegisterNatives(e, c.get(), gMethods, numMethods) < 0) {
            char* tmp;
            const char* msg;
            if (asprintf(&tmp, "RegisterNatives failed for '%s'; aborting...", className) == -1) {
                // Allocation failed, print default warning.
                msg = "RegisterNatives failed; aborting...";
            } else {
                msg = tmp;
            }
            e->FatalError(msg);
        }
    
        return 0;
    }
    
    

    可以发现,jniRegisterNativeMethods函数并不是具体实现,最终它会调用JNIEnv的RegisterNatives函数来完成JNI函数的注册。到此注册过程分析完成,终究是回到JNIEnv上。下面看看RegisterNatives函数原型:

    jint (*RegisterNatives) (JNIEnv* env, jclass clazz, const JNINativeMethod* gMethods , jint numMethods);
    
    

    可以发现,它和AndroidRuntime::registerNativeMethods函数的参数较为类似,除了第二个参数不同以外,其他均相同。而第二个参数正是要进行动态注册的类的Class运行时类,可以使用JNIEnv的FindClass函数来获取。

    第二种进行动态注册的方式就是基于上面的分析,即:第一步,使用JNIEnv的FindClass函数来拿到需要进行动态注册的类的运行时Class类;第二步,直接使用JNIEnv的RegisterNatives函数来完成JNI函数的注册。

    到此,我们分析了两种方案来完成JNI函数动态注册的目标。第一种,分析了使用AndroidRuntime::registerNativeMethods函数来完成动态注册的流程,使用该函数总体上来说使用方便,但流程较为复杂,第二种,使用JNIEnv的RegisterNatives函数完成动态注册,这种方法流程简单,但需要自个获取运行时Class类,稍显得烦琐点。

    本文实现注册的代码如下:

    // 需要注册的方法信息表
    static JNINativeMethod method_table[] = {
            {"NativeReadOkayData", "([B)I", (void*)Java_android_com_read_yu_data},
        {"NativeWriteOkayData", "([BI)I", (void*) Java_android_com_write_yu_data}, 
    };
    
    // 包含本地方法的类的全路径
    static const char* classPathName="com/yu/ops/OkayOps";
    
    // 使用AndroidRuntime的registerNativeMethods方法来完成注册
    static int register_com_yu_signature_ops(JNIEnv *env)
    {
        LOGI("register_com_yu_ops_OkayOps");
    
        return AndroidRuntime::registerNativeMethods(env,classPathName,method_table,NELEM(method_table));
    }
    // 加载动态库的时候被回调
    jint JNI_OnLoad(JavaVM* vm,void* reserved)
    {
     LOGI("JNI_OnLoad");
     JNIEnv* env = NULL;
     jint result = -1;
     if(vm->GetEnv((void**)&env,JNI_VERSION_1_6) != JNI_OK)  
    {
        goto bail;
     }
    
     LOGI("register method");
    
     if(register_com_yu_signature_ops(env) < 0)  // 注册
     {
        goto bail;
     }
     init();  // 做一些初始化工作
     return JNI_VERSION_1_6;
    bail:
     return result;
    }
    
    

    3 签名机制

    在上面动态注册小节提到一个函数签名(signature)的概念,这是用来干什么的呢?了解java语言的都知道它有一种方法重载机制,因此,为了能够调用正确的java层native方法,光凭方法名称是不够的,还需要知道它的具体参数与返回值。函数签名就是函数的参数与返回值的结合体,用来进行精准匹配。

    函数签名由字符串组成,第一部分是包含在圆括号()里的,用来说明参数类型,第二部分则跟的是返回值类型。比如”([Ljava/lang/Object;)Z”就是参数为Object[],返回值是boolean的函数的签名。下表列出类型与签名标识的对应关系:

    | Java类型 | 类型标识 |
    | boolean | Z |
    | byte | B |
    | char | C |
    | short | S |
    | int | I |
    | long | J |
    | float | F |
    | double | D |
    | String | L/java/lang/String; |
    | int[] | [I |
    | Object[] | [L/java/lang/Object; |
    
    

    int[]的标识是[I,其他基本数据类型的标识基本类似,用[+类型标识组合。需要注意的是,除了基本数据类型的数组以外,引用类型的标识后都需要跟上一个分号。一般,人为的写签名字符串难免会出错,而且类型签名标识又难以记忆,所幸的是java提供了相关命令来快速生成签名信息。到要生成签名的项目的bin目录下,使用javap命令加 –s选项来快速生成签名信息,如下:

    D:\code\yu_jar\bin>javap -s com.yu.ops.OkayOps
    Compiled from "OkayOps.java"
    public class com.yu.ops.OkayOps {
      public java.lang.String SERVICE;
        descriptor: Ljava/lang/String;
      static {};
        descriptor: ()V
      public com.yu.ops.OkayOps();
        descriptor: ()V
      public final int yu_read(byte[]);
        descriptor: ([B)I
      public final void yu_write(byte[]);
        descriptor: ([B)V
      public native int NativeReadOkayData(byte[]);
        descriptor: ([B)I
      public native int NativeWriteOkayData(byte[], int);
        descriptor: ([BI)I
    }
    
    

    在方法下面的descriptor的内容即是所需要的签名信息。签名信息比较有用,在JNI函数的调用中,经常会需要以签名作为参数。

    在jni.h头文件我们可以看到基本类型方法签名定义,如下:

    typedef union jvalue {
        jboolean    z;
        jbyte       b;
        jchar       c;
        jshort      s;
        jint        i;
        jlong       j;
        jfloat      f;
        jdouble     d;
        jobject     l;
    } jvalue;
    

    相关文章

      网友评论

          本文标题:Android JNI学习手册

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