Как стать автором
Обновить

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

Время на прочтение9 мин
Количество просмотров68K
В настоящее время среди Android-девелоперов большую популярность имеет среда разработки Android Studio, основанная на IntelliJ IDEA от JetBrains. Однако, при использовании данной IDE, могут возникнуть проблемы при разработке приложений, использующих нативный код, так как Android NDK рассчитан преимущественно на использование IDE Eclipse и ADT.

Цель данной статьи — подробное описание процесса создания 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 на русском языке, советую обратить внимание (написана не мной):

idev.by/android/21115

Перехожу к основному разделу статьи — настройке 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:
stackoverflow.com/questions/16667903/android-studio-gradle-and-ndk
Однако, данная информация достаточно краткая, разрозненная и требует от читателя определённых знаний синтаксиса 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 сделает всё сам.

Теперь, как и было обещано в начале статьи, краткое резюме, описывающее лишь полный алгоритм без лишних комментариев:

Краткое резюме


  1. Открываем папку "<путь_к_проекту>/<имя_проекта>Project/<имя_проекта>/src/main" и создаём там подпапку jni.
  2. Открываем файл "<путь_к_проекту>/<имя_проекта>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: '**/*.*')

  3. В подпапке jni размещаем файлы *.mk, *.h, *.c, пишем нативный код.
  4. Открываем папку "<путь_к_проекту>/<имя_проекта>Project/<имя_проекта>/src/main" в терминале.
  5. Вводим в терминале команду <путь_к_ndk>/ndk-build
  6. Запускаем проект.


Важно!
Данная инструкция предназначена для операционных систем Unix (в моём случае — MacOSX). Для операционной системы MS Windows пункты 4 и 5 не актуальны, так как для компиляции нативных библиотек требуются дополнительные утилиты. Также, скорее всего, будет целесообразным изменить пути хранения библиотек на более удобные и учесть это в скрипте сборки.

На этом я завершаю статью и откланиваюсь. Надеюсь, кому-то данная статья сэкономит время.

Удачного Вам нативного программирования, главное — каждый раз не забывайте себя спрашивать, стоит ли использовать нативный код. Вполне могут быть java-аналоги, использование которых проще, а в большинстве случаев – лучше, так как сокращается время разработки, улучшается понимаемость кода другими, снижается сложность архитектуры приложения, а мощности современных устройств хватает на выполнение большинства задач даже в Dalvik VM.

Теги:
Хабы:
Всего голосов 37: ↑36 и ↓1+35
Комментарии8

Публикации

Истории

Работа

Ближайшие события

15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань