Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

Вступление в Android NDK

Anna | 3.06.2014 | нет комментариев
Для разработки приложений под ОС 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, взамен привычной 0×00. Изменённые строки кодируются так, что последовательность символов, которые содержат только ненулевой 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. И как на зло это всё оказывалось не рабочим. Я 1-й раз когда столкнулся с 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.

Источник: programmingmaster.ru

Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB