Советы по JNI

JNI — это собственный интерфейс Java. Он определяет способ взаимодействия байт-кода, который Android компилирует из управляемого кода (написанного на языках программирования Java или Kotlin), с собственным кодом (написанным на C/C++). JNI не зависит от поставщика, поддерживает загрузку кода из динамических общих библиотек и, хотя временами громоздок, достаточно эффективен.

Примечание. Поскольку Android компилирует Kotlin в ART-совместимый байт-код аналогично языку программирования Java, вы можете применить рекомендации на этой странице как к языкам программирования Kotlin, так и к Java с точки зрения архитектуры JNI и связанных с ней затрат. Дополнительные сведения см. в разделе Kotlin и Android .

Если вы еще не знакомы с ним, прочтите Спецификацию собственного интерфейса Java, чтобы понять, как работает JNI и какие функции доступны. Некоторые аспекты интерфейса не сразу бросаются в глаза при первом прочтении, поэтому следующие несколько разделов могут оказаться вам полезными.

Чтобы просмотреть глобальные ссылки JNI и узнать, где создаются и удаляются глобальные ссылки JNI, используйте представление кучи JNI в профилировщике памяти в Android Studio 3.2 и более поздних версиях.

Общие советы

Постарайтесь минимизировать влияние вашего уровня JNI. Здесь следует учитывать несколько аспектов. Ваше решение JNI должно стараться следовать этим рекомендациям (перечисленным ниже в порядке важности, начиная с самого важного):

  • Минимизируйте сортировку ресурсов на уровне JNI. Маршалинг по уровню JNI требует нетривиальных затрат. Попробуйте разработать интерфейс, который сводит к минимуму объем данных, которые необходимо упорядочивать, и частоту, с которой вам придется упорядочивать данные.
  • По возможности избегайте асинхронного взаимодействия между кодом, написанным на управляемом языке программирования, и кодом, написанным на C++ . Это облегчит поддержку вашего интерфейса JNI. Обычно асинхронные обновления пользовательского интерфейса можно упростить, сохраняя асинхронное обновление на том же языке, что и пользовательский интерфейс. Например, вместо вызова функции C++ из потока пользовательского интерфейса в коде Java через JNI, лучше выполнить обратный вызов между двумя потоками языка программирования Java, при этом один из них выполняет блокирующий вызов C++, а затем уведомляет поток пользовательского интерфейса. когда блокирующий вызов завершен.
  • Минимизируйте количество потоков, которые должны быть затронуты JNI. Если вам все же необходимо использовать пулы потоков как на языках Java, так и на языках C++, постарайтесь поддерживать связь JNI между владельцами пулов, а не между отдельными рабочими потоками.
  • Храните код интерфейса в небольшом количестве легко идентифицируемых источников исходного кода C++ и Java, чтобы облегчить будущие рефакторинги. При необходимости рассмотрите возможность использования библиотеки автоматического создания JNI.

JavaVM и JNIEnv

JNI определяет две ключевые структуры данных: «JavaVM» и «JNIEnv». Оба они по сути являются указателями на указатели на таблицы функций. (В версии C++ это классы с указателем на таблицу функций и функцией-членом для каждой функции JNI, которая выполняет косвенное обращение к таблице.) JavaVM предоставляет функции «интерфейса вызова», которые позволяют создавать и уничтожать JavaVM. Теоретически вы можете иметь несколько JavaVM для каждого процесса, но Android позволяет использовать только одну.

JNIEnv предоставляет большинство функций JNI. Все ваши собственные функции получают JNIEnv в качестве первого аргумента, за исключением методов @CriticalNative , см. более быстрые собственные вызовы .

JNIEnv используется для локального хранилища потоков. По этой причине вы не можете совместно использовать JNIEnv между потоками . Если у фрагмента кода нет другого способа получить свой JNIEnv, вам следует поделиться JavaVM и использовать GetEnv для обнаружения JNIEnv потока. (Предполагаем, что он есть; см. AttachCurrentThread ниже.)

Объявления C JNIEnv и JavaVM отличаются от объявлений C++. Включаемый файл "jni.h" предоставляет различные определения типов в зависимости от того, включен ли он в C или C++. По этой причине включать аргументы JNIEnv в файлы заголовков, входящие в состав обоих языков, — плохая идея. (Иными словами: если ваш файл заголовка требует #ifdef __cplusplus , вам, возможно, придется проделать дополнительную работу, если что-либо в этом заголовке относится к JNIEnv.)

Темы

Все потоки являются потоками Linux, запланированными ядром. Обычно они запускаются из управляемого кода (с помощью Thread.start() ), но их также можно создать в другом месте, а затем прикрепить к JavaVM . Например, поток, запущенный с помощью pthread_create() или std::thread можно присоединить с помощью функций AttachCurrentThread() или AttachCurrentThreadAsDaemon() . Пока поток не присоединен, он не имеет JNIEnv и не может выполнять вызовы JNI .

Обычно лучше использовать Thread.start() для создания любого потока, которому необходимо вызвать код Java. Это гарантирует, что у вас достаточно места в стеке, что вы находитесь в правильной ThreadGroup и используете тот же ClassLoader , что и ваш код Java. Также проще установить имя потока для отладки в Java, чем из собственного кода (см. pthread_setname_np() , если у вас есть pthread_t или thread_t , и std::thread::native_handle() , если у вас есть std::thread и вы хотите pthread_t ).

Присоединение потока, созданного в собственном коде, приводит к созданию объекта java.lang.Thread и добавлению его в «основной» ThreadGroup , что делает его видимым для отладчика. Вызов AttachCurrentThread() для уже подключенного потока невозможен.

Android не приостанавливает потоки, выполняющие собственный код. Если выполняется сбор мусора или отладчик выдал запрос на приостановку, Android приостановит поток при следующем вызове JNI.

Потоки, подключенные через JNI , должны вызвать DetachCurrentThread() перед завершением . Если писать код напрямую неудобно, в Android 2.0 (Eclair) и выше вы можете использовать pthread_key_create() чтобы определить функцию деструктора, которая будет вызываться перед завершением потока, и вызвать оттуда DetachCurrentThread() . (Используйте этот ключ с pthread_setspecific() для хранения JNIEnv в локальном хранилище потока; таким образом он будет передан в ваш деструктор в качестве аргумента.)

jclass, jmethodID и jfieldID

Если вы хотите получить доступ к полю объекта из машинного кода, вы должны сделать следующее:

  • Получите ссылку на объект класса для класса с помощью FindClass
  • Получите идентификатор поля для поля с помощью GetFieldID
  • Получите содержимое поля с помощью чего-нибудь подходящего, например GetIntField

Аналогично, чтобы вызвать метод, вы сначала должны получить ссылку на объект класса, а затем идентификатор метода. Идентификаторы часто являются просто указателями на внутренние структуры данных времени выполнения. Для их поиска может потребоваться несколько сравнений строк, но как только они у вас появятся, реальный вызов для получения поля или вызова метода произойдет очень быстро.

Если производительность важна, полезно один раз просмотреть значения и кэшировать результаты в собственном коде. Поскольку на каждый процесс существует ограничение в одну JavaVM, разумно хранить эти данные в статической локальной структуре.

Ссылки на классы, идентификаторы полей и идентификаторы методов гарантированно действительны до тех пор, пока класс не будет выгружен. Классы выгружаются только в том случае, если все классы, связанные с ClassLoader, могут быть удалены сборщиком мусора, что случается редко, но вполне возможно в Android. Однако обратите внимание, что jclass является ссылкой на класс и должен быть защищен вызовом NewGlobalRef (см. следующий раздел).

Если вы хотите кэшировать идентификаторы при загрузке класса и автоматически повторно кэшировать их, если класс когда-либо выгружается и перезагружается, правильный способ инициализировать идентификаторы — добавить фрагмент кода, который выглядит следующим образом, в соответствующий сорт:

Котлин

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()
    }
}

Ява

    /*
     * 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();
    }

Создайте в коде C/C++ метод nativeClassInit , который выполняет поиск по идентификатору. Код будет выполнен один раз при инициализации класса. Если класс когда-либо выгружается, а затем перезагружается, он будет выполнен снова.

Локальные и глобальные ссылки

Каждый аргумент, передаваемый собственному методу, и почти каждый объект, возвращаемый функцией JNI, являются «локальной ссылкой». Это означает, что он действителен в течение всего времени действия текущего собственного метода в текущем потоке. Даже если сам объект продолжает существовать после возврата из собственного метода, ссылка недействительна.

Это относится ко всем подклассам jobject , включая jclass , jstring и jarray . (Среда выполнения предупредит вас о большинстве случаев неправильного использования ссылок, если включены расширенные проверки JNI.)

Единственный способ получить нелокальные ссылки — использовать функции NewGlobalRef и NewWeakGlobalRef .

Если вы хотите сохранить ссылку в течение более длительного периода, вы должны использовать «глобальную» ссылку. Функция NewGlobalRef принимает локальную ссылку в качестве аргумента и возвращает глобальную. Глобальная ссылка гарантированно будет действительна до тех пор, пока вы не вызовете DeleteGlobalRef .

Этот шаблон обычно используется при кэшировании jclass, возвращаемого из FindClass , например:

jclass localClass = env->FindClass("MyClass");
jclass globalClass = reinterpret_cast<jclass>(env->NewGlobalRef(localClass));

Все методы JNI принимают в качестве аргументов как локальные, так и глобальные ссылки. Ссылки на один и тот же объект могут иметь разные значения. Например, возвращаемые значения последовательных вызовов NewGlobalRef для одного и того же объекта могут различаться. Чтобы узнать, относятся ли две ссылки к одному и тому же объекту, вы должны использовать функцию IsSameObject . Никогда не сравнивайте ссылки с == в машинном коде.

Одним из следствий этого является то, что вы не должны предполагать, что ссылки на объекты являются постоянными или уникальными в машинном коде. Значение, представляющее объект, может отличаться от одного вызова метода к другому, и возможно, что два разных объекта могут иметь одно и то же значение при последовательных вызовах. Не используйте значения jobject в качестве ключей.

Программисты обязаны «не выделять чрезмерно» локальные ссылки. На практике это означает, что если вы создаете большое количество локальных ссылок, возможно, при работе с массивом объектов, вам следует освободить их вручную с помощью DeleteLocalRef , а не позволять JNI делать это за вас. Реализация требуется только для резервирования слотов для 16 локальных ссылок, поэтому, если вам нужно больше, вам следует либо удалить их по ходу, либо использовать EnsureLocalCapacity / PushLocalFrame чтобы зарезервировать больше.

Обратите внимание, что jfieldID и jmethodID являются непрозрачными типами, а не ссылками на объекты, и их не следует передавать в NewGlobalRef . Указатели на необработанные данные, возвращаемые такими функциями, как GetStringUTFChars и GetByteArrayElements также не являются объектами. (Они могут передаваться между потоками и действительны до соответствующего вызова Release.)

Отдельного упоминания заслуживает один необычный случай. Если вы прикрепляете собственный поток с помощью AttachCurrentThread , выполняемый вами код никогда не будет автоматически освобождать локальные ссылки, пока поток не будет отсоединен. Любые созданные вами локальные ссылки придется удалять вручную. В общем, любой собственный код, который создает локальные ссылки в цикле, вероятно, требует удаления вручную.

Будьте осторожны, используя глобальные ссылки. Глобальные ссылки могут быть неизбежны, но их трудно отлаживать, и они могут вызывать трудно диагностируемые (неправильные) поведения памяти. При прочих равных условиях решение с меньшим количеством глобальных ссылок, вероятно, будет лучше.

Строки UTF-8 и UTF-16.

Язык программирования Java использует UTF-16. Для удобства JNI предоставляет методы, которые также работают с модифицированной UTF-8 . Измененная кодировка полезна для кода C, поскольку она кодирует \u0000 как 0xc0 0x80 вместо 0x00. Приятно то, что вы можете рассчитывать на наличие строк в стиле C с нулевым завершением, подходящих для использования со стандартными строковыми функциями libc. Обратной стороной является то, что вы не можете передавать произвольные данные UTF-8 в JNI и ожидать, что они будут работать правильно.

Чтобы получить представление String в UTF-16, используйте GetStringChars . Обратите внимание, что строки UTF-16 не заканчиваются нулем и разрешен \u0000, поэтому вам нужно учитывать длину строки, а также указатель jchar.

Не забудьте Release Get строки . Строковые функции возвращают jchar* или jbyte* , которые представляют собой указатели в стиле C на примитивные данные, а не локальные ссылки. Они гарантированно действительны до тех пор, пока не будет вызван Release , что означает, что они не будут выпущены после возврата нативного метода.

Данные, передаваемые в NewStringUTF, должны быть в модифицированном формате UTF-8 . Распространенной ошибкой является чтение символьных данных из файла или сетевого потока и передача их в NewStringUTF без фильтрации. Если вы не уверены, что данные действительны в формате MUTF-8 (или 7-битном ASCII, который является совместимым подмножеством), вам необходимо удалить недопустимые символы или преобразовать их в правильную форму модифицированного UTF-8. Если вы этого не сделаете, преобразование UTF-16, скорее всего, даст неожиданные результаты. CheckJNI, включенная по умолчанию для эмуляторов, сканирует строки и прерывает работу виртуальной машины, если она получает недопустимые входные данные.

До Android 8 обычно работать со строками UTF-16 было быстрее, поскольку Android не требовал копирования в GetStringChars , тогда как GetStringUTFChars требовал выделения и преобразования в UTF-8. Android 8 изменил представление String , чтобы использовать 8 бит на символ для строк ASCII (для экономии памяти), и начал использовать движущийся сборщик мусора . Эти функции значительно сокращают количество случаев, когда ART может предоставить указатель на данные String без создания копии, даже для GetStringCritical . Однако если большинство строк, обрабатываемых кодом, короткие, в большинстве случаев можно избежать выделения и освобождения, используя буфер, выделенный стеком, и GetStringRegion или GetStringUTFRegion . Например:

    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);

Примитивные массивы

JNI предоставляет функции для доступа к содержимому объектов массива. Хотя к массивам объектов необходимо обращаться по одной записи за раз, массивы примитивов можно читать и записывать напрямую, как если бы они были объявлены в C.

Чтобы сделать интерфейс максимально эффективным, не ограничивая реализацию виртуальной машины, семейство вызовов Get<PrimitiveType>ArrayElements позволяет среде выполнения либо возвращать указатель на фактические элементы, либо выделять некоторую память и делать копию. В любом случае возвращаемый необработанный указатель гарантированно будет действительным до тех пор, пока не будет выдан соответствующий вызов Release (что означает, что, если данные не были скопированы, объект массива будет закреплен и не может быть перемещен в рамках сжатия массива). куча). Вы должны Release каждый Get массив. Кроме того, если вызов Get завершится неудачно, вы должны убедиться, что ваш код не попытается позже Release NULL-указатель.

Вы можете определить, были ли скопированы данные, передав указатель, отличный от NULL, для аргумента isCopy . Это редко бывает полезно.

Вызов Release принимает аргумент mode , который может иметь одно из трех значений. Действия, выполняемые средой выполнения, зависят от того, вернула ли она указатель на фактические данные или их копию:

  • 0
    • Фактически: объект массива не закреплен.
    • Копировать: данные копируются обратно. Буфер с копией освобождается.
  • JNI_COMMIT
    • Актуально: ничего не делает.
    • Копировать: данные копируются обратно. Буфер с копией не освобождается .
  • JNI_ABORT
    • Фактически: объект массива не закреплен. Более ранние записи не прерываются.
    • Копировать: освобождается буфер с копией; любые изменения в нем теряются.

Одной из причин проверки флага isCopy является необходимость знать, нужно ли вам вызывать Release с помощью JNI_COMMIT после внесения изменений в массив. Если вы чередуете внесение изменений и выполнение кода, использующего содержимое массива, вы можете пропустить безоперационный коммит. Другая возможная причина проверки флага — эффективная обработка JNI_ABORT . Например, вы можете захотеть получить массив, изменить его на месте, передать фрагменты другим функциям, а затем отменить изменения. Если вы знаете, что JNI создает для вас новую копию, нет необходимости создавать еще одну «редактируемую» копию. Если JNI передает вам оригинал, вам необходимо сделать собственную копию.

Распространенной ошибкой (повторяемой в примере кода) является предположение, что вы можете пропустить вызов Release , если *isCopy имеет значение false. Это не так. Если буфер копирования не был выделен, исходная память должна быть закреплена и не может быть перемещена сборщиком мусора.

Также обратите внимание, что флаг JNI_COMMIT не освобождает массив, и в конечном итоге вам придется снова вызвать Release с другим флагом.

Звонки в регионы

Существует альтернатива таким вызовам, как Get<Type>ArrayElements и GetStringChars , которая может оказаться очень полезной, когда все, что вам нужно, — это скопировать данные внутрь или наружу. Учтите следующее:

    jbyte* data = env->GetByteArrayElements(array, NULL);
    if (data != NULL) {
        memcpy(buffer, data, len);
        env->ReleaseByteArrayElements(array, data, JNI_ABORT);
    }

Это захватывает массив, копирует из него первые len байтовые элементы, а затем освобождает массив. В зависимости от реализации вызов Get либо закрепит, либо скопирует содержимое массива. Код копирует данные (возможно, второй раз), затем вызывает Release ; в этом случае JNI_ABORT гарантирует отсутствие третьей копии.

То же самое можно сделать и проще:

    env->GetByteArrayRegion(array, 0, len, buffer);

Это имеет несколько преимуществ:

  • Требуется один вызов JNI вместо двух, что снижает накладные расходы.
  • Не требует закрепления или дополнительных копий данных.
  • Снижает риск ошибки программиста — нет риска забыть вызвать Release в случае сбоя.

Аналогичным образом вы можете использовать вызов Set<Type>ArrayRegion для копирования данных в массив и GetStringRegion или GetStringUTFRegion для копирования символов из String .

Исключения

Вы не должны вызывать большинство функций JNI, пока ожидается исключение. Ожидается, что ваш код заметит исключение (через возвращаемое значение функции, ExceptionCheck или ExceptionOccurred ) и вернет или очистит исключение и обработает его.

Единственные функции JNI, которые вам разрешено вызывать во время ожидания исключения:

  • DeleteGlobalRef
  • DeleteLocalRef
  • DeleteWeakGlobalRef
  • ExceptionCheck
  • ExceptionClear
  • ExceptionDescribe
  • ExceptionOccurred
  • MonitorExit
  • PopLocalFrame
  • PushLocalFrame
  • Release<PrimitiveType>ArrayElements
  • ReleasePrimitiveArrayCritical
  • ReleaseStringChars
  • ReleaseStringCritical
  • ReleaseStringUTFChars

Многие вызовы JNI могут генерировать исключение, но часто предоставляют более простой способ проверки на наличие сбоя. Например, если NewString возвращает значение, отличное от NULL, вам не нужно проверять наличие исключения. Однако если вы вызываете метод (используя такую ​​функцию, как CallObjectMethod ), вы всегда должны проверять наличие исключения, поскольку возвращаемое значение не будет действительным, если было создано исключение.

Обратите внимание, что исключения, создаваемые управляемым кодом, не уничтожают собственные кадры стека. (А исключения C++, которые обычно не рекомендуются в Android, не должны выбрасываться через границу перехода JNI от кода C++ к управляемому коду.) Инструкции JNI Throw и ThrowNew просто устанавливают указатель исключения в текущем потоке. При возврате к управлению из машинного кода исключение будет отмечено и обработано соответствующим образом.

Собственный код может «перехватить» исключение, вызвав ExceptionCheck или ExceptionOccurred , и очистить его с помощью ExceptionClear . Как обычно, отбрасывание исключений без их обработки может привести к проблемам.

Встроенных функций для управления самим объектом Throwable нет, поэтому, если вы хотите (скажем) получить строку исключения, вам нужно будет найти класс Throwable , найдите идентификатор метода для getMessage "()Ljava/lang/String;" , вызовите его, и если результат не равен NULL, используйте GetStringUTFChars чтобы получить что-то, что можно передать printf(3) или его эквиваленту.

Расширенная проверка

JNI очень мало проверяет ошибки. Ошибки обычно приводят к сбою. Android также предлагает режим CheckJNI, в котором указатели таблиц функций JavaVM и JNIEnv переключаются на таблицы функций, которые выполняют расширенную серию проверок перед вызовом стандартной реализации.

Дополнительные проверки включают в себя:

  • Массивы: попытка выделить массив отрицательного размера.
  • Плохие указатели: передача неправильного jarray/jclass/jobject/jstring в вызов JNI или передача указателя NULL в вызов JNI с аргументом, не допускающим значение NULL.
  • Имена классов: передача в вызов JNI чего-либо, кроме стиля «java/lang/String».
  • Критические вызовы: выполнение вызова JNI между «критическим» получением и соответствующим выпуском.
  • Direct ByteBuffers: передача неверных аргументов в NewDirectByteBuffer .
  • Исключения: выполнение вызова JNI во время ожидания исключения.
  • JNIEnv*s: использование JNIEnv* из неправильного потока.
  • jfieldIDs: использование NULL jfieldID или использование jfieldID для установки поля в значение неправильного типа (скажем, попытка присвоить StringBuilder полю String) или использование jfieldID для статического поля для установки поля экземпляра или наоборот, или использовать jfieldID из одного класса с экземплярами другого класса.
  • jmethodIDs: использование неправильного типа jmethodID при выполнении Call*Method : неправильный тип возвращаемого значения, статическое/нестатическое несоответствие, неправильный тип для «this» (для нестатических вызовов) или неправильный класс (для статических вызовов).
  • Ссылки: использование DeleteGlobalRef / DeleteLocalRef для ссылки неправильного типа.
  • Режимы выпуска: передача неправильного режима выпуска в вызов выпуска (отличный от 0 , JNI_ABORT или JNI_COMMIT ).
  • Безопасность типов: возврат несовместимого типа из вашего собственного метода (скажем, возврат StringBuilder из метода, объявленного как возвращающий String).
  • UTF-8: передача недопустимой последовательности модифицированных байтов UTF-8 в вызов JNI.

(Доступность методов и полей по-прежнему не проверяется: ограничения доступа не распространяются на машинный код.)

Существует несколько способов включить CheckJNI.

Если вы используете эмулятор, CheckJNI включен по умолчанию.

Если у вас есть рутированное устройство, вы можете использовать следующую последовательность команд для перезапуска среды выполнения с включенной функцией CheckJNI:

adb shell stop
adb shell setprop dalvik.vm.checkjni true
adb shell start

В любом из этих случаев вы увидите что-то подобное в выводе logcat при запуске среды выполнения:

D AndroidRuntime: CheckJNI is ON

Если у вас обычное устройство, вы можете использовать следующую команду:

adb shell setprop debug.checkjni 1

Это не повлияет на уже запущенные приложения, но в любом приложении, запущенном с этого момента, будет включен CheckJNI. (Измените свойство на любое другое значение или просто перезагрузите, чтобы снова отключить CheckJNI.) В этом случае вы увидите что-то подобное в выводе logcat при следующем запуске приложения:

D Late-enabling CheckJNI

Вы также можете установить атрибут android:debuggable в манифесте вашего приложения, чтобы включить CheckJNI только для вашего приложения. Обратите внимание, что инструменты сборки Android сделают это автоматически для определенных типов сборок.

Родные библиотеки

Вы можете загрузить собственный код из общих библиотек с помощью стандартного System.loadLibrary .

На практике в более старых версиях Android были ошибки в PackageManager, из-за которых установка и обновление собственных библиотек были ненадежными. Проект ReLinker предлагает обходные пути для этой и других проблем с загрузкой встроенных библиотек.

Вызовите System.loadLibrary (или ReLinker.loadLibrary ) из инициализатора статического класса. Аргументом является «недекорированное» имя библиотеки, поэтому для загрузки libfubar.so необходимо передать "fubar" .

Если у вас есть только один класс с собственными методами, имеет смысл, чтобы вызов System.loadLibrary находился в статическом инициализаторе этого класса. В противном случае вы можете выполнить вызов из Application , чтобы знать, что библиотека всегда загружается и всегда загружается раньше.

Существует два способа, с помощью которых среда выполнения может найти ваши собственные методы. Вы можете либо явно зарегистрировать их с помощью RegisterNatives , либо позволить среде выполнения динамически искать их с помощью dlsym . Преимущества RegisterNatives заключаются в том, что вы заранее проверяете существование символов, а также можете иметь меньшие и более быстрые общие библиотеки, не экспортируя ничего, кроме JNI_OnLoad . Преимущество возможности обнаружения ваших функций средой выполнения состоит в том, что писать немного меньше кода.

Чтобы использовать RegisterNatives :

  • Предоставьте функцию JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) .
  • В вашем JNI_OnLoad зарегистрируйте все свои собственные методы с помощью RegisterNatives .
  • Создайте с -fvisibility=hidden , чтобы из вашей библиотеки экспортировался только ваш JNI_OnLoad . Это приводит к более быстрому и меньшему размеру кода и позволяет избежать потенциальных конфликтов с другими библиотеками, загруженными в ваше приложение (но создает менее полезные трассировки стека, если ваше приложение аварийно завершает работу в машинном коде).

Статический инициализатор должен выглядеть так:

Котлин

companion object {
    init {
        System.loadLibrary("fubar")
    }
}

Ява

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

Функция JNI_OnLoad должна выглядеть примерно так, если она написана на 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;
}

Чтобы вместо этого использовать «обнаружение» собственных методов, вам необходимо назвать их определенным образом (подробности см. в спецификации JNI ). Это означает, что если сигнатура метода неверна, вы не узнаете об этом до первого фактического вызова метода.

Любые вызовы FindClass , сделанные из JNI_OnLoad будут разрешать классы в контексте загрузчика классов, который использовался для загрузки общей библиотеки. При вызове из других контекстов FindClass использует загрузчик классов, связанный с методом в верхней части стека Java, или, если его нет (поскольку вызов происходит из только что присоединенного собственного потока), он использует «систему» загрузчик классов. Загрузчик системных классов не знает о классах вашего приложения, поэтому вы не сможете искать свои собственные классы с помощью FindClass в этом контексте. Это делает JNI_OnLoad удобным местом для поиска и кэширования классов: если у вас есть действительная глобальная ссылка jclass вы можете использовать ее из любого подключенного потока.

Более быстрые собственные вызовы с помощью @FastNative и @CriticalNative

Собственные методы могут быть аннотированы с помощью @FastNative или @CriticalNative (но не обоих), чтобы ускорить переходы между управляемым и собственным кодом. Однако эти аннотации содержат определенные изменения в поведении, которые необходимо тщательно учитывать перед использованием. Ниже мы кратко упомянем об этих изменениях, но за подробностями обратитесь к документации.

Аннотация @CriticalNative может применяться только к собственным методам, которые не используют управляемые объекты (в параметрах или возвращаемых значениях или в качестве неявного this ), и эта аннотация изменяет ABI перехода JNI. Собственная реализация должна исключить параметры JNIEnv и jclass из сигнатуры функции.

При выполнении метода @FastNative или @CriticalNative сборщик мусора не может приостановить поток для выполнения важной работы и может заблокироваться. Не используйте эти аннотации для долго выполняющихся методов, включая обычно быстрые, но, как правило, неограниченные методы. В частности, код не должен выполнять значительные операции ввода-вывода или устанавливать собственные блокировки, которые могут удерживаться в течение длительного времени.

Эти аннотации были реализованы для использования в системе начиная с Android 8 и стали общедоступными API, протестированными CTS, в Android 14. Эти оптимизации, вероятно, будут работать также на устройствах Android 8–13 (хотя и без строгих гарантий CTS), но динамический поиск собственных методов поддерживается только на Android 12+, явная регистрация с помощью JNI RegisterNatives строго необходима для работы на Android версий 8–11. Эти аннотации игнорируются в Android 7. Несоответствие ABI для @CriticalNative может привести к неправильной сортировке аргументов и, скорее всего, к сбоям.

Для критически важных для производительности методов, которым необходимы эти аннотации, настоятельно рекомендуется явно зарегистрировать методы с помощью JNI RegisterNatives вместо того, чтобы полагаться на «обнаружение» собственных методов на основе имен. Чтобы добиться оптимальной производительности запуска приложения, рекомендуется включить в базовый профиль вызывающие методы @FastNative или @CriticalNative . Начиная с Android 12, вызов собственного метода @CriticalNative из скомпилированного управляемого метода почти так же дешев, как невстроенный вызов в C/C++, если все аргументы помещаются в регистры (например, до 8 целочисленных и до 8 аргументы с плавающей запятой в Arm64).

Иногда может быть предпочтительнее разделить собственный метод на два: очень быстрый метод, который может дать сбой, и другой, который обрабатывает медленные случаи. Например:

Котлин

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)

Ява

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);

64-битные соображения

Для поддержки архитектур, использующих 64-битные указатели, используйте long поле, а не int при сохранении указателя на собственную структуру в поле Java.

Неподдерживаемые функции/обратная совместимость

Поддерживаются все функции JNI 1.6, за следующим исключением:

  • DefineClass не реализован. Android не использует байт-коды Java или файлы классов, поэтому передача данных двоичного класса не работает.

Для обратной совместимости со старыми версиями Android вам может потребоваться знать следующее:

  • Динамический поиск встроенных функций

    До версии Android 2.0 (Eclair) символ «$» не преобразовывался должным образом в «_00024» во время поиска имен методов. Чтобы обойти эту проблему, необходимо использовать явную регистрацию или переместить собственные методы из внутренних классов.

  • Отсоединение ниток

    До Android 2.0 (Eclair) было невозможно использовать функцию деструктора pthread_key_create чтобы избежать проверки «поток должен быть отсоединен перед выходом». (Среда выполнения также использует функцию деструктора ключа pthread, поэтому будет соревноваться, какой из них будет вызван первым.)

  • Слабые глобальные ссылки

    До версии Android 2.2 (Froyo) слабые глобальные ссылки не были реализованы. Старые версии будут категорически отвергать попытки их использования. Вы можете использовать константы версии платформы Android для проверки поддержки.

    До Android 4.0 (Ice Cream Sandwich) слабые глобальные ссылки можно было передавать только в NewLocalRef , NewGlobalRef и DeleteWeakGlobalRef . (Спецификация настоятельно рекомендует программистам создавать жесткие ссылки на слабые глобальные переменные, прежде чем что-либо с ними делать, так что это не должно быть никаким ограничением.)

    Начиная с Android 4.0 (Ice Cream Sandwich), слабые глобальные ссылки можно использовать так же, как и любые другие ссылки JNI.

  • Местные ссылки

    До версии Android 4.0 (Ice Cream Sandwich) локальные ссылки фактически были прямыми указателями. Ice Cream Sandwich добавил косвенность, необходимую для поддержки лучших сборщиков мусора, но это означает, что многие ошибки JNI не обнаруживаются в старых версиях. Дополнительные сведения см. в разделе «Изменения локальных ссылок JNI в ICS» .

    В версиях Android до Android 8.0 количество локальных ссылок ограничено пределом, зависящим от версии. Начиная с Android 8.0, Android поддерживает неограниченное количество локальных ссылок.

  • Определение типа ссылки с помощью GetObjectRefType

    До Android 4.0 (Ice Cream Sandwich) из-за использования прямых указателей (см. выше) было невозможно правильно реализовать GetObjectRefType . Вместо этого мы использовали эвристику, которая просматривала таблицу слабых глобальных переменных, аргументы, таблицу локальных переменных и таблицу глобальных переменных в указанном порядке. Когда он впервые обнаружит ваш прямой указатель, он сообщит, что ваша ссылка относится к тому типу, который он проверял. Это означало, например, что если вы вызвали GetObjectRefType для глобального jclass, который оказался таким же, как jclass, переданный в качестве неявного аргумента вашему статическому собственному методу, вы получите JNILocalRefType , а не JNIGlobalRefType .

  • @FastNative и @CriticalNative

    До Android 7 эти аннотации по оптимизации игнорировались. Несоответствие ABI для @CriticalNative может привести к неправильной сортировке аргументов и, скорее всего, к сбоям.

    Динамический поиск собственных функций для методов @FastNative и @CriticalNative не был реализован в Android 8–10 и содержит известные ошибки в Android 11. Использование этих оптимизаций без явной регистрации с помощью JNI RegisterNatives может привести к сбоям в Android 8–11.

  • FindClass выдает ClassNotFoundException

    В целях обратной совместимости Android выдает ClassNotFoundException вместо NoClassDefFoundError , когда FindClass не находит класс. Такое поведение соответствует API-интерфейсу отражения Java Class.forName(name) .

Часто задаваемые вопросы: Почему я получаю UnsatisfiedLinkError ?

При работе над нативным кодом нередко можно увидеть такой сбой:

java.lang.UnsatisfiedLinkError: Library foo not found

В некоторых случаях это означает то, что написано — библиотека не найдена. В других случаях библиотека существует, но ее не удается открыть с помощью dlopen(3) , а подробности об ошибке можно найти в подробном сообщении об исключении.

Общие причины, по которым вы можете столкнуться с исключениями «библиотека не найдена»:

  • Библиотека не существует или недоступна для приложения. Используйте adb shell ls -l <path> чтобы проверить его наличие и разрешения.
  • Библиотека не была создана с использованием NDK. Это может привести к зависимостям от функций или библиотек, которых нет на устройстве.

Другой класс ошибок UnsatisfiedLinkError выглядит так:

java.lang.UnsatisfiedLinkError: myfunc
        at Foo.myfunc(Native Method)
        at Foo.main(Foo.java:10)

В logcat вы увидите:

W/dalvikvm(  880): No implementation found for native LFoo;.myfunc ()V

Это означает, что среда выполнения попыталась найти соответствующий метод, но безуспешно. Некоторые распространенные причины этого:

  • Библиотека не загружается. Проверьте вывод logcat на наличие сообщений о загрузке библиотеки.
  • Метод не найден из-за несоответствия имени или подписи. Обычно это вызвано:
    • Для отложенного поиска метода невозможно объявить функции C++ с extern "C" и соответствующей видимостью ( JNIEXPORT ). Обратите внимание, что до Ice Cream Sandwich макрос JNIEXPORT был неправильным, поэтому использование нового GCC со старым jni.h не будет работать. Вы можете использовать arm-eabi-nm чтобы увидеть символы в том виде, в котором они появляются в библиотеке; Если они выглядят изуродованными (что -то вроде _Z15Java_Foo_myfuncP7_JNIEnvP7_jclass , а не Java_Foo_myfunc ), или если тип символа является строчным «t», а не в верхнем плане, то вам нужно настроить декларацию.
    • Для явной регистрации, незначительные ошибки при вводе подписи метода. Убедитесь, что то, что вы передаете на регистрационный вызов, соответствует подписи в файле журнала. Помните, что «B» - это byte , а «Z» - это boolean . Компоненты имени класса в подписях начинаются с «l», конец с ';', использовать '/' для отделения имен пакета/класса и использовать '$' для разделения имен внутреннего класса ( Ljava/util/Map$Entry; ).

Использование javah для автоматического генерации заголовков JNI может помочь избежать некоторых проблем.

FAQ: Почему FindClass не нашла мой класс?

(Большая часть этого совета одинаково хорошо применяется к ошибкам, чтобы найти методы с GetMethodID или GetStaticMethodID , или поля с GetFieldID или GetStaticFieldID .)

Убедитесь, что строка имени класса имеет правильный формат. Имена классов JNI начинаются с имени пакета и разделены с помощью чертов, таких как java/lang/String . Если вы ищете класс массива, вам нужно начать с соответствующего количества квадратных кронштейнов, а также должно обернуть класс с помощью «l» и ';';, чтобы одномерный массив String был бы [Ljava/lang/String; . Если вы ищете внутренний класс, используйте «$», а не '. ". В общем, использование javap в файле .class - это хороший способ узнать внутреннее имя вашего класса.

Если вы включите сокращение кода, убедитесь, что вы настраиваете, какой код сохранить . Настройка правильных правил сохранения важна, потому что усадка кода в противном случае может удалить классы, методы или поля, которые используются только из JNI.

Если имя класса выглядит правильно, вы можете столкнуться с проблемой загрузчика класса. FindClass хочет начать поиск класса в загрузке класса, связанный с вашим кодом. Он проверяет стек вызовов, который будет выглядеть как -то вроде:

    Foo.myfunc(Native Method)
    Foo.main(Foo.java:10)

Самый верхний метод - Foo.myfunc . FindClass находит объект ClassLoader связанный с классом Foo , и использует его.

Обычно это делает то, что вы хотите. Вы можете попасть в неприятности, если вы создадите поток самостоятельно (возможно, позвонив pthread_create , а затем подключив его к AttachCurrentThread ). Теперь из вашего приложения нет кадров стека. Если вы позвоните FindClass из этого потока, Javavm начнется в загрузке класса «Систем» вместо того, что связано с вашим приложением, поэтому попытки найти, специфичные для приложения классы потерпят неудачу.

Есть несколько способов обойти это:

  • Сделайте поиск FindClass один раз, в JNI_OnLoad , и кэшируйте ссылки на класс для последующего использования. Любые вызовы FindClass , выполненные как часть выполнения JNI_OnLoad будут использовать загрузчик класса, связанный с функцией, которая называется System.loadLibrary (это специальное правило, предоставленное для того, чтобы сделать инициализацию библиотеки более удобной). Если код вашего приложения загружает библиотеку, FindClass будет использовать правильный загрузчик класса.
  • Передайте экземпляр класса в необходимые функции, объявив свой родной метод, чтобы взять аргумент класса, а затем передав Foo.class .
  • Кэш Ссылка на объект ClassLoader где -нибудь под рукой, и выпустите вызовы loadClass напрямую. Это требует некоторых усилий.

FAQ: Как мне поделиться необработанными данными с нативным кодом?

Вы можете оказаться в ситуации, когда вам нужно получить доступ к большому буферу необработанных данных как из управляемого, так и из нативного кода. Общие примеры включают манипулирование растровыми изображениями или образцами звука. Есть два основных подхода.

Вы можете сохранить данные в byte[] . Это обеспечивает очень быстрый доступ из управляемого кода. Однако с родной стороны вы не гарантируете доступ к данным без необходимости их копирования. В некоторых реализациях GetByteArrayElements и GetPrimitiveArrayCritical вернет фактические указатели на необработанные данные в управляемой куче, но в других он выделяет буфер на собственной куче и скопирует данные.

Альтернатива состоит в том, чтобы хранить данные в прямом байтовом буфере. Они могут быть созданы с помощью java.nio.ByteBuffer.allocateDirect или функции jni NewDirectByteBuffer . В отличие от обычных байтовых буферов, хранилище не выделяется на управляемую кучу и всегда можно получить непосредственно из нативного кода (получить адрес с GetDirectBufferAddress ). В зависимости от того, как реализуется прямой доступ к байтовым буферам, доступ к данным из управляемого кода может быть очень медленным.

Выбор, который использует, зависит от двух факторов:

  1. Будет ли большинство доступа к данным произойти из кода, написанного в Java или в C/C ++?
  2. Если данные в конечном итоге передаются в системный API, в какой форме он должен быть? (Например, если данные в конечном итоге передаются в функцию, которая принимает байт [], выполнение обработки в прямом ByteBuffer может быть неразумно.)

Если нет явного победителя, используйте прямой байтовый буфер. Поддержка для них встроена непосредственно в JNI, и производительность должна улучшить в будущих выпусках.