Programación JNI: conceptos básicos de JNI

Qué es JNI y cómo usarlo

Interfaz nativa JNI-Java, es una característica de la plataforma Java (no es exclusiva del sistema Android). De hecho, define principalmente algunas funciones JNI para que los desarrolladores puedan usar estas funciones para llamar código C / C ++ desde código Java. El código C / C ++ también puede llamar código Java, de modo que se puedan utilizar las características de cada lenguaje. Entonces, cómo usar JNI, en general, primero compilamos el código C / C ++ escrito en una biblioteca dinámica correspondiente a la plataforma (Windows es generalmente un archivo dll, Linux es generalmente un archivo so, etc.), aquí estamos apuntando a Plataforma Android, así que solo discuta la biblioteca so. Dado que la programación JNI es compatible con la programación C y C ++, todas nuestras castañas aquí usan C ++. Puede haber algunas diferencias en la versión C, pero el contenido principal sigue siendo el mismo. Puedes aprender por analogía.

contenido principal:

1. ¿Cómo se vincula el método nativo de Java con las funciones en C / C ++?

2. JNI define el tipo de datos correspondiente a Java para la programación JNI.

3. Descriptor: se utiliza para describir el nombre de la clase o el tipo de datos. En la capa C / C ++, necesitamos usar cadenas para describir el nombre de la clase, el tipo de variable y el tipo de objeto que se debe obtener para obtener el objetos y variables de la capa Java y el método de descripción del método Java.
Este artículo ofrece principalmente una breve introducción de los tres aspectos anteriores.Cuando el próximo artículo presente la práctica de NDK, volveré y comprenderé mejor.

Hablando desde una castaña

Comencemos con el código directamente, que es más vívido e intuitivo y fácil de entender. El código Java que se usa hoy es el siguiente:

public class AndroidJni {

    static{
        System.loadLibrary("main");
    }

    public native void dynamicLog();

    public native void staticLog();

}

Aquí definimos dos métodos declarados como nativos, y declaramos un área estática, en el área estática la clase carga la biblioteca llamada libmain.so, aquí decimos que es la biblioteca libmain.so, pero solo escribimos al cargar "Main", de hecho , todo el mundo solo necesita saber que se trata de un acuerdo.

El código C ++ es el siguiente:

#include <jni.h>

#define LOG_TAG "main.cpp"

#include "mylog.h"

static void nativeDynamicLog(JNIEnv *evn, jobject obj){

    LOGE("hell main");
}

JNIEXPORT void JNICALL Java_com_github_songnick_jni_AndroidJni_staticLog (JNIEnv *env, jobject obj)
{
    LOGE("static register log ");
}

