0,0
рейтинг
21 ноября 2013 в 13:05

Разработка → Введение в Android 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 <jni.h>
    //...
    JavaVM *jvm;
    JNIEnv *env;
    JavaVMInitArgs vm_args;
    JavaVMOption* options = new JavaVMOption[1];
    options[0].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.
Вячеслав Титов @viacheslav_dev
карма
20,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (28)

  • +1
    Отличная статья, спасибо!
    • +1
      согласен, очень хорошо все изложено, спасибо!
  • –1
    Спасибо за статью. А продолжение будет? А то уже начитался вводными статьями для «новичков». Хотелось бы чего-нибудь интересного.
    Как настроить make файл для линковки с файлами из других проектов, про работу с потоками в jni, про работу с какими-то OpenSource либами, оптимизированными под Android и их сравнение, и т.д.

    • +2
      Последнее с чем я работал из нативного, так это ffmpeg. Возможно напишу статью как собрать и использовать эту библиотеку.
      Насчёт потоков, то самому интересно и думаю в ближайшее время будет статья.
      • 0
        Очень интересно. Я вот хочу собрать библиотеку клиентского (.so) доступа к PostgreSQL, но не знаю как подступиться. Везде все просто:
        ./configure && make
        

        А вот как это будет выглядеть для Андроида, даже не могу представить. Кстати, а куда нужно положить .so, чтобы он был доступен всем приложениям на системе Андроида?

        • 0
          Вроде как зависит от того, под какую версию API билдите.

          В папочке android-ndk-r8d\platforms находятся директории для различных версий API, туда и кидать.

          К примеру, для 14 под arm:
          либу сюда android-ndk-r8d\platforms\android-14\arch-arm\usr\lib
          заголовки сюда android-ndk-r8d\platforms\android-14\arch-arm\usr\include

          Для x86 тоже есть папочка.
          • 0
            А как такое можно провернуть официально? Вот допустим через Google Play. Типа, как под виндой набор кодеков идёт в инсталляторе.
            • 0
              Там выше ответ был про другое. «Официально» как вы говорите дать доступ к своей сошке другим приложениям не получится. Кто хочет использовать должен включать себе в приложение — так что поставляйте исходники, так проще всего.

              Ну вернее, варианты есть — идете к вендерам, и договариваетесь, что у определенного вендора в поставке идет и ваша либа.
  • 0
    И сразу совет, сразу настраиваете проект на компиляцию через clang — NDK_TOOLCHAIN_VERSION=clang3.3, а то потом если захотите под iOS разрабатывать, можно радостно ловить что какие-то фишки gcc не поддерживаются в clang-е и наоборот (хотя различия между официальным и apple clang-ом тоже есть)
    • 0
      А он не сильно плохой код для андроида делает?
      • 0
        Это смотря, что подразумевается под «плохим» и что под «кодом» :)
        И что значит «для андроида» — больно они разные по платформам.

        На мой взгляд, clang 3.3 для ряда платформ на сложном c++11-том коде, генерит более быстрый код GCC 4.8.x — но это на глазок, замеров я не делал.
        • 0
          Код значит бинарный а плохой значит медленный. На x86 пока в целом clang медленнее.
          • 0
            У меня другие наблюдения. Я сравнивал на x64 их на числодробилке — там премущество до 15% в пользу clang-а было, кода мало 2к LOC. И мог сравнивать под arm7 на примере игры (300к LOC) — там примерно 5% преимущество clang-а
    • 0
      только не на продакшене, clang тормоз по сравнению с gcc и особенно на ARM. А в gcc 4.9 куча многих вкусных плюшек.
      • 0
        Учитывая, что в osx последней gcc вообще алиас к clang-ку, то как раз для продакшен распространения своей либы в исходниках, лучше чтобы она компилялась в clang-е. Иначе многие будут иметь проблемы со сброкой. Не говоря уже о компиляции под iOS.

        Ну и говорю — у меня другие наблюдения, если сравнивать с gcc 4.8. И не только у меня, на стек оверфлоу тоже встречал замеры, где clang на процентов 15-20 лучше.
        • 0
          www.phoronix.com/scan.php?page=article&item=llvm_clang33_3way&num=1
          Правда llvm и gcc уже подросли с тех пор…

          А так недавно собирал трудный проект при помощи clang особых трудностей не было… были скорее DARWIN/BSD специфичные. Ну и честно говоря не вижу смысла менять шило на мыло если этого захотела Apple.
    • 0
      интересно а много таких контор кто в своих коммерческих приложениях весь функционал на плюсах пишет а потом просто добавляет гуй и собирает под андройд/айфон/винфон и т.д.? Такой подход оправдывает затраты или далеко не всегда?
      • +1
        Ну примерно половина всех денег в гугл плее делается приложениями с таким подходам. И это игры :) И гуи нативного там крайне мало. А так подход сложный — ибо современный гуи это как правило многопоточная фигня, и сама core logic тоже как правило многопоточная — скрешивать их универсально еще тот секс в гамаке.
        • 0
          вот точно, имел ввиду все что не игры (их то понятно на open gl разных делают).
          Т.е. обычные приложение так не получится клепать, несмотря на то что потока там всего 2: гуй и асинхронная задача (редко когда их больше одной в приложении)?
          • 0
            Можно, вопрос оправданно ли. Тут засада в том, что либо вам нужен программист универсал — который будет и c++, java и objc знать и все хорошо — что большая редкость и очень большая стоимость, либо разделять команду кто-то core team, кто-то за гуи на iOS, кто-то за гуи на android. На самом деле если исключить игры ( а там бывает и по 10+ программеров на проект) — то я не видил столь крупных команд разработчиков мобильных приложений, чтобы такое разделение было оправданно. И даже там где такое оправданно — там разделение скорее серверных/клиентских программистов — т.е. приложения сервисы.

            Более, менее близко к тому что вы описывается в разработках различных навигационных программ — tom tom и прочее. Но у них это сложилось еще до ios/android ибо они еще и до этого на куче платформ старались запускаться.
            • +1
              Отвечу за все выше написанные вопросы «зачем». На NDK зачастую пишут продуктовые компании.

              Например, dating (то с чем я работал) проекты делают ядро(api) на С/С++ и потом используют на разных платформах. Таким способом остаётся на платформах лишь работа с UI. Такой подход же сейчас пытаются сделать внедряя в проекты Xamarin, но эта штука ещё далека от идеала.

              Так, же взять ту же продуктовую компанию Samsung (приходилось сталкиваться с ней). Они пишут много нативного кода, очень много и большинство разработчиков именно необходимые такие скилы как знания Linux kernel. Ребята которые уходят с Samsung'a и приходят в обычную компанию SDK порой даже не видели и не могут написать обычный селектор.

              Вот такие мои наблюдения. Так что писать ли на NDK, зависит от случая, задач, проекта. Нужно в первую очередь делать оценку по времени и смотреть будет ли оправдан выбор писать на плюсах или нет.
  • +1
    Большое спасибо за статью, всё очень хорошо и правильно расписано.
    Два вопроса:
    1) есть ли какая-нибудь разница при использовании вместо Dalvik ADT? Подводные камни или грабли? Не пробовали?
    2) clang в NDK всё-таки сыроват, насколько он вообще сейчас используется для сборки native-библиотек?
  • 0
    Для облегчения жизни с Android NDK под Windows можно попробовать использовать плагин для vc2010-2012 — nvidia nsight.
  • –1
    Для облегчения жизни с Android NDK можно попробовать это www.crystax.net/android/ndk
    • 0
      Еще раз спасибо crystax за его доработанный NDK, в свое время он нам очень облегчил жизнь, но сейчас смысла нету, практически все эти плюшки есть в стандартном NDK начиная с 9-той ревизии.
  • 0
    в intellij idea поддержка всего этого пока отстает?
    • 0
      Да, задавал этот вопрос JetBrains, то не смогли ответить. Судя по всему у них пока и в планах нету вводить поддержку NDK.
  • НЛО прилетело и опубликовало эту надпись здесь

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