JNI es la interfaz nativa de Java. Define una forma para que el código de bytes que Android compila a partir de código administrado (escrito en los lenguajes de programación Java o Kotlin) interactúe con el código nativo (escrito en C/C++). JNI es neutra para los proveedores; es compatible con la carga de código de bibliotecas dinámicas compartidas; y, aunque a veces puede ser engorrosa, es bastante eficiente.
Nota: Debido a que Android compila Kotlin en un código de bytes compatible con ART de una manera similar al lenguaje de programación Java, puedes aplicar la orientación incluida en esta página a los dos lenguajes de programación, Kotlin y Java, en términos de la arquitectura de JNI y los costos asociados. Para obtener más información, consulta Kotlin y Android.
Si todavía no la conoces, lee la Especificación de la interfaz nativa Java para obtener un vistazo de cómo funciona JNI y cuáles son las funciones disponibles. Algunos aspectos de la interfaz no son obvios de inmediato en la primera lectura, por lo que te pueden resultar útiles las próximas secciones.
Para consultar referencias globales de JNI y conocer dónde se crean y se borran, usa la vista JNI heap en el Generador de perfiles de memoria de Android Studio 3.2 y versiones posteriores.
Sugerencias generales
Intenta minimizar la superficie de tu capa de JNI. Hay varias dimensiones para tener en cuenta. Tu solución de JNI debería respetar las siguientes pautas (detalladas a continuación según orden de importancia, comenzando por la más importante):
- Minimiza el ordenamiento de recursos en la capa de JNI. El ordenamiento en las capas de JNI tiene costos importantes. Intenta diseñar una interfaz que minimice la cantidad de datos necesarios para ordenar y la frecuencia con la que debes ordenar datos.
- Siempre que sea posible, evita la comunicación asíncrona entre código escrito en un lenguaje de programación administrado y código escrito en C++. De este modo, la interfaz JNI será más fácil de mantener. Por lo general, puedes simplificar las actualizaciones asíncronas de la IU si las mantienes en el mismo idioma que la IU. Por ejemplo, en lugar de invocar una función de C++ desde el subproceso de IU en el código Java mediante JNI, es mejor hacer una devolución de llamada entre dos subprocesos en el lenguaje de programación Java, en la que uno de ellos haga una llamada de bloqueo de C++ y, luego, notifique al subproceso de IU cuando se complete la llamada.
- Minimiza la cantidad de procesamientos que tienen que tocar JNI o que JNI tiene que tocar. Si tienes que usar conjuntos de subprocesos en los lenguajes Java y C++, intenta que la comunicación de JNI sea entre los propietarios de los conjuntos en lugar de entre subprocesos de trabajadores individuales.
- Mantén una baja cantidad de ubicaciones de origen C++ y Java en el código de la interfaz a fin de facilitar las reestructuraciones futuras. Considera usar una biblioteca de generación automática de JNI según corresponda.
JavaVM y JNIEnv
JNI define dos estructuras de datos clave: "JavaVM" y "JNIEnv". En esencia, ambas son punteros a punteros de tablas de funciones. (En la versión de C++, son clases con un puntero a una tabla de funciones y a una función de miembro para cada función de JNI que direcciona de manera indirecta por la tabla). La estructura JavaVM proporciona las funciones de la "interfaz de invocación", que permiten crear y destruir una JavaVM. En teoría, se pueden tener múltiples JavaVM por proceso, pero Android permite solo una.
La estructura JNIEnv proporciona la mayoría de las funciones de JNI. Todas tus funciones nativas reciben una JNIEnv como
el primer argumento, excepto para los métodos @CriticalNative
,
Consulta las llamadas nativas más rápidas.
La JNIEnv se usa para almacenamiento local de subprocesos. Por esa razón, no se puede compartir una JNIEnv entre subprocesos.
Si un fragmento de código no tiene otra forma de obtener JNIEnv, debes compartir JavaVM y usar GetEnv
para descubrir la JNIEnv del subproceso. (Si tiene una, consulta AttachCurrentThread
a continuación).
Las declaraciones C de JNIEnv y JavaVM son diferentes de las declaraciones C++. El archivo de inclusión de "jni.h"
proporciona diferentes typedefs según se incluya en C o C++. Por esta razón, es una mala idea incorporar argumentos de JNIEnv en los archivos de encabezado incluidos en ambos idiomas. (En otras palabras, si el archivo de encabezado requiere #ifdef __cplusplus
, es posible que debas realizar trabajos adicionales si alguna parte del encabezado hace referencia a JNIEnv).
Subprocesos
Todos los subprocesos son Linux, programados por el kernel. Por lo general, se inician desde el código administrado (mediante Thread.start()
), pero también pueden crearse en otro lugar y, luego, conectarse a JavaVM
. Por ejemplo, se puede conectar un subproceso iniciado con pthread_create()
o std::thread
usando las funciones AttachCurrentThread()
o AttachCurrentThreadAsDaemon()
. Un subproceso no tiene JNIEnv ni puede realizar llamadas de JNI hasta que se conecta.
En general, es mejor usar Thread.start()
para crear cualquier subproceso que necesite llamar al código Java. Esto te garantizará suficiente espacio de pila, estar en el ThreadGroup
correcto y usar el mismo ClassLoader
que tu código Java. También es más fácil establecer el nombre del subproceso para depurar en Java que en código nativo (consulta pthread_setname_np()
si tienes un pthread_t
o thread_t
, y std::thread::native_handle()
si tienes un std::thread
y quieres un pthread_t
).
Cuando se conecta un subproceso creado de forma nativa, se construye un objeto java.lang.Thread
y se agrega al elemento "principal" ThreadGroup
, lo que lo hará visible para el depurador. Llamar a AttachCurrentThread()
en un subproceso ya conectado constituye una no-op.
Android no suspende subprocesos que ejecuten código nativo. Si está en curso la recolección de elementos no utilizados o el depurador emitió una solicitud de suspensión, Android pausará el subproceso la próxima vez que haga una llamada de JNI.
Los subprocesos conectados a través de JNI deben llamar a DetachCurrentThread()
antes de salir.
Si la codificación resulta incómoda de manera directa, en Android 2.0 (Eclair) y versiones posteriores, puedes usar pthread_key_create()
para definir una función destructora que se llamará antes de que salga el subproceso, y llamar a DetachCurrentThread()
desde allí. (Usa esa clave con pthread_setspecific()
para almacenar la JNIEnv en thread-local-storage; de esa manera, se pasará a tu destructor como argumento).
jclass, jmethodID y jfieldID
Si deseas acceder al campo del objeto desde código nativo, deberías hacer lo siguiente:
- Obtén la referencia de objeto de clase para la clase con
FindClass
. - Obtén el ID de campo para el campo con
GetFieldID
. - Obtén los contenidos del campo con algo adecuado, como
GetIntField
.
De forma similar, para llamar un método, primero deberías obtener una referencia de objeto de clase y, luego, un ID de método. Los ID suelen ser punteros a estructuras internas de datos de tiempo de ejecución. Para buscarlos, es posible que se requieran varias comparaciones de strings. Sin embargo, una vez que los tienes, la llamada real para obtener el campo o invocar el método es muy rápida.
Si el rendimiento es importante, te resultará útil buscar los valores una vez y almacenar en caché los resultados en tu código nativo. Como hay un límite de una JavaVM por proceso, es razonable almacenar esos datos en una estructura local estática.
Las referencias de clase, los ID de campo y los ID de método tienen una validez garantizada hasta que se descarga la clase. Las clases solo se descargan si todas las clases asociadas con un ClassLoader pueden recolectarse como elementos no utilizados, lo que en Android es poco común, pero no imposible. No obstante, ten en cuenta que jclass
es una referencia de clase y debe protegerse con una llamada a NewGlobalRef
(consulta la siguiente sección).
Si deseas almacenar en caché los ID cuando se carga una clase y volver a almacenarlos en caché de manera automática si alguna vez se descarga la clase y se vuelve a cargar, la forma adecuada de inicializar los ID es agregar un fragmento de código como el siguiente en la clase adecuada:
Kotlin
companion object { /* * We use a static class initializer to allow the native code to cache some * field offsets. This native function looks up and caches interesting * class/field/method IDs. Throws on failure. */ private external fun nativeInit() init { nativeInit() } }
Java
/* * We use a class initializer to allow the native code to cache some * field offsets. This native function looks up and caches interesting * class/field/method IDs. Throws on failure. */ private static native void nativeInit(); static { nativeInit(); }
Crea un método nativeClassInit
en tu código C/C++ que realice las búsquedas de ID. El código se ejecutará una vez, cuando se inicialice la clase. Si alguna vez se descarga la clase y se vuelve a cargar, volverá a ejecutarse.
Referencias globales y locales
Todos los argumentos que se pasan a un método nativo y casi todos los objetos que muestra una función de JNI se consideran una "referencia local". Esto quiere decir que es válida mientras dure el método nativo actual en el subproceso actual. Incluso si el objeto en sí sigue activo después de que vuelve el método nativo, la referencia no es válida.
Esto se aplica a todas las subclases de jobject
, incluidas jclass
, jstring
y jarray
.
(El tiempo de ejecución sirve de aviso sobre la mayoría de los usos incorrectos de referencias cuando están habilitadas las verificaciones de JNI extendidas).
La única manera de obtener referencias no locales es a través de las funciones NewGlobalRef
y NewWeakGlobalRef
.
Si quieres conservar una referencia por un período más prolongado, debes usar una referencia "global". La función NewGlobalRef
toma la referencia local como un argumento y muestra una global.
La validez de la referencia global está garantizada hasta que llamas a DeleteGlobalRef
.
Este patrón se usa comúnmente cuando se almacena en caché una jclass que FindClass
muestra, p. ej.:
jclass localClass = env->FindClass("MyClass"); jclass globalClass = reinterpret_cast<jclass>(env->NewGlobalRef(localClass));
Todos los métodos de JNI aceptan referencias locales y globales como argumentos.
Es posible que las referencias del mismo objeto tengan valores diferentes.
Por ejemplo, los valores que se muestran de llamadas consecutivas a NewGlobalRef
en el mismo objeto pueden ser diferentes.
Para ver si dos referencias remiten al mismo objeto, debes usar la función IsSameObject
. Nunca compares referencias con ==
en código nativo.
Una consecuencia de eso es que no debes suponer que las referencias de objetos son constantes o únicas en código nativo. El valor que representa un objeto puede ser diferente
de una invocación de un método a la siguiente, y es posible que dos
objetos diferentes podrían tener el mismo valor en llamadas consecutivas. No utilizar
Valores jobject
como claves.
Los programadores están obligados a "no asignar de manera excesiva" referencias locales. En la práctica, esto significa que, si creas grandes cantidades de referencias locales, quizás mientras ejecutas un arreglo de objetos, debes liberarlas manualmente con DeleteLocalRef
en lugar de dejar que JNI lo haga por ti. Solo se requiere la implementación a fin de reservar ranuras para 16 referencias locales, por lo que, si necesitas más, debes borrarlas a medida que avanzas o usar EnsureLocalCapacity
/PushLocalFrame
para reservar más.
Ten en cuenta que jfieldID
y jmethodID
son tipos opacos, no referencias a objetos, y no se deben pasar a NewGlobalRef
. Los punteros de datos sin procesar que se muestran con funciones como GetStringUTFChars
y GetByteArrayElements
tampoco son objetos. (Se pueden pasar entre subprocesos y son válidos hasta que se realiza la llamada de liberación coincidente).
Vale la pena mencionar un caso inusual. Si conectas un subproceso nativo con AttachCurrentThread
, el código que ejecutes nunca liberará las referencias locales automáticamente hasta que se desconecte el subproceso. Deberás borrar de forma manual todas las referencias locales que crees. En general, es probable que debas borrar manualmente cualquier código nativo que cree referencias locales en un bucle.
Ten cuidado cuando uses referencias globales. Estas pueden ser inevitables, pero son difíciles de depurar y pueden provocar (malos) comportamientos de la memoria difíciles de diagnosticar. A igualdad de condiciones, es probable que sea mejor una solución con menos referencias globales.
Strings UTF-8 y UTF-16
El lenguaje de programación Java usa UTF-16. Para una mayor practicidad, JNI proporciona métodos que funcionan también con UTF-8 modificado. La codificación modificada es útil para el código C porque codifica \u0000 como 0xc0 0x80 en lugar de 0x00. La ventaja es que puedes contar con strings de estilo C terminadas en cero, compatibles para usar con funciones de strings libc estándares. La desventaja es que no puedes pasar datos UTF-8 arbitrarios a JNI y esperar que funcione correctamente.
Para obtener la representación UTF-16 de un String
, usa GetStringChars
.
Ten en cuenta que las strings UTF-16 no terminan en cero y se permite \u0000,
así que deberás conservar la longitud de la cadena
y el puntero jchar.
No olvides ejecutar Release
para las strings obtenidas con Get
. Las funciones de string muestran jchar*
o jbyte*
, que son punteros de estilo C para datos primitivos en lugar de referencias locales. Se garantizan como válidas hasta que se llama a Release
, lo que significa que no se liberan cuando se muestra el método nativo.
Los datos pasados a NewStringUTF deben tener formato UTF-8 modificado. Un error común es leer datos de caracteres de un archivo o una transmisión de red y entregarlos a NewStringUTF
sin filtrarlos.
A menos que sepas que los datos son MUTF-8 válidos (o ASCII de 7 bits, que es compatible con el subconjunto), debes quitar los caracteres no válidos o convertirlos a un formato adecuado UTF-8 modificado.
De lo contrario, es probable que la conversión a UTF-16 muestre resultados inesperados.
CheckJNI, que está activado de forma predeterminada para emuladores, analiza strings y anula la VM si recibe entradas no válidas.
Antes de Android 8, solía ser más rápido operar con cadenas UTF-16 como Android
no requería una copia en GetStringChars
, mientras que
GetStringUTFChars
requería una asignación y una conversión a UTF-8.
Android 8 cambió la representación String
para usar 8 bits por carácter
para cadenas ASCII (para ahorrar memoria) y comenzaste a usar un
moviéndose
recolector de elementos no utilizados. Estas funciones reducen en gran medida el número de casos en los que ART
puede proporcionar un puntero a los datos de String
sin hacer una copia, incluso
para GetStringCritical
. Sin embargo, si la mayoría de las cadenas que procesa el código
son breves, es posible evitar la asignación y la desasignación en la mayoría de los casos
con un búfer asignado de pila y GetStringRegion
, o
GetStringUTFRegion
Por ejemplo:
constexpr size_t kStackBufferSize = 64u; jchar stack_buffer[kStackBufferSize]; std::unique_ptr<jchar[]> heap_buffer; jchar* buffer = stack_buffer; jsize length = env->GetStringLength(str); if (length > kStackBufferSize) { heap_buffer.reset(new jchar[length]); buffer = heap_buffer.get(); } env->GetStringRegion(str, 0, length, buffer); process_data(buffer, length);
Arreglos primitivos
JNI proporciona funciones para acceder a los contenidos de objetos de arreglos. Si bien se debe acceder a los arreglos de objetos de a una entrada por vez, los arreglos de primitivos pueden leerse y escribirse directamente como si estuvieran declarados en C.
Para que la interfaz sea lo más eficiente posible sin limitar la implementación de VM, la familia de llamadas Get<PrimitiveType>ArrayElements
permite que el tiempo de ejecución muestre un puntero para los elementos reales o asigne un poco de memoria y cree una copia. De cualquier manera, se garantiza que el puntero sin procesar que se muestra sea válido hasta que se emita la llamada Release
correspondiente (lo que implica que, si no se copiaron los datos, se fijará el objeto del arreglo y no podrá reubicarse durante la compactación de la pila).
Debes ejecutar Release
para cada arreglo que obtengas con Get
. Además, si la llamada Get
falla, debes asegurarte de que el código no intente ejecutar Release
para un puntero NULL más adelante.
Puedes determinar si los datos se copiaron pasando un puntero que no sea NULL para el argumento isCopy
, aunque esto no suele ser muy útil.
La llamada Release
toma un argumento mode
que puede tener uno de tres valores. Las acciones realizadas por el tiempo de ejecución dependen de si devolvió un puntero a los datos reales o una copia de él:
0
- Real: El objeto de arreglos no está fijado.
- Copia: Los datos se vuelven a copiar. Se libera el búfer con la copia.
JNI_COMMIT
- Real: No pasa nada.
- Copia: Los datos se vuelven a copiar. No se libera el búfer con la copia.
JNI_ABORT
- Real: El objeto de arreglos no está fijado. No se anulan las escrituras anteriores.
- Copia: Se libera el búfer con la copia y se pierden todos los cambios hechos en él.
Una razón para verificar la marca isCopy
es saber si necesitas llamar a Release
con JNI_COMMIT
después de hacer cambios en un arreglo; si alternas entre hacer cambios y ejecutar código que use el contenido del arreglo, es posible que puedas omitir la confirmación no operativa. Otra posible razón para verificar la marca es tener un manejo eficiente de JNI_ABORT
. Por ejemplo, es posible que desees obtener un arreglo; modificarlo en su lugar, pasar fragmentos a otras funciones; y, después, descartar los cambios. Si sabes que JNI está haciendo una copia nueva para ti, no es necesario crear otra copia "editable". Si JNI te pasa el original, entonces sí es necesario que hagas tu propia copia.
Es un error común (repetido en el código de ejemplo) suponer que puedes omitir la llamada a Release
si *isCopy
es falso. Este no es el caso. Si no se asignó un búfer de copia, se debe fijar la memoria original y el recolector de elementos no utilizados no puede moverla.
Además, ten en cuenta que la marca JNI_COMMIT
no libera el arreglo y, llegado el momento, deberás volver a llamar a Release
con una marca diferente.
Llamadas de región
Existe una alternativa a llamadas como Get<Type>ArrayElements
y GetStringChars
que puede ser muy útil cuando solo deseas copiar datos. Ten en cuenta lo siguiente:
jbyte* data = env->GetByteArrayElements(array, NULL); if (data != NULL) { memcpy(buffer, data, len); env->ReleaseByteArrayElements(array, data, JNI_ABORT); }
En este caso, se toma el arreglo, se copian los primeros elementos de bytes de len
y luego se libera el arreglo. Según la implementación, la llamada a Get
fijará o copiará el contenido del arreglo.
El código copia los datos (quizá por segunda vez) y, luego, llama a Release
. En este caso, JNI_ABORT
garantiza que no haya una tercera copia.
Es posible obtener el mismo resultado de una manera más sencilla:
env->GetByteArrayRegion(array, 0, len, buffer);
Este cambio tiene varias ventajas, por ejemplo:
- Requiere una llamada de JNI en lugar de 2, lo que reduce la sobrecarga.
- No requiere fijaciones ni copias adicionales de datos.
- Reduce el riesgo de error del programador (no hay riesgo de olvidarse de llamar a
Release
después de que falla algo).
De manera similar, puedes usar la llamada Set<Type>ArrayRegion
para copiar datos en un array, y GetStringRegion
o
GetStringUTFRegion
para copiar caracteres de un
String
Excepciones
No debes llamar a casi ninguna de las funciones de JNI mientras haya una excepción pendiente.
Se espera que el código detecte la excepción (a través del valor de retorno de la función, ExceptionCheck
o ExceptionOccurred
) y la muestre, o borre la excepción y la maneje.
Las únicas funciones de JNI que puedes llamar cuando hay una excepción pendiente son las siguientes:
DeleteGlobalRef
DeleteLocalRef
DeleteWeakGlobalRef
ExceptionCheck
ExceptionClear
ExceptionDescribe
ExceptionOccurred
MonitorExit
PopLocalFrame
PushLocalFrame
Release<PrimitiveType>ArrayElements
ReleasePrimitiveArrayCritical
ReleaseStringChars
ReleaseStringCritical
ReleaseStringUTFChars
Muchas llamadas de JNI pueden generar una excepción, pero suelen proporcionar una forma más sencilla de verificar si hay fallas. Por ejemplo, si NewString
muestra un valor que no es NULL, no es necesario comprobar si hay una excepción. Sin embargo, si llamas a un método (con una función como CallObjectMethod
), siempre deberás verificar si hay una excepción, ya que el valor que se muestre no será válido si se genera una excepción.
Ten en cuenta que las excepciones arrojadas por el código administrado no desenrollan la pila nativa.
o los fotogramas. (Y las excepciones de C++, generalmente no recomendadas en Android, no deben
arrojado a través del límite de transición de JNI desde el código C++ al código administrado).
Las instrucciones de Throw
y ThrowNew
de JNI solo establecen un puntero de excepción en el subproceso actual. Cuando regreses del código nativo al administrado, se anotará y manejará la excepción de forma adecuada.
El código nativo puede "atrapar" una excepción llamando a ExceptionCheck
o ExceptionOccurred
, y borrarla con ExceptionClear
. Como siempre, descartar excepciones sin manejarlas puede ocasionar problemas.
No hay funciones integradas para manipular el objeto de Throwable
en sí, por lo que, si quieres (por ejemplo) obtener la string de excepción, deberás encontrar la clase de Throwable
; buscar el ID del método para getMessage "()Ljava/lang/String;"
; invocarlo; y, si el resultado no es NULL, usar GetStringUTFChars
para obtener algo que puedas entregar a printf(3)
o el equivalente.
Verificación extendida
En JNI, hay muy poca comprobación de errores. Los errores suelen provocar una falla. Android también ofrece un modo llamado CheckJNI, en el que los punteros de la tabla de funciones JavaVM y JNIEnv se pasan a tablas de funciones que realizan una serie extendida de verificaciones antes de llamar a la implementación estándar.
Las comprobaciones adicionales incluyen lo siguiente:
- Arreglos: Se intenta asignar un arreglo con tamaño negativo.
- Punteros erróneos: Se pasa un jarray/jclass/jobject/jstring erróneo a una llamada de JNI o se pasa un puntero NULL a una llamada de JNI con un argumento que no admite valores NULL.
- Nombres de clase: Se pasa algo que no es el estilo "java/lang/String" de nombre de clase a una llamada de JNI.
- Llamadas críticas: Se realiza una llamada de JNI entre una obtención "crítica" y su correspondiente liberación.
- ByteBuffers directos: se pasan argumentos incorrectos a
NewDirectByteBuffer
. - Excepciones: Se hace una llamada de JNI mientras hay una excepción pendiente.
- JNIEnv*: Se usa un JNIEnv* del subproceso incorrecto.
- jfieldIDs: Se usa un jfieldID NULL o un jfieldID para establecer un campo en un valor del tipo incorrecto (intentar asignar un StringBuilder a un campo String, por ejemplo), o se usa un jfieldID para un campo estático con el fin de establecer un campo de instancia o viceversa, o se usa un jfieldID de una clase con instancias de otra clase.
- jmethodIDs: Se usa un tipo incorrecto de jmethodID cuando se realiza una llamada de JNI con
Call*Method
: tipo incorrecto de datos que se muestra, falta de coincidencia estática/no estática, tipo incorrecto para "esto" (llamadas no estáticas) o clase incorrecta (para llamadas estáticas). - Referencias: Se usa
DeleteGlobalRef
oDeleteLocalRef
en el tipo de referencia incorrecto. - Modos de liberación: Se pasa un modo de liberación erróneo a una llamada de liberación (algo distinto de
0
,JNI_ABORT
oJNI_COMMIT
). - Seguridad de tipos: Se devuelve un tipo incompatible desde tu método nativo (se devuelve un StringBuilder desde un método que declara que devuelve una String, por ejemplo).
- UTF-8: Se pasa una secuencia de bytes UTF-8 modificados no válida a una llamada de JNI.
(Todavía no se verifica la accesibilidad de los métodos y campos; las restricciones de acceso no se aplican al código nativo).
Hay varias formas de habilitar CheckJNI.
Si estás utilizando el emulador, CheckJNI está activado de manera predeterminada.
Si tienes un dispositivo con derechos de administrador, puedes usar la siguiente secuencia de comandos para reiniciar el tiempo de ejecución con CheckJNI habilitado:
adb shell stop adb shell setprop dalvik.vm.checkjni true adb shell start
En cualquiera de estos casos, verás algo como esto en el resultado del logcat cuando se inicie el tiempo de ejecución:
D AndroidRuntime: CheckJNI is ON
Si tienes un dispositivo normal, puedes usar el siguiente comando:
adb shell setprop debug.checkjni 1
Esto no afectará a las apps que ya se estén ejecutando, pero cualquier app iniciada a partir de ese momento tendrá CheckJNI habilitado. (Cambiar la propiedad a cualquier otro valor o reiniciarla volverá a inhabilitar CheckJNI). En este caso, la próxima vez que se inicie una app, verás en el resultado del logcat algo similar a lo que se muestra a continuación:
D Late-enabling CheckJNI
También puedes establecer el atributo android:debuggable
en el manifiesto de tu aplicación como
activar CheckJNI solo para tu app. Ten en cuenta que las herramientas de compilación de Android harán esto automáticamente para
ciertos tipos de compilaciones.
Bibliotecas nativas
Puedes cargar código nativo de bibliotecas compartidas con la biblioteca System.loadLibrary
estándar.
En la práctica, las versiones anteriores de Android tenían errores en PackageManager que hacían que la instalación y la actualización de bibliotecas nativas no fueran procesos confiables. El proyecto ReLinker ofrece soluciones alternativas para ese y otros problemas de carga de bibliotecas nativas.
Llama a System.loadLibrary
(o ReLinker.loadLibrary
) desde un inicializador de clase estático. El argumento es el nombre de la biblioteca "sin adornar", por lo que, para cargar libfubar.so
, pasarías en "fubar"
.
Si solo tienes una clase con métodos nativos, lo lógico es que la llamada a System.loadLibrary
esté en un inicializador estático para esa clase. De lo contrario, tal vez te convenga realizar la llamada desde Application
para asegurarte de que siempre se cargue la biblioteca y de que se cargue antes.
Hay dos formas de que el tiempo de ejecución encuentre los métodos nativos. Puedes registrarlos de forma explícita con RegisterNatives
o puedes permitir que el tiempo de ejecución los busque de forma dinámica con dlsym
. Las ventajas de RegisterNatives
son que verificas por adelantado que los símbolos existan y, además, puedes tener bibliotecas compartidas más pequeñas y rápidas si no exportas nada más que JNI_OnLoad
. La ventaja de permitir que el tiempo de ejecución descubra tus funciones es que hay menos código para escribir.
Para usar RegisterNatives
, haz lo siguiente:
- Proporciona una función
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved)
. - En tu
JNI_OnLoad
, registra todos los métodos nativos conRegisterNatives
. - Compila con
-fvisibility=hidden
para que solo se exporteJNI_OnLoad
de tu biblioteca. De este modo, se produce un código más rápido y más pequeño, y se evitan posibles colisiones con otras bibliotecas cargadas en tu app (pero crea seguimientos de pila menos útiles) si tu app falla en el código nativo).
El inicializador estático debe tener la siguiente apariencia:
Kotlin
companion object { init { System.loadLibrary("fubar") } }
Java
static { System.loadLibrary("fubar"); }
La función JNI_OnLoad
debería verse de la siguiente manera si está escrita en C++:
JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) { JNIEnv* env; if (vm->GetEnv(reinterpret_cast<void**>(&env), JNI_VERSION_1_6) != JNI_OK) { return JNI_ERR; } // Find your class. JNI_OnLoad is called from the correct class loader context for this to work. jclass c = env->FindClass("com/example/app/package/MyClass"); if (c == nullptr) return JNI_ERR; // Register your class' native methods. static const JNINativeMethod methods[] = { {"nativeFoo", "()V", reinterpret_cast<void*>(nativeFoo)}, {"nativeBar", "(Ljava/lang/String;I)Z", reinterpret_cast<void*>(nativeBar)}, }; int rc = env->RegisterNatives(c, methods, sizeof(methods)/sizeof(JNINativeMethod)); if (rc != JNI_OK) return rc; return JNI_VERSION_1_6; }
Para usar en su lugar el "descubrimiento" de métodos nativos, debes nombrarlos de una manera específica (consulta las especificaciones de JNI para obtener más detalles). Eso significa que, si la firma de un método es incorrecta, no lo sabrás hasta la primera vez que se invoque el método.
Cualquier llamada a FindClass
realizada desde JNI_OnLoad
resolverá las clases en el contexto del cargador de clases utilizado para cargar la biblioteca compartida. Cuando se llama desde otros contextos, FindClass
usa el cargador de clases asociado con el método en la parte superior de la pila de Java; si no hay uno (porque la llamada proviene de un subproceso nativo que acaba de conectarse), utiliza el cargador de la clase "system" (sistema). El cargador de clases del sistema no conoce las clases de tu aplicación, por lo que no podrás buscar tus propias clases con FindClass
en ese contexto. Esto hace que JNI_OnLoad
sea un lugar conveniente para buscar y almacenar clases en caché: una vez
tienes una referencia global válida de jclass
puedes usarlo desde cualquier subproceso adjunto.
Llamadas nativas más rápidas con @FastNative
y @CriticalNative
Los métodos nativos pueden anotarse con
@FastNative
o
@CriticalNative
(pero no ambos) para acelerar las transiciones entre el código administrado y el nativo. Sin embargo, estas anotaciones
incluyen ciertos cambios de comportamiento que se deben considerar cuidadosamente antes de usarlos. Si bien
menciona brevemente estos cambios a continuación, consulta la documentación para obtener más detalles.
La anotación @CriticalNative
solo se puede aplicar a métodos nativos que no
usar objetos administrados (en parámetros o valores que se devuelven, o como un this
implícito), y este
cambia la ABI de transición de JNI. La implementación nativa debe excluir la
Los parámetros JNIEnv
y jclass
de su firma de función.
Cuando ejecutas un método @FastNative
o @CriticalNative
, el elemento no utilizado
no puede suspender el subproceso para tareas esenciales y es posible que se bloquee. No utilices
anotaciones para métodos de larga duración, incluidos los métodos que suelen ser rápidos, pero generalmente no delimitados.
En particular, el código no debería realizar operaciones significativas de E/S ni adquirir bloqueos nativos que
pueden retenerse por mucho tiempo.
Estas anotaciones se implementaron para el uso del sistema desde
Android 8
y pasó a pruebas públicas de CTS
de la API en Android 14. Es probable que estas optimizaciones también funcionen en dispositivos con Android 8 a 13 (aunque
sin las sólidas garantías de CTS), pero la búsqueda dinámica de métodos nativos solo se admite en
A partir de Android 12 y versiones posteriores, el registro explícito con JNI RegisterNatives
es obligatorio.
para ejecutar versiones de Android 8 a 11. Estas anotaciones se ignoran en Android 7: la falta de coincidencia de ABI
para @CriticalNative
daría lugar a un agrupamiento de argumentos incorrecto y probablemente fallas.
En el caso de los métodos críticos de rendimiento que necesitan estas anotaciones, se recomienda
registra de forma explícita los métodos con RegisterNatives
de JNI en lugar de depender del
“descubrimiento” basado en nombres de métodos nativos. Para obtener un rendimiento
óptimo de inicio de la app,
para incluir emisores de métodos @FastNative
o @CriticalNative
en la
perfil de Baseline. A partir de Android 12,
una llamada a un método nativo @CriticalNative
desde un método administrado compilado es casi tan
más económico que una llamada no intercalada en C/C++, siempre que todos los argumentos se ajusten a los registros (por ejemplo, hasta un
y hasta 8 argumentos de punto flotante en arm64.
A veces puede ser preferible dividir un método nativo en dos, un método muy rápido que puede y otro para los casos lentos. Por ejemplo:
Kotlin
fun writeInt(nativeHandle: Long, value: Int) { // A fast buffered write with a `@CriticalNative` method should succeed most of the time. if (!nativeTryBufferedWriteInt(nativeHandle, value)) { // If the buffered write failed, we need to use the slow path that can perform // significant I/O and can even throw an `IOException`. nativeWriteInt(nativeHandle, value) } } @CriticalNative external fun nativeTryBufferedWriteInt(nativeHandle: Long, value: Int): Boolean external fun nativeWriteInt(nativeHandle: Long, value: Int)
Java
void writeInt(long nativeHandle, int value) { // A fast buffered write with a `@CriticalNative` method should succeed most of the time. if (!nativeTryBufferedWriteInt(nativeHandle, value)) { // If the buffered write failed, we need to use the slow path that can perform // significant I/O and can even throw an `IOException`. nativeWriteInt(nativeHandle, value); } } @CriticalNative static native boolean nativeTryBufferedWriteInt(long nativeHandle, int value); static native void nativeWriteInt(long nativeHandle, int value);
Consideraciones para 64 bits
Para admitir arquitecturas con punteros de 64 bits, usa un campo long
en lugar de un int
cuando almacenes un puntero en una estructura nativa de un campo Java.
Funciones no compatibles y retrocompatibilidad
Se admiten todas las funciones de JNI 1.6, salvo las siguientes excepciones:
DefineClass
no está implementado. Android no usa archivos de clase ni códigos de byte Java, por lo que el pasaje en datos de clase binaria no funciona.
En cuanto a la retrocompatibilidad de Android, es posible que debas tener en cuenta lo siguiente:
- Búsqueda dinámica de funciones nativas
Hasta Android 2.0 (Eclair), el carácter "$" no se había convertido de manera correcta a "_00024" durante las búsquedas de nombres de métodos. Para solucionar este problema, es necesario utilizar un registro explícito o sacar los métodos nativos de las clases internas.
- Separación de subprocesos
Hasta Android 2.0 (Eclair), no era posible usar una función destructora
pthread_key_create
para evitar la verificación "thread should be detached before exit" (el subproceso debe estar desconectado antes de salir). (El tiempo de ejecución también usa una función destructora pthread key, por lo que sería una carrera para ver a quién se llama primero). - Referencias globales no seguras
Hasta Android 2.2 (Froyo), no se habían implementado las referencias globales no seguras. Las versiones anteriores no permiten que se usen de ninguna manera. Puedes usar las constantes de la versión de la plataforma Android para probar la compatibilidad.
Hasta Android 4.0 (Ice Cream Sandwich), las referencias globales poco seguras solo podían pasarse a
NewLocalRef
,NewGlobalRef
yDeleteWeakGlobalRef
. (La especificación recomienda encarecidamente a los programadores que creen referencias definitivas para las no seguras globales antes de hacer cualquier cosa con ellas, por lo que esto no debería ser un impedimento para nada).A partir de Android 4.0 (Ice Cream Sandwich), se pueden usar las referencias globales no seguras como cualquier otra de las referencias de JNI.
- Referencias locales
Hasta Android 4.0 (Ice Cream Sandwich), las referencias locales eran, en realidad, punteros directos. Ice Cream Sandwich agregó el direccionamiento indirecto necesario para admitir una mejor recolección de elementos no utilizados, pero eso quiere decir que muchos de los errores de JNI no se pueden detectar en versiones anteriores. Consulta Cambios de referencia local de JNI en ICS para obtener más información.
En las versiones de Android anteriores a Android 8.0, la cantidad de referencias locales tenía un límite específico de la versión. A partir de Android 8.0, el sistema admite referencias locales ilimitadas.
- Determinación del tipo de referencia con
GetObjectRefType
Hasta Android 4.0 (Ice Cream Sandwich), como consecuencia del uso de punteros directos (ver arriba), era imposible implementar correctamente
GetObjectRefType
. En su lugar, utilizábamos una heurística que analizaba la tabla de las referencias globales no seguras, los argumentos, la tabla de las referencias locales y la tabla de las referencias globales (en ese orden). La primera vez que encontraba tu puntero directo, informaba que la referencia era del tipo que estaba examinando en ese momento. Esto significaba, por ejemplo, que, si llamabas aGetObjectRefType
en una jclass global igual a la jclass pasada como un argumento implícito para el método nativo estático, obteníasJNILocalRefType
en lugar deJNIGlobalRefType
. @FastNative
y@CriticalNative
Hasta Android 7, se ignoraron estas anotaciones de optimización. La ABI la discrepancia para
@CriticalNative
generaría un argumento incorrecto el ordenamiento y las posibles fallas.La búsqueda dinámica de funciones nativas para
@FastNative
y Los métodos@CriticalNative
no se implementaron en Android 8 a 10. contiene errores conocidos en Android 11. Usar estas optimizaciones sin es probable que el registro explícito conRegisterNatives
de JNI provocarán fallas en Android 8 a 11.FindClass
tiraClassNotFoundException
Para garantizar la retrocompatibilidad, Android arroja
ClassNotFoundException
. en lugar deNoClassDefFoundError
cuando no se encuentra una claseFindClass
Este comportamiento es coherente con la API de reflexión de Java.Class.forName(name)
Preguntas frecuentes: ¿Por qué se muestra UnsatisfiedLinkError
?
Cuando se trabaja con código nativo, no es raro ver un error como este:
java.lang.UnsatisfiedLinkError: Library foo not found
En algunos casos, significa lo que dice: no se encontró la biblioteca. En otros casos, la biblioteca existe, pero dlopen(3)
no pudo abrirla, y los detalles del error se pueden encontrar en el mensaje de detalles de la excepción.
Razones comunes por las que puedes encontrar excepciones de biblioteca no encontrada:
- La biblioteca no existe o no es accesible para la app. Usa
adb shell ls -l <path>
para verificar su presencia y sus permisos. - La biblioteca no se construyó con el NDK. Esto puede provocar dependencias en funciones o bibliotecas que no existen en el dispositivo.
Otra clase de errores de UnsatisfiedLinkError
se ve así:
java.lang.UnsatisfiedLinkError: myfunc at Foo.myfunc(Native Method) at Foo.main(Foo.java:10)
En logcat, verás:
W/dalvikvm( 880): No implementation found for native LFoo;.myfunc ()V
Esto significa que el tiempo de ejecución trató de encontrar un método coincidente, pero no lo logró. A continuación, se indican algunas razones comunes para eso:
- No se carga la biblioteca. Consulta el resultado del logcat para ver si hay mensajes sobre la carga de la biblioteca.
- No se encuentra el método debido a una falta de coincidencia de nombre o firma. Las causas más comunes de eso pueden ser las siguientes:
- Para la búsqueda diferida de métodos, no declarar funciones de C++ con
extern "C"
y no contar con visibilidad adecuada (JNIEXPORT
). Ten en cuenta que, antes de Ice Cream Sandwich, la macro JNIEXPORT era incorrecta, por lo que usar una GCC nueva con unjni.h
anterior no funcionará. Puedes usararm-eabi-nm
para ver los símbolos a medida que aparecen en la biblioteca. Si se ven alterados (algo así como_Z15Java_Foo_myfuncP7_JNIEnvP7_jclass
en lugar deJava_Foo_myfunc
), o si el tipo de símbolo es una "t" minúscula en lugar de una "T" mayúscula, debes ajustar la declaración. - Para registros explícitos, errores menores al ingresar la firma del método. Asegúrate de que lo que estés pasando a la llamada de registro coincida con la firma en el archivo de registro.
Recuerda que "B" es
byte
y "Z" esboolean
. Los componentes de nombre de clase en las firmas comienzan con "L", terminan con ";" y usan "/" para separar nombres de paquete/clase y "$" para separar nombres de clase interna (Ljava/util/Map$Entry;
, por ejemplo).
- Para la búsqueda diferida de métodos, no declarar funciones de C++ con
Usar javah
para generar automáticamente encabezados de JNI puede ayudar a evitar algunos problemas.
Preguntas frecuentes: ¿Por qué FindClass
no encontró mi clase?
(Esta recomendación se puede aplicar cuando no se pueden encontrar métodos con GetMethodID
o GetStaticMethodID
, o campos con GetFieldID
o GetStaticFieldID
).
Asegúrate de que la string del nombre de clase tenga el formato correcto. Los nombres de las clases JNI comienzan con el nombre del paquete y se separan con barras diagonales, como java/lang/String
. Si buscas una clase de arreglo, debes comenzar con la cantidad adecuada de corchetes y también debes unir la clase con "L" y ";". Por lo tanto, un arreglo unidimensional de String
sería [Ljava/lang/String;
.
Si buscas una clase interna, usa "$" en lugar de ".". Usar javap
en el archivo .class suele ser una buena manera de averiguar el nombre interno de la clase.
Si habilitas la reducción de código, asegúrate de configurar el código que se va a conservar. Es importante configurar reglas de conservación adecuadas, ya que, de lo contrario, el reductor de código podría quitar clases, métodos o campos que solo se usan desde JNI.
Si el nombre de clase se ve bien, es posible que el problema esté relacionado con el cargador de clases. FindClass
desea iniciar la búsqueda de clases en el cargador de clases asociado con tu código. Examina la pila de llamadas, que será similar a la siguiente:
Foo.myfunc(Native Method) Foo.main(Foo.java:10)
El mejor método es Foo.myfunc
. FindClass
encuentra el objeto ClassLoader
asociado con la clase Foo
y lo usa.
Eso suele funcionar bien. Puedes tener problemas si creas un hilo tú mismo (quizá si llamas a pthread_create
y lo conectas con AttachCurrentThread
). Así, no habrá marcos de pilas de tu aplicación.
Si llamas a FindClass
desde este subproceso, JavaVM se iniciará en el cargador de clases "system" en lugar de en el que esté asociado con tu aplicación, por lo que los intentos de encontrar clases específicas de la app fallarán.
Hay algunas maneras de solucionar eso, por ejemplo:
- Realiza las búsquedas de
FindClass
una vez, enJNI_OnLoad
, y almacena en caché las referencias de clase para uso posterior. Todas las llamadas deFindClass
realizadas como parte de la ejecución deJNI_OnLoad
usarán el cargador de clases asociado con la función que llamó aSystem.loadLibrary
(esta es una regla especial proporcionada para hacer más conveniente la inicialización de bibliotecas). Si el código de tu app carga la biblioteca,FindClass
usará el cargador de clases correcto. - Declara que tu método nativo tome un argumento de clase, y luego pasa
Foo.class
, a fin de pasar una instancia de la clase a las funciones que la necesitan. - Almacena en caché una referencia al objeto
ClassLoader
en alguna ubicación conveniente y emite directamente llamadas aloadClass
. Esto requiere un poco de esfuerzo.
Preguntas frecuentes: ¿Cómo comparto datos sin procesar con código nativo?
Es posible que te encuentres en una situación en la que necesites acceder tanto desde código administrado como desde código nativo a un búfer grande de datos sin procesar. Los ejemplos comunes incluyen la manipulación de mapas de bits o muestras de sonido. Hay dos enfoques básicos.
Puedes almacenar los datos en un byte[]
. Eso permite un acceso muy rápido desde el código administrado. Sin embargo, en lo que respecta al código nativo, no se garantiza que puedas acceder a los datos sin tener que copiarlos. En algunas implementaciones, GetByteArrayElements
y GetPrimitiveArrayCritical
mostrarán punteros reales para los datos sin procesar en la pila administrada, pero, en otras, se asignará un búfer a la pila nativa y se copiarán los datos.
La alternativa es almacenar los datos en un búfer de bytes directos. Estos se pueden crear con la función java.nio.ByteBuffer.allocateDirect
o la función NewDirectByteBuffer
de JNI. A diferencia de los búferes de bytes regulares, el almacenamiento no se asigna a la pila administrada, y siempre se puede acceder directamente a este desde el código nativo (obtén la dirección con GetDirectBufferAddress
). En función de cómo se implemente el acceso directo del búfer de bytes, el acceso a los datos de código administrado puede ser muy lento.
La elección de cuál usar depende de dos factores:
- ¿La mayoría de los accesos de datos se realizarán desde el código escrito en Java o en C/C++?
- Si, finalmente, los datos se pasan a una API del sistema, ¿en qué formato deben estar? (Por ejemplo, si los datos se pasan a una función que toma un byte [], probablemente no sea aconsejable hacer el procesamiento en un
ByteBuffer
directo).
Si no hay una mejor opción, usa un búfer de bytes directos. La compatibilidad con ellos está integrada de manera directa en JNI y el rendimiento debería mejorar en futuras versiones.