JNINativeMethod nativeMethod[] = {
   
   {"dynamicLog", "()V", (void*)nativeDynamicLog},};

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *jvm, void *reserved) {

    JNIEnv *env;
    if (jvm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {

        return -1;
    }
    LOGE("JNI_OnLoad comming");
    jclass clz = env->FindClass("com/github/songnick/jni/AndroidJni");

    env->RegisterNatives(clz, nativeMethod, sizeof(nativeMethod)/sizeof(nativeMethod[0]));

    return JNI_VERSION_1_4;
}

Aquí se citan dos archivos de encabezado, jni.h y mylog.h, donde jni.h define una gran cantidad de funciones y estructuras JNI que usamos, y mylog.h es una función que definí para imprimir el registro de Android (función y Java Igual que Clase de registro).
Cómo compilar en la biblioteca so y algunas especificaciones de la biblioteca so no se discuten aquí por el momento, y se presentarán en el próximo artículo. Se asume que el programa C ++ anterior está compilado en un archivo llamado libmain.so. En la capa de Java, use el método System.loadLibarary ("main") para cargar la biblioteca so, de modo que dynamicLog (), staticLog () y el correspondiente Java_com_github_songnick_jni_AndroidJni_staticLog (), nativeDynamicLog () dos métodos nativos estén vinculados. Por supuesto, esta parte del trabajo es todo Lo hace la máquina virtual Java, entonces, ¿cómo se hace? A continuación, lo analizaremos en base al código anterior.

Registre estáticamente métodos nativos

En el código anterior, verá las palabras clave JNIEXPORT y JNICALL. Estas dos palabras clave son dos definiciones de macro. Su función principal es mostrar que la función es una función JNI. Cuando se carga la máquina virtual Java, se vinculará a la correspondiente En la clase AndroidJni.java, staticLog () se declara como un método nativo, y su función JNI correspondiente es Java_com_github_songnick_jni_AndroidJni_staticLog (), entonces, ¿cómo se vincula? Al cargar la biblioteca so en la máquina virtual Java, si encuentra que contiene las dos definiciones de macro anteriores Cuando la función está vinculada al método nativo de la capa Java correspondiente, ¿cómo sabe a qué método nativo de qué clase en Java corresponde? Observamos cuidadosamente que la composición del nombre de la función JNI es en realidad: Java_PkgName_ClassName_NativeMethodName , con el prefijo Java y con "_" El guión bajo conecta el nombre del paquete, el nombre de la clase y el nombre del método nativo para formar la función JNI correspondiente.

En circunstancias normales, podemos escribir manualmente de acuerdo con esta regla, pero si hay demasiados métodos nativos, todavía hay una cierta cantidad de trabajo y, en el proceso de escritura, puede haber errores. De hecho, Java nos proporciona con javah La herramienta ayuda a generar el archivo de encabezado correspondiente. En el archivo de encabezado generado, la función JNI correspondiente se genera de acuerdo con las reglas mencionadas anteriormente, y podemos copiarla directamente durante el desarrollo. Tome el código anterior como ejemplo. Después de compilar en AndroidStudio, ingrese el directorio del proyecto app / build / intermediates / classes / debug y ejecute el siguiente comando:

javah -d jni com.github.songnick.jni.AndroidJni

Aquí -d especifica el directorio donde se almacena el archivo .h generado (de lo contrario, se creará automáticamente), y com.github.songnick.jni.AndroidJni representa el archivo de clase en el directorio especificado. Aquí hay una breve introducción de que la función JNI generada contiene dos variables de parámetros fijas, JNIEnv y jobject. JNIEnv se presentará más adelante. Jobject es el objeto de clase al que pertenece el método nativo actualmente vinculado (similar a esto en Java). Ambas variables son generadas por la máquina virtual Java y se pasan durante la llamada.

Registro dinámico

Anteriormente introdujimos el proceso de registrar estáticamente el método nativo, es decir, el método nativo declarado por la capa Java corresponde a la función JNI uno a uno, por lo que hay una forma de vincular el método nativo de la capa Java con cualquier función JNI ? Por supuesto que es posible, esto requiere el uso de métodos de registro dinámicos. A continuación, echemos un vistazo a cómo implementar el registro dinámico.

Función JNI_OnLoad

Cuando usamos el método System.loadLibarary () para cargar la biblioteca so, la máquina virtual Java encontrará esta función y la llamará, por lo que podemos hacer algunas acciones de inicialización en esta función. De hecho, esta función es equivalente a onCreate en Método de actividad (). Hay tres palabras clave delante de esta función, a saber, JNIEXPORT, JNICALL y jint, entre las cuales JNIEXPORT y JNICALL son dos definiciones de macro que se utilizan para especificar que la función es una función JNI. Jint es un tipo de datos definido por JNI, porque los tipos de datos u objetos de la capa Java y C / C ++ no pueden hacer referencia directa o usarse entre sí. La capa JNI define sus propios tipos de datos para conectar la capa Java y la capa JNI. Como para estos tipos de datos, lo presentaremos más adelante. El jint aquí corresponde al tipo de datos int de Java. El int devuelto por esta función indica la versión de JNI actualmente en uso, que es similar a la versión API del sistema Android. Hay algunas funciones JNI diferentes definidas en diferentes versiones de JNI . Esta función tendrá dos parámetros, entre los cuales * jvm es una instancia de máquina virtual Java, y la estructura JavaVM define las siguientes funciones:

DestroyJavaVM
AttachCurrentThread
DetachCurrentThread
GetEnv

Aquí usamos la función GetEnv para obtener la variable JNIEnv, la función JNI_OnLoad anterior tiene el siguiente código:

JNIEnv *env;
if (jvm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {

    return -1;
}

La función GetEnv se llama aquí para obtener el puntero de estructura JNIEnv. De hecho, la estructura JNIEnv apunta a una tabla de funciones, que apunta a la función JNI correspondiente. Implementamos la programación JNI llamando a estas funciones JNI, y lo haremos más adelante Introducción .

Obtenga objetos Java y complete el registro dinámico

Lo anterior describe cómo obtener el puntero de estructura JNIEnv Después de obtener este puntero de estructura, podemos llamar a la función RegisterNatives en JNIEnv para completar el registro dinámico del método nativo. El método es como sigue:

jint RegisterNatives(jclass clazz, const JNINativeMethod* methods, jint nMethods)

El primer parámetro es la capa de Java correspondiente al objeto que contiene el método nativo (aquí está el objeto AndroidJni), el objeto de clase se obtiene llamando a la función correspondiente a JNIEnv (el parámetro de la función FindClass es el descriptor de clase que necesita obtener el objeto de clase):

jclass clz = env->FindClass("com/github/songnick/jni/AndroidJni");

El segundo parámetro es un puntero a la estructura JNINativeMethod. La estructura JNINativeMethod aquí describe el método nativo de la capa Java. Su definición es la siguiente:

typedef struct {
    const char* name;//Java层native方法的名字
    const char* signature;//Java层native方法的描述符
    void*       fnPtr;//对应JNI函数的指针
} JNINativeMethod;

El tercer parámetro es el número de métodos nativos registrados. Generalmente, varios métodos nativos se registran dinámicamente. Primero, se define una matriz JNINativeMethod y luego el puntero de matriz se pasa como un parámetro de la función RegisterNative, por lo que la siguiente matriz JNINativeMethod se define aquí:

JNINativeMethod nativeMethod[] = {
   
   {"dynamicLog", "()V", (void*)nativeDynamicLog}};

Finalmente, se llama a la función RegisterNative para completar el registro dinámico:

env->RegisterNatives(clz, nativeMethod, sizeof(nativeMethod)/sizeof(nativeMethod[0]));

Estructura JNIEnv

La estructura de JNIEnv mencionada anteriormente es antigua y poderosa. Apunta a una tabla de funciones que apunta a una serie de funciones JNI. Podemos realizar la interacción con la capa Java llamando a estas funciones JNI. Aquí hay algunas funciones definidas:

..........
jfieldID GetFieldID(jclass clazz, const char* name, const char* sig)
jboolean GetBooleanField(jobject obj, jfieldID fieldID)
jmethodID GetMethodID(jclass clazz, const char* name, const char* sig)
CallVoidMethod(jobject obj, jmethodID methodID, ...)
CallBooleanMethod(jobject obj, jmethodID methodID, ...)
..........

Aquí hay un breve vistazo a las cuatro funciones anteriores, la función GetFieldID () es para obtener el ID de una determinada variable en el objeto Java, y la función GetBooleanField () es para obtener la variable cuyo tipo de datos es booleano de acuerdo con el ID de la variable. La función GetMethodID () es para obtener el ID del método correspondiente en el objeto Java. CallVoidMethod () llama al método en el objeto correspondiente según el methodID, y el valor de retorno del método es de tipo Void. Mediante estas funciones podemos implementar el código que llama a la capa Java. Para obtener más funciones, consulte la documentación de la API.

Tipo de datos JNI

Mencionamos anteriormente que JNI define algunos de sus propios tipos de datos. Estos tipos de datos están conectados entre la capa Java y la capa C / C ++. Si se transmite un objeto, entonces C / C ++ no puede identificar este objeto. De manera similar, si el puntero C / C ++ es para la capa Java , it No hay forma de identificarlo, por lo que se necesita JNI para la coincidencia, por lo que debe definir algunos de sus propios tipos de datos.

1. Tipo de datos originales

Tipo de Java Tipo nativo Descripción
booleano jboolean 8 bits sin firmar
byte jbyte 8 bits firmados
carbonizarse jchar 16 bits sin firmar
corto jshort 16 bits firmados
En t jit 32 bits firmados
largo jlong 64 bits firmados
flotador jfloat 32 bits
doble jdouble 64 bits
vacío vacío N / A

2. Tipo de referencia

Anteriormente, solíamos obtener el objeto AndroidJni definiendo la referencia jclass y luego llamando a la función FindClass para obtener el objeto, por lo que JNI también define algunos tipos de referencia para que la capa JNI llame. Los tipos de referencia específicos son los siguientes:

jobject                     (all Java objects)
|
|-- jclass                  (java.lang.Class objects)
|-- jstring                 (java.lang.String objects)
|-- jarray                  (array)
|     |--jobjectArray       (object arrays)
|     |--jbooleanArray      (boolean arrays)
|     |--jbyteArray         (byte arrays)
|     |--jcharArray         (char arrays)
|     |--jshortArray        (short arrays)
|     |--jintArray          (int arrays)
|     |--jlongArray         (long arrays)
|     |--jfloatArray        (float arrays)
|     |--jdoubleArray       (double arrays)
|
|--jthrowable

3. Método e ID de variable.
 Cuando necesitamos llamar a un método en Java, primero debemos obtener su ID, llamar a la función JNI de acuerdo con el ID para obtener el método, el proceso de adquisición de variables es el mismo proceso, la definición de estructura de estos ID de la siguiente manera:

struct _jfieldID;              /* opaque structure */ 
typedef struct _jfieldID *jfieldID;   /* field IDs */ 

struct _jmethodID;              /* opaque structure */ 
typedef struct _jmethodID *jmethodID; /* method IDs */ 

Descriptor

1.
 Para obtener el objeto AndroidJni de Java, el descriptor de clase se obtiene llamando a la función FindClass (). El parámetro de la función solo tiene un parámetro de cadena. Encontramos que la cadena es la siguiente:

com/github/songnick/jni/AndroidJni

De hecho, este JNI define el descriptor de la clase, su regla es reemplazar el "." En "com.github.songnick.jni.AndroidJni" por "/".

2. Descriptor del método Cuando
 antes registramos dinámicamente el método nativo, la estructura JNINativeMethod contiene el descriptor del método, que es para determinar los parámetros y el valor de retorno del método nativo. El método dynamicLog () que definimos aquí no tiene parámetros, y el el valor está vacío por lo que corresponde El descriptor es: "() V", los corchetes son parámetros, y V significa que el valor de retorno está vacío. Echemos un vistazo a algunas castañas:

Descriptor de método Tipo de lenguaje Java
"() Ljava / lang / String;" Cadena f ();
"(ILjava / lang / Class;) J" long f (int i, clase c);
"([B) V" Cadena (byte [] bytes);

En el cuadro anterior, vemos que el tipo de retorno y los parámetros del método del método tienen tipos de referencia y tipos de datos básicos como boolean e int. Estos tipos de descriptores se introducen en la siguiente sección. Aquí, el descriptor de matriz se expresa mediante "[" y el descriptor de tipo correspondiente. Para matrices bidimensionales y matrices tridimensionales, se representa mediante "[[" y "[[[":

Descriptor Tipo de idioma Java
"[[YO" En t[][]
"[[[RE" doble[][][]

3. Descriptores de tipo de datos
 Hablamos de descriptores de método. Entonces, ¿qué pasa con los descriptores de tipo de datos como boolean e int? JNI define los descriptores de tipos de datos básicos de la siguiente manera:

Desciptor de campo Tipo de lenguaje Java
CON booleano
segundo byte
C carbonizarse
S corto
yo En t
J largo
F floa
re doble

Para los descriptores de tipo de referencia, comience con "L" y termine con ";", un ejemplo es el siguiente:

Desciptor de campo Tipo de lenguaje Java
"Ljava / lang / String;" Cuerda
"[Ljava / lang / Object;" Objeto[]

para resumir

La parte anterior es un análisis simple de la programación de JNI a través de una castaña, aquí es solo una simple introducción, es solo una parte de la programación de JNI, creo que cualquier tecnología o punto técnico no puede ser competente a través de un artículo, más La mayoría de ellos todavía dependemos de la práctica Solo descubriendo y resolviendo problemas en el proceso de la práctica, podemos tener una mejor comprensión y comprensión del conocimiento y lograr la competencia. Así que espero que pueda tener una comprensión preliminar de la programación JNI a través de este artículo, y no sentirá que la programación JNI es difícil. Puede leer más sobre la documentación de la API de JNI, para que comprenda mejor JNI. Aquí me gustaría hablar sobre la diferencia entre la programación JNI para Android. Puede leer más sobre los documentos oficiales de Google para la guía de programación JNI y los programas de demostración.



Autor: SongNick
Enlace: https: //www.jianshu.com/p/aba734d5b5cd
Fuente: Los libros de Jane
tienen derechos de autor del autor. Para reimpresiones comerciales, comuníquese con el autor para obtener autorización. Para reimpresiones no comerciales, indique la fuente.

Supongo que te gusta

Origin blog.csdn.net/qq_37381177/article/details/111526802
Recomendado
Clasificación