Содержание
Исользование нативного кода, написанного на C или С++ — это тема которая затрагивается большинством разработчиков в лучшем случае поверхностно. И чаще всего это оправданно, так как использование нативного кода на порядок усложняет процесс разработки приложения и случаи, когда использование Android NDK действительно оправданно можно пересчетать на пальцах одной руки опытного токаря. Из этой статье вы сможете получить действительно базовый навыки работы с NDK, включая использование STL_PORT, полезной утилиты javah и пару моих мыслей о том, когда применение NDK себя оправдывает.
NDK or not to NDK?
Статьи о NDK принято начинать с описания случаев, когда использование NDK уместно. Не буду отходить от этой традиции с смысле последовательности, но отойду от ее содежательной части.
Часто рекомендуют использовать NDK для «сложных вычислительных процессов». Признаться честно, я не уверен, что кто-то занимается вычислением числа π при помощи своего Android-девайса. Поэтому я перечислю те случаи, которые, на мой взгляд, явлются «NDKable» в порядке возрастающей важности:
- Работа с OpenGL ES
- Использование кросс-платформенных игровых движков, например Cocos2Dx
- Использование уже написанного на C/C++ кода (а его ох как дофига написано!). Часто, это работа с мультимедиа, например FFMPEG, libpng или наукоемкие вещи типа openCV
Таким образом, работа с NDK чаще всего представляем из себя процесс (часто — мучительный) сборки некой библиотеки под ARM и написиние оберток (wrappers) на нативные методы. В тоже время, сейчас есть возможность ваять приложение практически без использования Java, используя NativeActivity (API 9 и выше).
Если говорить о возможностях NDK, то они обширны. Мы можем вызывать Java-методы и обращаться к объектам в нативном коде, так же мы можем вызывать нативные методы из Java-кода. Нативный код можно дебажить, можно настроить Eclipse так, что работать с ним будет так же просто, как работать с Java-кодом. Но эти вещи тянут на отдельную статью и сегодня касаться их мы не будем.
Зато мы будем:
- Вызывать нативные меоды из Java
- Использовать в нативном коде C++ и STL_PORT
- Писать make-файлы и использовать полезную утилиту javah
С чего начинается NDK
Забегая вперед, скажу сразу: забудьте о Windows, если хотите работать с NDK. Я не явлюясь противником технологий Microsoft (как и любых технологий в принципе), но сборка проекта под виндой с использованием Cygwin — это не самое веселое занятие, тем более наукой не зафиксированно ни одного случая успешной сборки того же FFMPEG в среде Windows (если вам известен такой случай, буду рад узнать подробности ).
Так что первый шаг — это поставить какой-нибудь UNIX.
Допустим, первый шаг уже выполнен, второй — это скачать сам NDK (естественно, вы уже должны располагать стандартным набором Android-инструментов: JDK, Android SDK, ADT, Eclipse).
Следующий шаг — это добавление папки с NDK в системные пути, на Mac OS X это делаюется добавлением следующих строчек в файл .profile:
export NDKROOT="/Users/yourusername/your/path/to/ndk/android-ndk-r8"
export PATH=$PATH:$NDKROOT
Для проверки запустите (или перезапустите терминал если он был запущен) и попробуйте ввести «ndk» после чего нажать клавишу tab, если при этой вы наблюдаете примерно следующий вывод в консоли:
mymacname:~ dmitrykunin$ ndk-
ndk-build ndk-gdb ndk-stack
то вы все сделали правильно
С установкой мы завершили, теперь я рекомендую вам зайти в папочку с NDK и ознакомится с файлом documentation.html, после чего можно глянуть папку samples с поучительными примерами кода.
Дорога из Java в натив и обратно. Некоторые правила движения по этой дороге
Следующий 2 раздела опишут правила описания нативных методов как со стороны Java, так и со стороны C/C++ кода.
Описание нативных методов в Java
В учебном проекте мы определим 3 нативных метода, заниматься они будут большей часть бесполезной но показательной работой, а именно:
public class NativeUtils {
static {
System.loadLibrary("tinymath");
}
/**
* Статический нативный метод суммирования
*
* @param arg0 первое слагаемое
* @param arg1 второе слагаемое
* @return сумма
*/
public static native double nativeSum(double arg0, double arg1);
/**
* Статический экземплярный метод проверки числа на простоту
*
* @param candidate число, которое нужно проверить на простоту
* @return true если число простое, иначе false
*/
public native boolean nativeIsPrime(int candidate);
/**
* Получение информации о возможностях CPU
*
*
* @return строку, с поддерживаемы системой технологиями
*/
public native String getCpuInfo();
}
Эти методы примечательны следующим:
- Отсутсутвует реализация (тело метода)
- При объявлении метода используется модификатор
native
Жизненно важной часть является вот эта часть кода:
static {
System.loadLibrary("tinymath");
}
Этот код выполнит загрузку модуля «tinymath» — нативной библиотеки, в которой и будут реализованы методы. Название этого модуля задается в файле Android.mk
, но об этом позже.
Описание нативных методов в C/C++
Самая интересная и самая жуткая (на первый взгляд!) часть работы.
Для того чтобы связать Java-методы с нативными создадим в папке проекта папку jni
, в ней создайте следующие файлы:
Android.mk
— основные параметры для сборки приложения с нативным кодомApplication.mk
— дополнительные параметрыby_idev_jni_NativeUtils.h
— хэдеры нативных функций (методов)tinymath.cpp
— реализация нативных методов на C++
Мы не будем углубляться в суть первых двух, я приведу лишь их содержимое с небольшими комментариями, но очень рекомендую почитать документацию к NDK о которой говорил выше.
И так, файл by_idev_jni_NativeUtils.h будет выглядеть так:
/* DO NOT EDIT THIS FILE - it is machine generated */
#include
/* Header for class by_idev_jni_NativeUtils */
#ifndef _Included_by_idev_jni_NativeUtils
#define _Included_by_idev_jni_NativeUtils
#ifdef __cplusplus
extern "C" {
#endif
/*
* Class: by_idev_jni_NativeUtils
* Method: nativeSum
* Signature: (DD)D
*/
JNIEXPORT jdouble JNICALL Java_by_idev_jni_NativeUtils_nativeSum
(JNIEnv *, jclass, jdouble, jdouble);
/*
* Class: by_idev_jni_NativeUtils
* Method: nativeIsPrime
* Signature: (I)Z
*/
JNIEXPORT jboolean JNICALL Java_by_idev_jni_NativeUtils_nativeIsPrime
(JNIEnv *, jobject, jint);
/*
* Class: by_idev_jni_NativeUtils
* Method: getCpuInfo
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_by_idev_jni_NativeUtils_getCpuInfo
(JNIEnv *, jobject);
#ifdef __cplusplus
}
#endif
#endif
Некисло… особенно если писать руками. Но мы рыками писать не будем, вы ведь заметили комментарий /* DO NOT EDIT THIS FILE - it is machine generated */
? Так вот генерирует этот файл утилита javah (входит в стандартную поставку JDK), почитайте о ней вот тут, которой нужно скормить .class-файл. Вызов ее будет выглядеть вот так:
cd ПУТЬ_К_ПРОЕКТУ/bin/classes
javah -d КУДА_ЗАПИСАТЬ_СГЕНЕРИРОВАННЫЙ_ФАЙЛ ПОЛНОЕ_ИМЯ_КЛАССА
А вот конкретный пример:
cd $HOME/workspace/SimpleJNI/bin/classes
javah -d $HOME/workspace/SimpleJNI/jni by.idev.jni.NativeUtils
Получаем готовый файл и радуемся, что есть такая полезная утилита как javah.
Теперь давайте разберемся в содержимом этого файла.
#ifndef _Included_by_idev_jni_NativeUtils
#define _Included_by_idev_jni_NativeUtils
#ifdef __cplusplus
extern "C" {
#endif
//методы методы методы
#ifdef __cplusplus
}
#endif
#endif
Этот самый extern "C"
нужем потому, что компилятор C++ любит менять имена объявленных функций. После его вмешательства приложение не будет, поэтому мы запрещаем компилятору заниматься самодеятельностью с именами функций при помощи extern "C"
JNIEXPORT jdouble JNICALL Java_by_idev_jni_NativeUtils_nativeSum
(JNIEnv *, jclass, jdouble, jdouble);
..
JNIEXPORT jboolean JNICALL Java_by_idev_jni_NativeUtils_nativeIsPrime
(JNIEnv *, jobject, jint);
JNIEXPORT
— необходимый для JNI модификатор. Типы данных с префиксом «j»: jdouble, jobject, jstring etc
— это «отражения» объектов и типов Java в C/C++.
Дам подсказку, если вы откроете файл jni.h, то узнаете много интересного, в частности об этих самых типах:
/*
* Primitive types that match up with Java equivalents.
*/
#ifdef HAVE_INTTYPES_H
# include /* C99 */
typedef uint8_t jboolean; /* unsigned 8 bits */
typedef int8_t jbyte; /* signed 8 bits */
typedef uint16_t jchar; /* unsigned 16 bits */
typedef int16_t jshort; /* signed 16 bits */
typedef int32_t jint; /* signed 32 bits */
typedef int64_t jlong; /* signed 64 bits */
typedef float jfloat; /* 32-bit IEEE 754 */
typedef double jdouble; /* 64-bit IEEE 754 */
#else
typedef unsigned char jboolean; /* unsigned 8 bits */
typedef signed char jbyte; /* signed 8 bits */
typedef unsigned short jchar; /* unsigned 16 bits */
typedef short jshort; /* signed 16 bits */
typedef int jint; /* signed 32 bits */
typedef long long jlong; /* signed 64 bits */
typedef float jfloat; /* 32-bit IEEE 754 */
typedef double jdouble; /* 64-bit IEEE 754 */
#endif
Из описания следует, что работать с примитивными типами Java можно также как с примитивными типами C/C++. С объектами и массивами другая история и для нее нужна другая статья
Обратим внимание, что в каждой функции в качесте аргумента имеется JNIEnv*
— интерфейс для работы с Java, при помощи него можно вызывать Java-методы, создавать Java-объекты и делать еще много всяких полезных Java-штук. Второй обязательный параметр — jobject или jclacc — в зависимости от того, является ли метод статическим. Если метод статический, то аргумент будет типа jclass (ссылка на класс объекта, в котором объявлен метод), если не статический — jobject — ссылка на объект, у которого был вызван метод.
Перейдем к реализации и файлу tinymath.cpp:
#include
#include <android/log.h>
#include
#include
#include
#include
using namespace std;
#define LOG_TAG "SimpleJni"
#define LOGI(x...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG,x)
#ifdef __cplusplus
extern "C" {
#endif
JNIEXPORT jdouble JNICALL Java_by_idev_jni_NativeUtils_nativeSum(JNIEnv *env,
jclass clazz, jdouble arg0, jdouble arg1) {
LOGI("JNI: nativeSum called");
return arg0 + arg1;
}
JNIEXPORT jboolean JNICALL Java_by_idev_jni_NativeUtils_nativeIsPrime(
JNIEnv *env, jobject obj, jint candidate) {
LOGI("JNI: nativeIsPrime called");
if (candidate < 2)
return JNI_FALSE;
double srt = sqrt((float) candidate);
int lowerSrt = (int) floor(srt);
char *log = new char[64];
sprintf(log, "JNI: sqrt=%.4f floor=%d", srt, lowerSrt);
LOGI(log);
delete[] log;
for (int i = 2; i NewStringUTF("Not ARM");
}
cpu_features = android_getCpuFeatures();
if (cpu_features & ANDROID_CPU_ARM_FEATURE_ARMv7) {
a.append(" ARMc7 n");
LOGI("Arm7");
}
if (cpu_features & ANDROID_CPU_ARM_FEATURE_VFPv3) {
a.append(" ARM w VFPv3 support n");
LOGI("VFP3");
}
if ((cpu_features & ANDROID_CPU_ARM_FEATURE_NEON)) {
a.append(" ARM w NEON support n");
LOGI("NEON");
}
if (cpu_features & ANDROID_CPU_ARM_FEATURE_LDREX_STREX) {
a.append(" LDREX_STREX ");
LOGI("LDREX_STREX");
}
if (a == "") {
return env->NewStringUTF("Unknown");
}
return env->NewStringUTF(a.c_str());
}
}
Если вы знакомы и с C, и с С++, то наверное заметили, что код написан с испозованием С++ специфичных вещей (string — часть STL). Обычно примеры работы с NDK приводят на C-коде, может быть из соображения простоты. Но как вы увидите далее, для того чтобы использовать C++ с элементами STL (к сожалению он поддерживается не полностью) не нужно прикладывать много усилий.
Давайте посмотрим на Android.mk:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
# имя модуля, который будет вызываться в Java при помощи System.loadLibrary()
LOCAL_MODULE := tinymath
# Add all source file names to be included in lib separated by a whitespace
LOCAL_SRC_FILES := tinymath.cpp
# статические библиотеки, уже скомпиленные за нас
LOCAL_STATIC_LIBRARIES := cpufeatures
# добавим библиотеку для логирования
LOCAL_LDLIBS := -llog
LOCAL_CFLAGS := -g
include $(BUILD_SHARED_LIBRARY)
#
$(call import-module,cpufeatures)
И на Application.mk:
# без этой строчки никого STL (включая string) мы не дождемся
APP_STL := stlport_static
Надеюсь комментариев к коду будет достаточно, если у вас возникнут вопросы, вы запросто можете задать их на форуме или в комментариях к статье.
Вот почти и все, осталось собрать приложение.
Сборка нативного кода при помощи ndk-build
В начале статьи мы добавляли директорую с NDK в системные пути как раз для ускорения доступа к ndk-build.
Для сборки приложения в терменали выполните слеюдующие инструкции:
cd ПУТЬ_К_ПРОЕКТУ
ndk-build
В ответ должны получить примерно следующее:
Gdbserver : [arm-linux-androideabi-4.4.3] libs/armeabi/gdbserver
Gdbsetup : libs/armeabi/gdb.setup
Compile++ thumb : tinymath libs/armeabi/libtinymath.so
Если так и есть — то все в порядке, вас можно поздравить с первым NDK в папке проекта должны появится директории obj и libs с набором .a (статические библиотеки) и .so (динамические библиотеки) файлов.
Живой пример
Вы можете скачать исходники приложения SimpleJNI.
Запустив его вы сможете лицезреть это:
На этом все. Вернее конечно не все. Очень многое осталось за кадром (в том числе очень много вкусного), но я обязательно постараюсь освятить еще несколько темных углов JNI и NDK в будущих статьях
Полезные и очень полезные ссылки
На пути рыцаря-NDK возникнет немало трудностей, с которыми проще будет справиться, если почитать инфу, доступную по ссылкам ниже
- JNI 1.5 guide http://docs.oracle.com/javase/1.5.0/docs/guide/jni/
- JNI Design http://docs.oracle.com/javase/1.5.0/docs/guide/jni/spec/intro.html
- JNI Tips for Android http://developer.android.com/guide/practices/design/jni.html
- Референс по C и С++ http://www.cplusplus.com/reference/
Вакантное место: ведущий Android разработчик в компании Softeq Development
Что будете делать Вы?
- разрабатывать приложения для мобильных Android устройств
- работать с заказчиками мирового уровня
Что нужно от Вас?
- опыт разработки под Android от одного года
- отличное знание основ Java
- отличное знание основ Android
- хорошее знание С/С++ и опыт работы с NDK
- знание принципов ООП и способность эффективно применять их в архитектуре приложения
- знание и умение применять на практике шаблоны проектирования
- английский язык: уметь уверенно читать и писать, разговорный приветствуется
Большим плюсом будет:
- опыт кросс-платформенного программирования
Сайт: www.softeq.by, www.softeq.com
Cтраничка в Facebook: http://www.facebook.com/pages/Softeq/110374298801
Страничка в VK: http://vk.com/club21079655
Контактная информация:
Ждем Ваше резюме на jobs@softeq.by!
Если вы хотите узнать о вакансии и нашей компании больше, Вы можете связаться с менеджером по персоналу Ириной Протащик:
Skype: irina.protaschik
E-mail: irina.protaschik@softeq.com
Моб.: +375 29 175 47 00, +375 29 234 47 00
Сообщить об опечатке
Текст, который будет отправлен нашим редакторам: