Цель данной статьи — подробное описание процесса создания Android-приложения, использующего NDK в Android Studio, в частности — предложение достаточно простой и эффективной конфигурации gradle (системы сборки пакетов, используемая в Android Studio), гарантирующей включение нативных библиотек в APK-файл. Также статья включает краткую инструкцию работы с NDK в IDE Eclipse и введение в нативную разработку, достаточное для написания первого приложения.
Данная статья предназначена преимущественно для начинающих разработчиков. Описанное решение не является единственным, но оно достаточно удобно, особенно для тех, кто работал с NDK в Eclipse. Если кому-то из читателей объяснение покажется слишком подробным, то в конце статьи есть краткое резюме, описывающее лишь алгоритм требуемых действий без комментариев к каждому шагу.
Так как существует множество статей, описывающих работу с NDK, я не буду использовать в качестве примера сложные библиотеки, а ограничусь лишь самым простым примером hello-jni. Исходный код данного примера можно найти в каталоге <путь_к_ndk>/samples/hello-jni
В среде Eclipse особых проблем с использованием NDK не возникало. Каталог проекта выглядит примерно так:
Рис.1 Главный каталог проекта для Eclipse
Нас интересуют каталоги jni и libs. Каталог jni содержит исходные коды на нативных языках (*.c; *.cpp), заголовочные файлы (*.h), makefiles (*.mk). Долго останавливаться на предназначении данных файлов не буду, так как этому посвящено немало материалов и статей. Упомяну лишь, что jni означает java native interface. Именно через этот интерфейс производится вызов нативных процедур из кода на java. Поэтому не забывайте подключать библиотеку <jni.h> в ваши c/c++ файлы и помните о правильном синтаксисе функций, которые будут вызываться через jni. Например, в моём случае приложение имеет package name:
evi.ntest
поэтому описание функции выглядит так:
jstring Java_evi_ntest_MainActivity_stringFromJNI( JNIEnv* env, jobject thiz )
где jstring — название типа данных c, соответствующего типу string в java, Java — в данном случае служебный префикс, показывающий язык, из которого будет вызвана функция, evi_ntest — имя пакета, который будет вызывать функцию, MainActivity — имя активити, из которой будет вызываться функция, stringFromJNI — название функции.
В java-коде описание данной функции выглядит гораздо проще:
public native String stringFromJNI();
Не забывайте также указывать используемые файлы кода в .mk файлах. Для начала можете воспользоваться .mk файлами из примеров NDK, модифицируя названия файлов, но в дальнейшем рекомендую изучить их структуру.
Каталог libs содержит готовые бинарные библиотеки для различных архитектур процессоров (по умолчанию — armeabi). Динамическая библиотека представляет собой файл с расширением .so, статическая — файл с расширением .a. Для получения данных библиотек требуется компиляция исходных кодов с помощью Android NDK. В Unix-системах (в моём случае — Mac OSX) для этого требуется в терминале ввести следующие строчки:
cd <путь_к_проекту> <путь_к_ndk>/ndk-build
При этом NDK автоматически компилирует исходные коды из папки jni и помещает полученные библиотеки в libs/armeabi (также можно с помощью параметров командной строки задать компиляцию под x86, mips, arm v7-neon процессоры).
При использовании Windows потребуется воспользоваться дополнительными утилитами, возможно — плагинами для MS Visual Studio.
В любом случае, не имеет значения, каким именно способом получены готовые библиотеки, важен факт, что если в папке с проектом находится подкаталог libs, Eclipse при сборке автоматически включает его содержимое в APK-файл.
Так как данная статья посвящена лишь основам работы с NDK, то на этом введение в программирование на нативных языках под android я закончу. Для более подробного ознакомления с принципами нативной разработки рекомендую изучить примеры из каталога <путь_к_ndk>/samples/, а также читать статьи, в том числе и на русском языке. Пример хорошей статьи о Android NDK на русском языке, советую обратить внимание (написана не мной):
Перехожу к основному разделу статьи — настройке IDE Android Studio для работы с нативным кодом.
Среда Android Studio по умолчанию собирает APK с помощью gradle. Данный сборщик имеет широкие возможности кастомизации, но при стандартных настройках gradle не включает нативные библиотеки в APK-файл.
Рассмотрим частичную структуру проекта в Android Studio:
Рис.2 Путь к исходному коду проекта Android Studio.
При работе у меня возникло логичное желание разместить папку jni в каталоге src/main, так как именно там хранятся все остальные файлы с исходным кодом. Разумеется, читатель может размещать каталог jni там, где ему удобно. Главное — не забыть собрать бинарные библиотеки с помощью NDK (повторюсь, в UNIX системах для этого нужно в терминале перейти к каталогу, содержащему jni, затем вызвав исполняемый файл ndk-build, лежащий в папке с NDK, прописав полный путь к нему в этом же терминале, в MS Windows нужно использовать дополнительные утилиты). Проблема же заключается в том, что по умолчанию gradle не будет упаковывать в APK библиотеки.
Однако gradle несложно настроить на включение в сборку java-библиотек (файлов *.jar). Стоит заметить, что jar-файлы представляют собой zip-архивы, содержащие какие-либо ресурсы, а также объектный код. Таким образом, для включения бинарных библиотек *.so и *.a достаточно упаковать их в jar-файл.
Делается это так:
- Переименовываем папку libs, содержащую наши бинарные библиотеки в lib
- Сжимаем данную папку любым zip-архиватором
- Меняем расширение полученного файла на .jar
Полученную библиотеку можно подключить на этапе сборки проекта, при этом полученный APK-файл будет включать двоичные библиотеки, а приложение — вызывать процедуры, написанные на нативном коде.
Данный вопрос не раз обсуждался на различных англоязычных форумах, например Stack Overflow:
Однако, данная информация достаточно краткая, разрозненная и требует от читателя определённых знаний синтаксиса gradle. Цель моей статьи – предоставить читателям подробное русскоязычное объяснение, доступное даже тем, кто только начал работать с Android Studio и gradle.
Давайте рассмотрим 2 способа упаковки библиотек: ручной и автоматический.
Ручной способ упаковки:
Данный способ весьма неудобен, однако имеет место на существование. Допустимый случай применения на практике: наличие готовой библиотеки и отсутствие необходимости её изменять. В таком случае, описанные ниже операции потребуется выполнить лишь один раз.
Откройте build.gradle, находящийся по адресу "<путь_к_проекту>/<имя_проекта>Project/<имя_проекта>/", в моём случае:
Рис.3 Местонахождение конфигурируемого файла build.gradle.
Данный файл изначально выглядит приблизительно так:
buildscript { repositories { mavenCentral() } dependencies { classpath 'com.android.tools.build:gradle:0.5.+' } } apply plugin: 'android' repositories { mavenCentral() } android { compileSdkVersion 17 buildToolsVersion "17.0.0" defaultConfig { minSdkVersion 9 targetSdkVersion 9 } } dependencies { compile 'com.android.support:support-v4:18.0.0' }
В самом низу есть раздел dependencies. Туда следует добавить такую строчку:
compile fileTree(dir: ‘src/main/’, include: ‘*.jar’)
Рассмотрим эту команду: gradle в ходе компиляции будет вынужден включить дерево файлов (структуру файлов и папок, соответствующих заданной маске ), расположенное по адресу ‘src/main/’ (т.е. в том каталоге, где расположены исходные коды, а также созданный нами jar файл), при этом в качестве маски использован параметр ‘*.jar’, т.е. включатся будут все файлы с таким расширением. Обратите внимание, что в данном случае путь считается относительно месторасположения файла build.gradle.
В результате выполнения данной команды gradle распакует jar-файл и включит двоичные библиотеки в APK-файл.
Ознакомтесь с модифицированным файлом build.gradle, что бы не перепутать dependencies и buildscript.dependencies.
buildscript { repositories { mavenCentral() } dependencies { classpath 'com.android.tools.build:gradle:0.5.+' } } apply plugin: 'android' repositories { mavenCentral() } android { compileSdkVersion 17 buildToolsVersion "17.0.0" defaultConfig { minSdkVersion 9 targetSdkVersion 9 } } dependencies { compile 'com.android.support:support-v4:18.0.0' compile fileTree(dir: 'src/main/', include: '*.jar') }
Читатель скорее всего подметит, что данный способ весьма неудобен при наличии необходимости частых изменений в нативном коде, так как после каждой перекомпиляции необходимо удалять старый jar-файл, переименовывать папку libs в lib, архивировать её, менять расширение архива. Поэтому воспользуемся мощью gradle и автоматизируем процесс.
Автоматический способ
Сборщик пакетов gradle позволяет создавать задания (функции), также в его возможности входит создание различных типов архивов, в том числе zip. Воспользуемся этим и добавим в build.gradle (расположение данного файла рассмотрено выше) такие строчки:
task nativeLibsToJar(type: Zip, description: 'create a jar archive of the native libs') { destinationDir file("$buildDir/native-libs") baseName 'native-libs' extension 'jar' from fileTree(dir: 'src/main/libs', include: '**/*.so') into 'lib/' } tasks.withType(Compile) { compileTask -> compileTask.dependsOn(nativeLibsToJar) }
Данный отрывок кода можно добавить в любую часть файла, кроме существующих разделов, например в конец файла. Я поместил их перед разделом dependencies.
Этот код включает в себя task, который создает в папке build, находящийся по адресу "<путь_к_проекту>/<имя_проекта>Project/<имя_проекта>/" подкаталог native-libs, в этой подпапке создается файл native-libs.jar, структура файла соответствует требуемой структуре java-библиотеки, содержащей бинарные библиотеки .so. Если Вы планируете использовать также статические библиотеки .a, то вместо строки:
from fileTree(dir: ‘src/main/libs’, include: ‘**/*.so’)
Вам следует использовать:
from fileTree(dir: ‘src/main/libs’, include: ‘**/*.*’)
Далее остается добавить в раздел dependencies строку:
compile fileTree(dir: "$buildDir/native-libs", include: ‘native-libs.jar’)
В ходе сборки эта команда включит содержимое созданной программно библиотеки native-libs.jar в APK-файл.
Пример build.gradle с данным кодом:
buildscript { repositories { mavenCentral() } dependencies { classpath 'com.android.tools.build:gradle:0.5.+' } } apply plugin: 'android' repositories { mavenCentral() } android { compileSdkVersion 17 buildToolsVersion "17.0.0" defaultConfig { minSdkVersion 9 targetSdkVersion 9 } } task nativeLibsToJar(type: Zip, description: 'create a jar archive of the native libs') { destinationDir file("$buildDir/native-libs") baseName 'native-libs' extension 'jar' from fileTree(dir: 'src/main/libs', include: '**/*.so') into 'lib/' } tasks.withType(Compile) { compileTask -> compileTask.dependsOn(nativeLibsToJar) } dependencies { compile 'com.android.support:support-v4:18.0.0' compile fileTree(dir: "$buildDir/native-libs", include: 'native-libs.jar') }
Обратите внимание, что у меня дирректории jni, libs расположены по адресу "<путь_к_проекту>/<имя_проекта>Project/<имя_проекта>/src/main". Если в Вашем проекте эти папки лежат в другом месте, то Вам следует учесть это в формировании путей для всех команд.
Если всё сделано правильно, то Android Studio в ходе сборки проекта автоматически создаст в каталоге build правильную библиотеку и включит её в готовую программу. Таким образом, после каждой перекомпиляции нативного кода отпадает необходимость совершения каких-либо дополнительных действий и настроек, gradle сделает всё сам.
Теперь, как и было обещано в начале статьи, краткое резюме, описывающее лишь полный алгоритм без лишних комментариев:
Краткое резюме
- Открываем папку "<путь_к_проекту>/<имя_проекта>Project/<имя_проекта>/src/main" и создаём там подпапку jni.
- Открываем файл "<путь_к_проекту>/<имя_проекта>Project/<имя_проекта>/build.gradle", модифицируем раздел dependencies, после чего добавляем туда код:
dependencies { compile 'com.android.support:support-v4:18.0.0' compile fileTree(dir: "$buildDir/native-libs", include: 'native-libs.jar') } task nativeLibsToJar(type: Zip, description: 'create a jar archive of the native libs') { destinationDir file("$buildDir/native-libs") baseName 'native-libs' extension 'jar' from fileTree(dir: 'src/main/libs', include: '**/*.so') into 'lib/' } tasks.withType(Compile) { compileTask -> compileTask.dependsOn(nativeLibsToJar) }
Для включения также статических библиотек *.a (при их наличии) меняем строку
from fileTree(dir: ‘src/main/libs’, include: ‘**/*.so’)
на
from fileTree(dir: ‘src/main/libs’, include: ‘**/*.*’)
- В подпапке jni размещаем файлы *.mk, *.h, *.c, пишем нативный код.
- Открываем папку "<путь_к_проекту>/<имя_проекта>Project/<имя_проекта>/src/main" в терминале.
- Вводим в терминале команду <путь_к_ndk>/ndk-build
- Запускаем проект.
Важно!
Данная инструкция предназначена для операционных систем Unix (в моём случае — MacOSX). Для операционной системы MS Windows пункты 4 и 5 не актуальны, так как для компиляции нативных библиотек требуются дополнительные утилиты. Также, скорее всего, будет целесообразным изменить пути хранения библиотек на более удобные и учесть это в скрипте сборки.
На этом я завершаю статью и откланиваюсь. Надеюсь, кому-то данная статья сэкономит время.
Удачного Вам нативного программирования, главное — каждый раз не забывайте себя спрашивать, стоит ли использовать нативный код. Вполне могут быть java-аналоги, использование которых проще, а в большинстве случаев – лучше, так как сокращается время разработки, улучшается понимаемость кода другими, снижается сложность архитектуры приложения, а мощности современных устройств хватает на выполнение большинства задач даже в Dalvik VM.
ссылка на оригинал статьи http://habrahabr.ru/post/193122/
Добавить комментарий