Введение в Android NDK. Установка с помощью Android Studio

02.04.2019

Android NDK является инструментом для Android SDK, который позволяет разработчикам приложений Android построить производительность критически важных частей своих приложений в машинном коде. Он предназначен для использования только в сочетании с Android SDK, так что если вы еще не установили последние Android SDK, пожалуйста, сделайте это до загрузки НДК. Кроме того, следует почитать, что такое , чтобы получить понимание того, что НДК предложение и будет ли это полезно для вас.

Выберите скачать пакет, который подходит для вашего компьютера.

Platform Package Size MD5 Checksum
Windows android-ndk-r4b-windows.zip 45792835 bytes
Mac OS X (intel) android-ndk-r4b-darwin-x86.zip 50586041 bytes
Linux 32/64-bit (x86) android-ndk-r4b-linux-x86.zip 49464776 bytes

Изменения
В нижеследующих разделах предоставлена информация и заметки о последовательных релизах НДК, по обозначению номера ревизии.
Android NDK, Revision 4b Android NDK, Версия 4, б (июнь 2010):
Включает в себя исправления для нескольких вопросов в НДК создания и отладки скриптов - если вы используете NDK R4, мы рекомендуем скачать R4b NDK . Для получения подробной информации изменений в этом выпуске, читать CHANGES.txt документа, включенного в скачанный NDK пакет.
Общие указания:
Предоставляет упрощенную систему сборки с помощью новой НДК-строителя команд.
Добавлена поддержка для легкой отладки сгенерированного кода машины по производству устройств через новую команду НДК-GDB.
Добавление новых Android-конкретных ABI для ARM основе процессорных архитектур, armeabi-v7a. Новые ABI расширяют существующие armeabi ABI включенные в этот набор инструкции процессора расширений:
Thumb-2 инструкции аппаратных VFP FPU инструкции (VFPv3-D16)
Дополнительная поддержка ARM расширений SIMD (неон) GCC и встроенные VFPv3-D32. Поддержка устройств, таких как Verizon Droid от Motorola, Google Nexus-первых, и другие.
Добавление новых cpufeatures статической библиотеки (с источниками), что позволяет вашему приложению обнаружить процессор хост-устройства во время выполнения. В частности, приложения могут проверить на ARMv7-поддержку, а также VFPv3-D32 и NEON поддержки, а затем предоставить отдельные пути коду по мере необходимости.
Добавляет пример приложения, привет-неон, который иллюстрирует, как использовать cpufeatures библиотеку, чтобы проверить процессор, а затем обеспечить оптимизированный код, используя NEON instrinsics, если поддерживается процессором.
Позволяет генерировать машинный код для одной или обеих наборов инструкций поддерживаемых НДК. Например, вы можете построить для ARMv5 и ARMv7-архитектуры в то же время всё сохранится.

АПК приложения.
Для того чтобы приложения были доступны для пользователей, только если их устройства способны запускать их, Android Market имеет фильтры приложений, основанных на информации, набор инструкций, включенных в приложения - с вашей стороны необходимы действия для того, чтобы сделать фильтрацию. Кроме того, во время установки система Android проверяет также и себя,приложения и позволяет продолжить установку только тогда, когда приложение предоставляет библиотеку, которая составляется для архитектуры процессора устройства.
Добавлена поддержка для Android 2.2, в том числе новых стабильных API для доступа к пикселям буферов растровых объектов из машинного кода.
Android NDK, Revision 3 Android NDK, Версия 3 (март 2010)
Общие указания:
Добавляет OpenGL ES 2.0 встроенную поддержку библиотек.
Добавляет пример приложения, привет-gl2, который иллюстрирует использование OpenGL ES 2.0 и верхние шейдеры фрагментов.
набор инструментов исполняемых файлов были обновлены для этой версии с GCC 4.4.0, которая должна генерировать чуть более компактный и эффективный машинный код, чем предыдущий (4.2.1). НДК также по-прежнему обеспечивает 4.2.1 двоичные файлы, которые можно дополнительно использовать для создания машинного кода.
Android NDK, Revision 2 Android NDK, Пересмотр 2 (сентябрь 2009)
Первоначально выпущенный как «Android 1,6 NDK, выпуск 1».
Общие указания:
Добавляет OpenGL ES 1.1 встроенная поддержка библиотек.
Добавляет пример приложения, Сан -Анджелесе, что делает 3D-графику через родной OpenGL ES API, в то время как управление жизненным циклом деятельности с объектом GLSurfaceView.
Android NDK, Revision 1 Android NDK, Пересмотр 1 (июнь 2009)
Первоначально выпущенный как «Android 1,5 NDK, выпуск 1».
Общие указания:
Включает в себя компилятор поддержки (ССЗ) для ARMv5TE инструкции, в том числе Thumb- инструкции.
Включает в себя системы заголовков для стабильной родной API, документацию и примеры приложений.

Android NDK (Native Development Kit) является очень популярным инструментарием, используемым для разработки приложений для мобильных устройств. Многие приложения из магазина приложений Android Market используют компоненты, разработанные с использованием языков программирования, отличных от Java, для достижения максимальной производительности. Исходя из этого NDK является инструментарием, помогающим разработчикам создавать компоненты для своих приложений с использованием компилируемых языков программирования для различных целей, начиная с достижения оптимальной производительности и заканчивая упрощением используемого кода.

Для чего и как используется бинарный код

Мы все знаем о том, что процесс разработки приложений для Android тесно связан с использованием языка программирования Java, а также о том, что использование данного языка программирования значительно упрощает жизнь разработчиков, ведь они могут использовать элегантную объектно-ориентированную модель Java. Приложения или алгоритмы, реализованные на языке Java, преобразуются в специальный байткод, который выполняется аналогичным образом на всех поддерживаемых платформах. При этом виртуальная машина Java или JVM (Java Virtual Machine) ответственная за JIT-компиляцию и исполнение байткода Java, доступна практически для всех существующих платформ, начиная с мэйнфреймами и заканчивая мобильными телефонами.

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

Что касается разработки приложений для Android, описанный выше фактор является незначительным недостатком. Но программирование на языке Java может оказаться достаточно сложным ввиду постоянного усложнения кода и затруднения его понимания. Более того, использование мультиплатформенного байткода и виртуальной машины обуславливает значительные затраты вычислительных ресурсов устройства.

Другим важным фактором, который требует внимания, является мультиплатформенный код. Если нам нужно создать программу для множества аппаратных платформ, мы можем переписать большую часть кода, относящегося к контроллеру и отображению, для каждой из платформ, что является не самым разумным решением. Но весь код, относящийся к контроллеру, должен быть обязательно портирован на языки C и C++, так, как практически все мобильные платформы поддерживают их; таким образом, если нам удастся реализовать логику в рамках библиотек на языках C и C++ и впоследствии использовать ее на нескольких платформах, нам удастся максимально сократить потери производительности приложения. В подобных случаях мы будем использовать код на языках C и C++ вместе с привычным кодом на языке Java или "мультиплатформенным кодом".

При использовании компилируемого языка программирования исходный код компилируется непосредственно в машинный код для центрального процессора, а не в промежуточное представление, такое, как в языке Java. Таким образом разработчики приложений могут создавать приложения с оптимальной производительностью для различных Android-устройств. Фрагменты компилируемого кода могут быть структурированы в рамках одной разделяемой библиотеки, функции из которой могут вызываться из кода на языке Java. Отдельная разделяемая библиотека должна создаваться для каждой из поддерживаемых архитектур центральных процессоров. Большая часть ее исходного кода при этом может оставаться неизменной. Скомпилированные разделяемые библиотеки должны быть добавлены в файл.apk вашего приложения. С учетом всего вышесказанного, фундаментальная модель приложений Android не изменится.

Использование Android NDK

Android NDK является инструментарием, позволяющим реализовывать части приложения для Android на таких компилируемых языках программирования, как C и C++ и содержит библиотеки для управления активностями и доступа к физическим компонентам устройства, таким, как различные сенсоры и дисплей.

Android NDK интегрирован с инструментами из набора компонентов для разработки программного обеспечения (Android SDK), а также с интегрированной средой разработки Android Studio или устаревшей средой разработки Eclipse ADT. Однако, NDK не может использоваться отдельно.

Принцип работы Android NDK

Сердцем пакета Android NDK является сценарий ndk-build , который отвечает за автоматический обход файлов проекта Android (разработка каждого нового приложения для Android с помощью интегрированной среды разработки, такой, как Android Studio или Eclipse начинается с создания файлов нового проекта) и собирает информацию о том, какие компоненты нужно компилировать. Данный сценарий также ответственен за генерацию бинарных файлов и копирование этих бинарных файлов в директорию файлов проекта приложения.

Мы можем использовать ключевое слово native для того, чтобы компилятор знал, что данный фрагмент реализован в рамках компилируемого кода. Например:

Public native int numbers(int x, int y);

Также в процессе сборки проекта создаются разделяемые библиотеки (Native Shared Libraries, с расширением.so) и статические библиотеки (Native Shared Libraries, с расширением.a), которые могут связываться с другими библиотеками. Бинарный интерфейс приложения (Application Binary Interface, ABI) использует разделяемые библиотеки с расширением.so для исполнения машинного кода в системе в процессе работы приложения.

Весь компилируемый код исполняется посредством интерфейса под названием Java Native Interface (JNI), который позволяет связать друг с другом компоненты на языках Java и C/C++.

Для сборки проекта с помощью сценария ndk-build нам придется создать два файла: Android.mk и Application.mk . Оба этих файла должны размещаться в директории JNI . Файл Android.mk описывает модуль и его имя, флаги сборки, используемые библиотеки файлы исходного кода, которые должны компилироваться, а файл Application.mk - бинарные модули, необходимые для работы приложения.

Установка и использование Android NDK в Ubuntu

Android NDK поставляется в формате самораспаковывающегося архива. По этой причине нам придется лишь установить бит исполнения и распаковать его:

$ chmod +x android-ndk-r10c-linux-x86_64.bin $ ./android-ndk-r10c-linux-x86_64.bin

В результате компоненты NDK будут сохранены в текущей рабочей директории.

Распаковка в ручном режиме

Ввиду того, что файл с расширением.bin является ничем иным, как самораспаковывающимся архивом формата 7-Zip, мы можем извлечь его содержимое вручную с помощью следующей команды:

$ 7za x -o/путь/к/целевой/директории/ android-ndk-r10c-linux-x86_64.bin

Пакет с компонентами архиватора 7-Zip доступен из официального репозитория Ubuntu и может быть установлен, к примеру, с помощью команды apt-get:

$ sudo apt-get install p7zip-full

Установка с помощью Android Studio

Мы можем установить Android NDK с помощью компонента SDK Manager непосредственно из Android Studio.

Для этого после открытия проекта следует осуществить переход в рамках главного меню окна Tools > Android > SDK Manager . После этого нужно установить флажки напротив названий компонентов LLDB , CMake и NDK . Далее нужно просто применить изменения с помощью соответствующей кнопки.

Создание или импорт проекта с бинарными компонентами

После настройки Android Studio мы можем создать новый проект с поддержкой языков программирования C/C++. Однако, если нам понадобится добавить или импортировать существующий код на этих языках в проект Android Studio, мы будем вынуждены выполнить описанные ниже действия.

Для начала следует создать новые файлы исходного кода с использованием упомянутых языков программирования и добавить их в проект, открытый в Android Studio. Мы можем пропустить этот шаг, если в проекте уже имеются подобные файлы или нам нужно импортировать в него предварительно скомпилированную библиотеку.

Сценарий сборки CMake позволяет сообщить одноименной системе сборки о том, как нужно осуществлять компиляцию файлов исходного кода и сборку результирующей бинарной библиотеки. Этот файл также необходим для импорта и связывания с нашей библиотекой существующих или поставляемых в комплекте NDK библиотек. Мы также можем без каких-либо последствий пропустить данный шаг в том случае, если в комплекте поставки нашей существующей бинарной библиотеки уже содержится файл сценария сборки CMakeLists.txt или она использует компонент ndk-build и в ее комплекте поставки содержится файл сценария сборки Android.mk .

Далее следует сообщить Gradle о существовании нашей бинарной библиотеки путем указания пути к файлу сценария сборки CMake или ndk-build . Gradle использует указанный сценарий сборки для импорта исходного кода в проект Android Studio и упаковки результирующей бинарной библиотеки (файла с расширением.so) в файл пакета формата APK.

Важное замечание: если в рамках проекта используется устаревший инструмент ndkCompile , нам придется открыть файл build.poperties и удалить из него следующую строку кода перед настройкой Gradle с целью использования CMake или ndk-build:

Android.useDeprecatedNdk = true

Теперь мы можем собрать и выполнить наше приложение путем нажатия на кнопку Run . Gradle будет рассматривать процесс CMake или ndk-build в качестве зависимости, которая должна быть собрана, осуществлять сборку бинарной библиотеки и упаковывать ее в файл пакета формата APK.

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

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

Процесс создания простого приложения на основе Android NDK не связан с какими-либо сложностями. Однако, каждому разработчику следует уяснить один важный момент: набор программных компонентов Android NDK разрабатывался для использования в определенных случаях и не должен применяться при разработке любых приложений.

Android NDK может как помочь в процессе разработки приложения, так и максимально осложнить его. Не является тайной и то, что использование бинарного кода на платформе Android в некоторых случаях не приводит к заметному повышению производительности приложения (хотя в большинстве случаев его производительность все же повышается), но при этом оно в любом случае усложняет его код. Обычно повышение производительности приложений достигается благодаря задействованию кода со специфичными для используемого центрального процессора инструкциями. Но в общем случае рекомендуется использовать NDK лишь тогда, когда производительность приложения является критически важным параметром, а не тогда, когда разработчику удобнее писать код на языках C/C++.

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

Для разработки приложений под ОС Android, Google предоставляет два пакета разработки: SDK и NDK. Про SDK существует много статей, книжек, а так же хорошие guidelines от Google. Но про NDK даже сам Google мало что пишет. А из стоящих книг я бы выделил только одну, Cinar O. - Pro Android C++ with the NDK – 2012 .

Эта статья ориентирована на тех, кто ещё не знаком (или мало знаком) с Android NDK и хотел бы укрепить свои знания. Внимание я уделю JNI, так как мне кажется начинать нужно именно с этого интерфейса. Так же, в конце рассмотрим небольшой пример с двумя функциями записи и чтения файла.

Что такое Android NDK?

Android NDK (native development kit) – это набор инструментов, которые позволяют реализовать часть вашего приложения используя такие языки как С/С++.

Для чего используют NDK?

Google рекомендует прибегать к использованию NDK только в редчайших случаях. Зачастую это такие случаи:
  • Нужно увеличить производительность (например, сортировка большого объема данных);
  • Использовать стороннюю библиотеку. Например, много уже чего написано на С/С++ языках и нужно просто заиспользовать существующий материал. Пример таких библиотек, как, Ffmpeg, OpenCV;
  • Программирование на низком уровне (например, всё что выходит за рамки Dalvik);

Что такое JNI?

Java Native Interface – стандартный механизм для запуска кода, под управлением виртуальной машины Java, который написан на языках С/С++ или Assembler, и скомпонован в виде динамических библиотек, позволяет не использовать статическое связывание. Это даёт возможность вызова функции С/С++ из программы на Java, и наоборот.

Преимущества JNI

Основное преимущество перед аналогами (Netscape Java Runtime Interface или Microsoft’s Raw Native Interface and COM/Java Interface) является то что JNI изначально разрабатывался для обеспечения двоичной совместимости, для совместимости приложений, написанных на JNI, для любых виртуальных машин Java на конкретной платформе (когда я говорю о JNI, то я не привязываюсь к Dalvik машине, потому как JNI был написан Oracle для JVM который подходит для всех Java виртуальных машин). Поэтому скомпилированный код на С/С++ будет выполнятся в не зависимости от платформы. Более ранние версии не позволяли реализовывать двоичную совместимость.

Двоичная совместимость или же бинарная совместимость – вид совместимости программ, позволяющий программе работать в различных средах без изменения её исполняемых файлов.

Как устроен JNI

JNI таблица, организована как таблица виртуальных функций в С++. VM может работать с несколькими такими таблицами. Например, одна будет для отладки, вторая для использования. Указатель на JNI интерфейс действителен только в текущем потоке. Это значит, что указатель не может гулять с одного потока в другой. Но нативные методы могут быть вызваны из разных потоков. Пример:

Jdouble Java_pkg_Cls_f__ILjava_lang_String_2 (JNIEnv *env, jobject obj, jint i, jstring s) { const char *str = (*env)->GetStringUTFChars(env, s, 0); (*env)->ReleaseStringUTFChars(env, s, str); return 10; }

  • *env – указатель на интерфейс;
  • оbj – ссылка на объект в котором описан нативный метод;
  • i and s – передаваемые аргументы;
Примитивные типы копируются между VM и нативным кодом, а объекты передаются по ссылке. VM обязана отслеживать все ссылки которые передаются в нативный код. Все переданные ссылки в нативный код не могут быть освобождены GC. Но нативный код в свою очередь должен информировать VM о том что ему больше не нужны ссылки на переданные объекты.

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

JNI делит ссылки на три типа: локальные, глобальные и слабые глобальные ссылки. Локальные действительны пока не завершиться метод. Все Java объекты которые возвращает функции JNI являются локальными. Программист должен надеется на то что VM сама подчистит все локальные ссылки. Локальные ссылки доступны лишь в том потоке в котором были созданы. Однако если есть необходимость то их можно освобождать сразу методом JNI интерфейса DeleteLocalRef:

Jclass clazz; clazz = (*env)->FindClass(env, "java/lang/String"); //ваш код (*env)->DeleteLocalRef(env, clazz);
Глобальные ссылки остаются пока они явно не будут освобождены. Что бы зарегистрировать глобальную ссылку следует вызвать метод NewGlobalRef. Если же глобальная ссылка уже не нужна, то её можно удалить методом DeleteGlobalRef:

Jclass localClazz; jclass globalClazz; localClazz = (*env)->FindClass(env, "java/lang/String"); globalClazz = (*env)->NewGlobalRef(env, localClazz); //ваш код (*env)->DeleteLocalRef(env, localClazz);

Обработка ошибок

JNI не проверяет ошибки такие как NullPointerException, IllegalArgumentException. Причины:
  • снижение производительности;
  • в большинстве функций C библиотек очень и очень трудно защитится от ошибок.
JNI позволяет использовать Java Exception. Большинство JNI функций возвращают код ошибок а не сам Exception, и поэтому приходится обрабатывать сам код, а в Java уже выбрасывать Exception. В JNI следует проверять код ошибки вызываемых функций и после них следует вызвать ExceptionOccurred(), которая в свою очередь возвращает объект ошибки:

Jthrowable ExceptionOccurred(JNIEnv *env);
Например, некоторые функции JNI доступа к массивам не возвращают ошибки, но могут вызвать исключения ArrayIndexOutOfBoundsException или ArrayStoreException.

Примитивные типы JNI

В JNI существуют свои примитивные и ссылочные типы данных.
Java Type Native Type Description
boolean jboolean unsigned 8 bits
byte jbyte signed 8 bits
char jchar unsigned 16 bits
short jshort signed 16 bits
int jint signed 32 bits
long jlong signed 64 bits
float jfloat 32 bits
double jdouble 64 bits
void void N/A

Ссылочные типы JNI

Модифицированный UTF-8

JNI использует модифицированную кодировку UTF-8 для представления строк. Java в свою очередь использует UTF-16. UTF-8 в основном используется в С, потому что он кодирует \u0000 в 0xc0, вместо привычной 0x00. Изменённые строки кодируются так, что последовательность символов, которые содержат только ненулевой ASCII символы могут быть представлены с использованием только одного байта.

Функции JNI

Интерфейс JNI содержит в себе не только собственный набор данных, но и свои собственные функции. На их рассмотрение уйдёт много времени, так как их не один десяток. Ознакомится с ними вы сможете в официальной документации .

Пример использования функций JNI

Небольшой пример, что бы вы усвоили пройденный материал:
#include //... JavaVM *jvm; JNIEnv *env; JavaVMInitArgs vm_args; JavaVMOption* options = new JavaVMOption; options.optionString = "-Djava.class.path=/usr/lib/java"; vm_args.version = JNI_VERSION_1_6; vm_args.nOptions = 1; vm_args.options = options; vm_args.ignoreUnrecognized = false; JNI_CreateJavaVM(&jvm, &env, &vm_args); delete options; jclass cls = env->FindClass("Main"); jmethodID mid = env->GetStaticMethodID(cls, "test", "(I)V"); env->CallStaticVoidMethod(cls, mid, 100); jvm->DestroyJavaVM();
Разберём построчно:
  • JavaVM – предоставляет интерфейс для вызова функций, которые позволяют создавать и уничтожать JavaVM;
  • JNIEnv – обеспечивает большинство функций JNI;
  • JavaVMInitArgs – аргументы для JavaVM;
  • JavaVMOption – опции для JavaVM;
Метод JNI_CreateJavaVM() инициализирует JavaVM и возвращает на неё указатель. Метод JNI_DestroyJavaVM() выгружает созданную JavaVM.

Потоки

Всеми потоками в Linux управляет ядро, но они могут быть прикреплены к JavaVM функциями AttachCurrentThread и AttachCurrentThreadAsDaemon. Пока поток не присоединён он не имеет доступа к JNIEnv. Важно, Android не приостанавливает потоки которые были созданы JNI, даже если срабатывает GC . Но перед тем как поток завершиться он должен вызвать метод DetachCurrentThread что бы отсоединиться от JavaVM.

Первые шаги

Структура проекта у вас должна выглядеть следующим образом:

Как мы видим из рисунка 3, весь нативный код находится в папке jni. После сборки проекта, в папке libs создастся четыре папки под каждую архитектуру процессора, в которой будет лежать ваша нативная библиотека (количество папок зависит от количество выбранных архитектур).

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

  • В корне проекта нужно создать папку jni, в которую поместить исходники нативного кода;
  • Создать файл Android.mk, который будет собирать проект;
  • Создать файл Application.mk, в котором описываются детали сборки. Он не является обязательным условием, но позволяет гибко настроить сборку;
  • Создать файл ndk-build, который будет запускать процесс сборки (тоже не является обязательным).

Android.mk

Как упоминалось уже выше, это make файл для сборки нативного проекта. Android.mk позволяет группировать ваш код в модули. Модули могут быть как статические библиотеки (static library, только они будут скопированные в ваш проект, в папку libs), разделяемые библиотеки (shared library), автономный исполняемый файл (standalone executable).

Пример минимальной конфигурации:
LOCAL_PATH:= $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE:= NDKBegining LOCAL_SRC_FILES:= ndkBegining.c include $(BUILD_SHARED_LIBRARY)
Рассмотрим детально:

  • LOCAL_PATH:= $(call my-dir) – функция call my-dir возвращает путь папки в которой вызывается файл;
  • include $(CLEAR_VARS) – очищает переменные которые использовались до этого кроме LOCAL_PATH. Это необходимо так как все переменные являются глобальными, потому что сборка происходит в контексте одного GNU Make;
  • LOCAL_MODULE – имя выходного модуля. В нашем примере имя выходной библиотеки установлено как NDKBegining, но после сборки в папке libs создадутся библиотеки с именами libNDKBegining. Android добавляет к названию префикс lib, но в java коде при подключении вы должны указывать название библиотеки без префикса (то есть названия должны совпадать с установленными в make файлах);
  • LOCAL_SRC_FILES – перечисление исходных файлов из которых следует создать сборку;
  • include $(BUILD_SHARED_LIBRARY) – указывает тип выходного модуля.
В Android.mk можно определить свои переменные, но они не должны иметь такой синтаксис: LOCAL_, PRIVATE_, NDK_, APP_, my-dir. Google, рекомендует называть свои переменные, как MY_. Например:
MY_SOURCE:= NDKBegining.c Что бы обратится к переменной: $(MY_SOURCE) Переменные, так же можно конкатенировать, например: LOCAL_SRC_FILES += $(MY_SOURCE)

Application.mk

В этом make файле описывается несколько переменных, которые помогут сделать сборку более гибкой:
  • APP_OPTIM – дополнительная переменная которая устанавливается в значения release или debug. Используется для оптимизации при сборке модулей. Отлаживать можно как release так и debug, но debug предоставляет больше информации для отладки;
  • APP_BUILD_SCRIPT – указывает на альтернативный путь к Android.mk;
  • APP_ABI – наверное одна из самых важных переменных. Она указывает для какой архитектуры процессоров собирать модули. По умолчанию стоит armeabi которая соответствует ARMv5TE архитектуры. Например для поддержки ARMv7 следует использовать armeabi-v7a, для IA-32 – x86, для MIPS – mips, или если вам нужно поддерживать все архитектуры то значение должно быть таким: APP_ABI:= armeabi armeabi-v7a x86 mips. Если вы использует ndk версии 7 и выше, то можно не перечислять все архитектуры, а установить так APP_ABI:= all.
  • APP_PLATFORM – таргет платформы;
  • APP_STL – Android использует runtime библиотеку libstdc++.so которая является урезанной и разработчику доступен не весь функционал С++. Однако, переменная APP_STL позволяет включить в сборку поддержку расширений;
  • NDK_TOOLCHAIN_VERSION – позволяет выбрать версию компилятора gcc (по умолчанию 4.6);

NDK-BUILDS

Ndk-build из себя представляет обёртку GNU Make. После 4-й версии ввели флаги для ndk-build:
  • clean – очищает все сгенеренные бинарные файлы;
  • NDK_DEBUG=1 – генерирует отладочный код;
  • NDK_LOG=1 – показывает лог сообщений (используется для отладки);
  • NDK_HOST_32BIT=1 – Android имеет средства для поддержки 64-х битных версий утилит (например NDK_PATH\toolchains\mipsel-linux-android-4.8\prebuilt\windows-x86_64 и т.д.);
  • NDK_APPLICATION_MK - указывается путь к Application.mk.
В 5-й версии NDK был введён такой флаг как NDK_DEBUG. Если он установлен в 1 то создаётся отладочная версия. Если флаг не установлен то ndk-build по умолчанию проверяет стоит ли атрибут android:debuggable=«true» в AndroidManifest.xml. Если вы используете ndk выше 8-й версии, то Google не рекомендует использовать атрибут android:debuggable в AndroidManifest.xml (потому что если вы используете «ant debug» или строите отладочную версию с помощью ADT плагина то они автоматически добавляют флаг NDK_DEBUG=1).

По умолчанию устанавливается поддержка 64-х разрядной версии утилит, но вы можете принудительно собрать только для 32-х установив флаг NDK_HOST_32BIT=1. Google, рекомендует всё же использовать 64-х разрядность утилит для повышения производительности больших программ.

Как собрать проект?

Раньше это было мучением. Нужно было установить CDT плагин, скачать компилятор cygwin или mingw. Скачать Android NDK. Подключить это всё в настройках Eclipse. И как на зло это всё оказывалось не рабочим. Я первый раз когда столкнулся с Android NDK, то настраивал это всё 3 дня (а проблема оказалось в том что в cygwin нужно было дать разрешение 777 на папку проекта).

Сейчас с этим всё намного проще. Идёте по этой ссылке . Качаете Eclipse ADT Bundle в котором уже есть всё то что необходимо для сборки.

Вызов нативных методов из Java кода

Для того что бы использовать нативный код из Java вам сперва следует определить нативные методы в Java классе. Например:
native String nativeGetStringFromFile(String path) throws IOException; native void nativeWriteByteArrayToFile(String path, byte b) throws IOException;
Перед методом следует поставить зарезервированное слово «native». Таким образом компилятор знает, что это точка входа в JNI. Эти методы нам нужно реализовать в С/С++ файле. Так же Google рекомендует начинать именовать методы со слова nativeХ, где Х – реальное название метода. Но перед тем как реализовывать эти методы вручную, следует сгенерировать header файл. Это можно сделать вручную, но можно использовать утилиту javah, которая находится в jdk. Но пойдём дальше и не будет использовать её через консоль, а будем это делать при помощи стандартных средств Eclipse.

Теперь можете запускать. В директории bin/classes будут лежать ваши header файлы.

Далее копируем эти файлы в jni директорию нашего нативного проекта. Вызываем контекстное меню проекта и выбираем пункт Android Tools – Add Native Library. Это позволит нам использовать jni.h функции. Дальше вы уже можете создавать cpp файл (иногда Eclipse его создаёт по умолчанию) и писать тела методов, которые уже описаны в header файле.

Пример кода я не стал добавлять в статью, чтобы не растягивать её. Пример вы можете посмотреть/скачать с github .

Теги: Добавить метки

Для разработки приложений под ОС Android, Google предоставляет два пакета разработки: SDK и NDK. Про SDK существует много статей, книжек, а так же хорошие guidelines от Google. Но про NDK даже сам Google мало что пишет. А из стоящих книг я бы выделил только одну, Cinar O. - Pro Android C++ with the NDK – 2012 .

Эта статья ориентирована на тех, кто ещё не знаком (или мало знаком) с Android NDK и хотел бы укрепить свои знания. Внимание я уделю JNI, так как мне кажется начинать нужно именно с этого интерфейса. Так же, в конце рассмотрим небольшой пример с двумя функциями записи и чтения файла.

Что такое Android NDK?

Android NDK (native development kit) – это набор инструментов, которые позволяют реализовать часть вашего приложения используя такие языки как С/С++.

Для чего используют NDK?

Google рекомендует прибегать к использованию NDK только в редчайших случаях. Зачастую это такие случаи:
  • Нужно увеличить производительность (например, сортировка большого объема данных);
  • Использовать стороннюю библиотеку. Например, много уже чего написано на С/С++ языках и нужно просто заиспользовать существующий материал. Пример таких библиотек, как, Ffmpeg, OpenCV;
  • Программирование на низком уровне (например, всё что выходит за рамки Dalvik);

Что такое JNI?

Java Native Interface – стандартный механизм для запуска кода, под управлением виртуальной машины Java, который написан на языках С/С++ или Assembler, и скомпонован в виде динамических библиотек, позволяет не использовать статическое связывание. Это даёт возможность вызова функции С/С++ из программы на Java, и наоборот.

Преимущества JNI

Основное преимущество перед аналогами (Netscape Java Runtime Interface или Microsoft’s Raw Native Interface and COM/Java Interface) является то что JNI изначально разрабатывался для обеспечения двоичной совместимости, для совместимости приложений, написанных на JNI, для любых виртуальных машин Java на конкретной платформе (когда я говорю о JNI, то я не привязываюсь к Dalvik машине, потому как JNI был написан Oracle для JVM который подходит для всех Java виртуальных машин). Поэтому скомпилированный код на С/С++ будет выполнятся в не зависимости от платформы. Более ранние версии не позволяли реализовывать двоичную совместимость.

Двоичная совместимость или же бинарная совместимость – вид совместимости программ, позволяющий программе работать в различных средах без изменения её исполняемых файлов.

Как устроен JNI

JNI таблица, организована как таблица виртуальных функций в С++. VM может работать с несколькими такими таблицами. Например, одна будет для отладки, вторая для использования. Указатель на JNI интерфейс действителен только в текущем потоке. Это значит, что указатель не может гулять с одного потока в другой. Но нативные методы могут быть вызваны из разных потоков. Пример:

Jdouble Java_pkg_Cls_f__ILjava_lang_String_2 (JNIEnv *env, jobject obj, jint i, jstring s) { const char *str = (*env)->GetStringUTFChars(env, s, 0); (*env)->ReleaseStringUTFChars(env, s, str); return 10; }

  • *env – указатель на интерфейс;
  • оbj – ссылка на объект в котором описан нативный метод;
  • i and s – передаваемые аргументы;
Примитивные типы копируются между VM и нативным кодом, а объекты передаются по ссылке. VM обязана отслеживать все ссылки которые передаются в нативный код. Все переданные ссылки в нативный код не могут быть освобождены GC. Но нативный код в свою очередь должен информировать VM о том что ему больше не нужны ссылки на переданные объекты.

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

JNI делит ссылки на три типа: локальные, глобальные и слабые глобальные ссылки. Локальные действительны пока не завершиться метод. Все Java объекты которые возвращает функции JNI являются локальными. Программист должен надеется на то что VM сама подчистит все локальные ссылки. Локальные ссылки доступны лишь в том потоке в котором были созданы. Однако если есть необходимость то их можно освобождать сразу методом JNI интерфейса DeleteLocalRef:

Jclass clazz; clazz = (*env)->FindClass(env, "java/lang/String"); //ваш код (*env)->DeleteLocalRef(env, clazz);
Глобальные ссылки остаются пока они явно не будут освобождены. Что бы зарегистрировать глобальную ссылку следует вызвать метод NewGlobalRef. Если же глобальная ссылка уже не нужна, то её можно удалить методом DeleteGlobalRef:

Jclass localClazz; jclass globalClazz; localClazz = (*env)->FindClass(env, "java/lang/String"); globalClazz = (*env)->NewGlobalRef(env, localClazz); //ваш код (*env)->DeleteLocalRef(env, localClazz);

Обработка ошибок

JNI не проверяет ошибки такие как NullPointerException, IllegalArgumentException. Причины:
  • снижение производительности;
  • в большинстве функций C библиотек очень и очень трудно защитится от ошибок.
JNI позволяет использовать Java Exception. Большинство JNI функций возвращают код ошибок а не сам Exception, и поэтому приходится обрабатывать сам код, а в Java уже выбрасывать Exception. В JNI следует проверять код ошибки вызываемых функций и после них следует вызвать ExceptionOccurred(), которая в свою очередь возвращает объект ошибки:

Jthrowable ExceptionOccurred(JNIEnv *env);
Например, некоторые функции JNI доступа к массивам не возвращают ошибки, но могут вызвать исключения ArrayIndexOutOfBoundsException или ArrayStoreException.

Примитивные типы JNI

В JNI существуют свои примитивные и ссылочные типы данных.
Java Type Native Type Description
boolean jboolean unsigned 8 bits
byte jbyte signed 8 bits
char jchar unsigned 16 bits
short jshort signed 16 bits
int jint signed 32 bits
long jlong signed 64 bits
float jfloat 32 bits
double jdouble 64 bits
void void N/A

Ссылочные типы JNI

Модифицированный UTF-8

JNI использует модифицированную кодировку UTF-8 для представления строк. Java в свою очередь использует UTF-16. UTF-8 в основном используется в С, потому что он кодирует \u0000 в 0xc0, вместо привычной 0x00. Изменённые строки кодируются так, что последовательность символов, которые содержат только ненулевой ASCII символы могут быть представлены с использованием только одного байта.

Функции JNI

Интерфейс JNI содержит в себе не только собственный набор данных, но и свои собственные функции. На их рассмотрение уйдёт много времени, так как их не один десяток. Ознакомится с ними вы сможете в официальной документации .

Пример использования функций JNI

Небольшой пример, что бы вы усвоили пройденный материал:
#include //... JavaVM *jvm; JNIEnv *env; JavaVMInitArgs vm_args; JavaVMOption* options = new JavaVMOption; options.optionString = "-Djava.class.path=/usr/lib/java"; vm_args.version = JNI_VERSION_1_6; vm_args.nOptions = 1; vm_args.options = options; vm_args.ignoreUnrecognized = false; JNI_CreateJavaVM(&jvm, &env, &vm_args); delete options; jclass cls = env->FindClass("Main"); jmethodID mid = env->GetStaticMethodID(cls, "test", "(I)V"); env->CallStaticVoidMethod(cls, mid, 100); jvm->DestroyJavaVM();
Разберём построчно:
  • JavaVM – предоставляет интерфейс для вызова функций, которые позволяют создавать и уничтожать JavaVM;
  • JNIEnv – обеспечивает большинство функций JNI;
  • JavaVMInitArgs – аргументы для JavaVM;
  • JavaVMOption – опции для JavaVM;
Метод JNI_CreateJavaVM() инициализирует JavaVM и возвращает на неё указатель. Метод JNI_DestroyJavaVM() выгружает созданную JavaVM.

Потоки

Всеми потоками в Linux управляет ядро, но они могут быть прикреплены к JavaVM функциями AttachCurrentThread и AttachCurrentThreadAsDaemon. Пока поток не присоединён он не имеет доступа к JNIEnv. Важно, Android не приостанавливает потоки которые были созданы JNI, даже если срабатывает GC . Но перед тем как поток завершиться он должен вызвать метод DetachCurrentThread что бы отсоединиться от JavaVM.

Первые шаги

Структура проекта у вас должна выглядеть следующим образом:

Как мы видим из рисунка 3, весь нативный код находится в папке jni. После сборки проекта, в папке libs создастся четыре папки под каждую архитектуру процессора, в которой будет лежать ваша нативная библиотека (количество папок зависит от количество выбранных архитектур).

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

  • В корне проекта нужно создать папку jni, в которую поместить исходники нативного кода;
  • Создать файл Android.mk, который будет собирать проект;
  • Создать файл Application.mk, в котором описываются детали сборки. Он не является обязательным условием, но позволяет гибко настроить сборку;
  • Создать файл ndk-build, который будет запускать процесс сборки (тоже не является обязательным).

Android.mk

Как упоминалось уже выше, это make файл для сборки нативного проекта. Android.mk позволяет группировать ваш код в модули. Модули могут быть как статические библиотеки (static library, только они будут скопированные в ваш проект, в папку libs), разделяемые библиотеки (shared library), автономный исполняемый файл (standalone executable).

Пример минимальной конфигурации:
LOCAL_PATH:= $(call my-dir) include $(CLEAR_VARS) LOCAL_MODULE:= NDKBegining LOCAL_SRC_FILES:= ndkBegining.c include $(BUILD_SHARED_LIBRARY)
Рассмотрим детально:

  • LOCAL_PATH:= $(call my-dir) – функция call my-dir возвращает путь папки в которой вызывается файл;
  • include $(CLEAR_VARS) – очищает переменные которые использовались до этого кроме LOCAL_PATH. Это необходимо так как все переменные являются глобальными, потому что сборка происходит в контексте одного GNU Make;
  • LOCAL_MODULE – имя выходного модуля. В нашем примере имя выходной библиотеки установлено как NDKBegining, но после сборки в папке libs создадутся библиотеки с именами libNDKBegining. Android добавляет к названию префикс lib, но в java коде при подключении вы должны указывать название библиотеки без префикса (то есть названия должны совпадать с установленными в make файлах);
  • LOCAL_SRC_FILES – перечисление исходных файлов из которых следует создать сборку;
  • include $(BUILD_SHARED_LIBRARY) – указывает тип выходного модуля.
В Android.mk можно определить свои переменные, но они не должны иметь такой синтаксис: LOCAL_, PRIVATE_, NDK_, APP_, my-dir. Google, рекомендует называть свои переменные, как MY_. Например:
MY_SOURCE:= NDKBegining.c Что бы обратится к переменной: $(MY_SOURCE) Переменные, так же можно конкатенировать, например: LOCAL_SRC_FILES += $(MY_SOURCE)

Application.mk

В этом make файле описывается несколько переменных, которые помогут сделать сборку более гибкой:
  • APP_OPTIM – дополнительная переменная которая устанавливается в значения release или debug. Используется для оптимизации при сборке модулей. Отлаживать можно как release так и debug, но debug предоставляет больше информации для отладки;
  • APP_BUILD_SCRIPT – указывает на альтернативный путь к Android.mk;
  • APP_ABI – наверное одна из самых важных переменных. Она указывает для какой архитектуры процессоров собирать модули. По умолчанию стоит armeabi которая соответствует ARMv5TE архитектуры. Например для поддержки ARMv7 следует использовать armeabi-v7a, для IA-32 – x86, для MIPS – mips, или если вам нужно поддерживать все архитектуры то значение должно быть таким: APP_ABI:= armeabi armeabi-v7a x86 mips. Если вы использует ndk версии 7 и выше, то можно не перечислять все архитектуры, а установить так APP_ABI:= all.
  • APP_PLATFORM – таргет платформы;
  • APP_STL – Android использует runtime библиотеку libstdc++.so которая является урезанной и разработчику доступен не весь функционал С++. Однако, переменная APP_STL позволяет включить в сборку поддержку расширений;
  • NDK_TOOLCHAIN_VERSION – позволяет выбрать версию компилятора gcc (по умолчанию 4.6);

NDK-BUILDS

Ndk-build из себя представляет обёртку GNU Make. После 4-й версии ввели флаги для ndk-build:
  • clean – очищает все сгенеренные бинарные файлы;
  • NDK_DEBUG=1 – генерирует отладочный код;
  • NDK_LOG=1 – показывает лог сообщений (используется для отладки);
  • NDK_HOST_32BIT=1 – Android имеет средства для поддержки 64-х битных версий утилит (например NDK_PATH\toolchains\mipsel-linux-android-4.8\prebuilt\windows-x86_64 и т.д.);
  • NDK_APPLICATION_MK - указывается путь к Application.mk.
В 5-й версии NDK был введён такой флаг как NDK_DEBUG. Если он установлен в 1 то создаётся отладочная версия. Если флаг не установлен то ndk-build по умолчанию проверяет стоит ли атрибут android:debuggable=«true» в AndroidManifest.xml. Если вы используете ndk выше 8-й версии, то Google не рекомендует использовать атрибут android:debuggable в AndroidManifest.xml (потому что если вы используете «ant debug» или строите отладочную версию с помощью ADT плагина то они автоматически добавляют флаг NDK_DEBUG=1).

По умолчанию устанавливается поддержка 64-х разрядной версии утилит, но вы можете принудительно собрать только для 32-х установив флаг NDK_HOST_32BIT=1. Google, рекомендует всё же использовать 64-х разрядность утилит для повышения производительности больших программ.

Как собрать проект?

Раньше это было мучением. Нужно было установить CDT плагин, скачать компилятор cygwin или mingw. Скачать Android NDK. Подключить это всё в настройках Eclipse. И как на зло это всё оказывалось не рабочим. Я первый раз когда столкнулся с Android NDK, то настраивал это всё 3 дня (а проблема оказалось в том что в cygwin нужно было дать разрешение 777 на папку проекта).

Сейчас с этим всё намного проще. Идёте по этой ссылке . Качаете Eclipse ADT Bundle в котором уже есть всё то что необходимо для сборки.

Вызов нативных методов из Java кода

Для того что бы использовать нативный код из Java вам сперва следует определить нативные методы в Java классе. Например:
native String nativeGetStringFromFile(String path) throws IOException; native void nativeWriteByteArrayToFile(String path, byte b) throws IOException;
Перед методом следует поставить зарезервированное слово «native». Таким образом компилятор знает, что это точка входа в JNI. Эти методы нам нужно реализовать в С/С++ файле. Так же Google рекомендует начинать именовать методы со слова nativeХ, где Х – реальное название метода. Но перед тем как реализовывать эти методы вручную, следует сгенерировать header файл. Это можно сделать вручную, но можно использовать утилиту javah, которая находится в jdk. Но пойдём дальше и не будет использовать её через консоль, а будем это делать при помощи стандартных средств Eclipse.

Теперь можете запускать. В директории bin/classes будут лежать ваши header файлы.

Далее копируем эти файлы в jni директорию нашего нативного проекта. Вызываем контекстное меню проекта и выбираем пункт Android Tools – Add Native Library. Это позволит нам использовать jni.h функции. Дальше вы уже можете создавать cpp файл (иногда Eclipse его создаёт по умолчанию) и писать тела методов, которые уже описаны в header файле.

Пример кода я не стал добавлять в статью, чтобы не растягивать её. Пример вы можете посмотреть/скачать с github .

Теги:

  • программирование на android
  • android ndk
  • jni
Добавить метки