№1 в разработке мобильных решений для бизнеса
123,44
рейтинг
24 августа 2015 в 16:18

Разработка → NDK c полпинка tutorial

Вместо пролога


Я, пожалуй, нарушу традицию всех подобных статей и не буду рассказывать, зачем нужен NDK. Если вы это читаете, значит вам он зачем-то понадобился. Но один из реальных кейсов использования мы, конечно же, рассмотрим.

Эта история началась с того, что на Google I/O 2015 была представлена поддержка нативной разработки прямо не выходя из студии, Android-студии. Естественно, Google, как всегда, наобещал и заставил ждать, пока они все это выкатят для обычных людей. И вот 9 июля это свершилось: на stable channel появился кусочек того пирога, который нам наобещали.

Естественно, нельзя так просто взять и начать использовать это так, как хотелось бы.

image

Но мы все равно будем.

Готовим инструмент


Для начала создадим простой проект с одной activity, на которую нужно кинуть 2 TextView, EditText и Button. Далее идем в настройки проекта (⌘;) и в SDK Location указываем путь к NDK. Если ничего такого у вас до сих пор не существовало, то студия любезно предложит его скачать.

Самое трудное позади ;) Теперь настроим систему сборки. Для этого топаем в gradle/wrapper/gradle-wrapper.properties и меняем указанный там distributionUrl на такой:

distributionUrl=https\://services.gradle.org/distributions/gradle-2.5-all.zip

Экспериментальный плагин, который делает всю магию, работает пока что только с этой версией Gradle.

Далее необходимо этот самый плагин натянуть на проект. Сделать это можно в корневом build.gradle файле. Для этого меняем имя и версию плагина:

classpath 'com.android.tools.build:gradle-experimental:0.2.0'

И переходим к build файлу модуля, где и начнется интересное. Официальная документация предлагает нам привести файл к такому виду:

build.gradle
apply plugin: 'com.android.model.application'
model {
    android {
        compileSdkVersion = 22
        buildToolsVersion = "22.0.1"

    defaultConfig.with {
            applicationId = "com.redmadrobot.ndkdemo"
            minSdkVersion.apiLevel = 15
            targetSdkVersion.apiLevel = 22
            versionCode = 1
            versionName = "1.0"
        }
    }

    /*
     * native build settings
     */
    android.ndk {
        moduleName = "security"

        cppFlags += "-std=c++11"
        stl = "stlport_static"
    }
    android.buildTypes {
        release {
            minifyEnabled = false
            proguardFiles += file('proguard-rules.txt')
        }

    }
    android.productFlavors {
        // for detailed abiFilter descriptions, refer to "Supported ABIs" @
        // https://developer.android.com/ndk/guides/abis.html#sa
        create("arm") {
            ndk.abiFilters += "armeabi"
        }
        create("arm7") {
            ndk.abiFilters += "armeabi-v7a"
        }
        create("arm8") {
            ndk.abiFilters += "arm64-v8a"
        }
        create("x86") {
            ndk.abiFilters += "x86"
        }
        create("x86-64") {
            ndk.abiFilters += "x86_64"
        }
        create("mips") {
            ndk.abiFilters += "mips"
        }
        create("mips-64") {
            ndk.abiFilters += "mips64"
        }
        // To include all cpu architectures, leaves abiFilters empty
        create("all")
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:22.2.1'
}



И обещает, что все будет хорошо. Врет. Не будет. При попытке собрать проект студия плюнет в нас достаточно невнятным логом и предложит примерить на себя роль алтайского шамана. Покопавшись (как водится) в интернетах, я нашел решение. Необходимо явно сказать системе сборки, что будет использоваться Java 1.7. Кто бы мог подумать… В результате получается вот такой build-файл:

Правильный build.gradle
apply plugin: 'com.android.model.application'
model {
    android {
        compileSdkVersion = 22
        buildToolsVersion = "22.0.1"

        defaultConfig.with {
            applicationId = "com.redmadrobot.ndkdemo"
            minSdkVersion.apiLevel = 15
            targetSdkVersion.apiLevel = 22
            versionCode = 1
            versionName = "1.0"
        }
    }

    /*
     * native build settings
     */
    android.ndk {
        moduleName = "security"

        cppFlags += "-std=c++11"
        stl = "stlport_static"
    }
    android.buildTypes {
        release {
            minifyEnabled = false
            proguardFiles += file('proguard-rules.txt')
        }

    }
    android.productFlavors {
        // for detailed abiFilter descriptions, refer to "Supported ABIs" @
        // https://developer.android.com/ndk/guides/abis.html#sa
        create("arm") {
            ndk.abiFilters += "armeabi"
        }
        create("arm7") {
            ndk.abiFilters += "armeabi-v7a"
        }
        create("arm8") {
            ndk.abiFilters += "arm64-v8a"
        }
        create("x86") {
            ndk.abiFilters += "x86"
        }
        create("x86-64") {
            ndk.abiFilters += "x86_64"
        }
        create("mips") {
            ndk.abiFilters += "mips"
        }
        create("mips-64") {
            ndk.abiFilters += "mips64"
        }
        // To include all cpu architectures, leaves abiFilters empty
        create("all")
    }
    // Our workaround
    compileOptions.with {
        sourceCompatibility = JavaVersion.VERSION_1_7
        targetCompatibility = JavaVersion.VERSION_1_7
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:22.2.1'
}


Рассматривать подробно формат файла я не буду. Вот ссылка для пытливых умов:тык

Все приготовления закончены и можно программировать. Проделать все описанное выше для того, чтобы написать Hello world было бы по меньшей мере преступлением, поэтому мы сделаем кое-что поинтереснее.

Постановка задачи


Как и обещал, мы затронем один из основных кейсов использования нативного кода в Android-приложениях. Я говорю о безопасности. Часто бывает, что нужно скрыть от посторонних глаз какую-то информацию путем ее шифрования. И тут возникает резонный вопрос: как же обеспечить безопасность ключей и алгоритмов шифрования? Ведь любой школьник может получить почти точную копию исходного кода приложения и узнать все явки и пароли!

Да, это действительно так. И тут нам на помощь приходят нативные библиотеки. Все алгоритмы, ключи и тому подобные вещи можно спрятать глубоко в этих библиотеках, а наружу выставить интерфейс из пары функций encrypt/decrypt. Да, это тоже ломается, но уже не кем попало. Человек должен быть достаточно мотивирован, чтобы залезть в нативный код вашего приложения. Но это уже другая история. И так — решено! Будем писать систему шифрования данных. В статье я рассмотрю ОЧЕНЬ примитивные алгоритмы. Пожалуйста, не используйте их в production.

public class MainActivity extends AppCompatActivity {

    static {
        System.loadLibrary("security");
    }

    public native String encrypt(String plainText);
    public native String decrypt(String ciphertext);

Тут мы загружаем библиотеку и объявляем 2 метода, которые будут использоваться для шифровки/дешифровки. Естественно, имена этих методов студия нам подсветит красным и скажет Cannot resolve corresponding JNI function..., так как мы еще их не написали. И тут в игру вступает вся прелесть интеграции, о которой я говорил в самом начале. Устанавливаем курсор на имя функции (например encrypt), нажимаем Alt+Enter и нам предложат создать эту функцию. Любезно соглашаемся и наблюдаем поистине магическое действо! У нас создался каталог jni, а в нем файлик security.c таким содержанием:

#include <jni.h>

JNIEXPORT jstring JNICALL
Java_ru_freedomlogic_ndktester_MainActivity_encrypt(JNIEnv *env, jobject instance,
                                                    jstring plainText_) {
    const char *plainText = (*env)->GetStringUTFChars(env, plainText_, 0);

    // TODO

    (*env)->ReleaseStringUTFChars(env, plainText_, plainText);

    return (*env)->NewStringUTF(env, returnValue);
}

Да это просто какая-то добрая магия!!! Все круто, но есть нюанс. Почему С? Я хочу С++! Как сделать так, чтобы по умолчанию создавался cpp файл, я не нашел, поэтому решено было просто переименовать файл. Матерый читатель сейчас ухмыльнется моей наивности и будет прав. Конечно же, ничего сразу не заработало. Пришлось доставать инструмент решения любых проблем.



Результатом камлания, затронувшего верхний и нижний миры стал вот такой ритуал прихода к С++
  • Clean project
  • Измениить расширение файла
  • Resync build.gralde
  • Исправить появившиеся ошибки в .cpp файле
  • Сделать make project
  • Наслаждаться написанием С++ кода

Теперь напишем код нашей «суперкриптосистемы».

security.cpp
#include <jni.h>
#include <algorithm>

#include "base64.h"

using namespace std;

unsigned char key[] = {4, 2, 9, 4, 9, 6, 7, 2, 9, 5};


string applyXor(string sequence) {
    int maxIndex = sizeof(key) - 1;

    string result = sequence;
    size_t sequenceSize = sequence.size();

    int keyIndex = 0;

    for (int i = 0; i < sequenceSize; i++) {
        if (keyIndex > maxIndex)
            keyIndex = 0;

        result[i] = sequence[i] ^ key[keyIndex++];
    }

    return result;
}

extern "C" {

JNIEXPORT jstring JNICALL
Java_com_redmadrobot_ndkdemo_MainActivity_encrypt(JNIEnv *env, jobject instance,
                                                  jstring plainText_) {
    const char *plainText = env->GetStringUTFChars(plainText_, 0);

    string sequence = applyXor(plainText);
    size_t sequenceSize = sequence.size();

    reverse(sequence.begin(), sequence.end());

    int code = 0;

    for (unsigned long i = 0; i < sequenceSize; i++) {
        code = sequence[i] + 5;
        sequence[i] = (char) code;
    }

    env->ReleaseStringUTFChars(plainText_, plainText);

    return env->NewStringUTF(base64::encode(sequence).c_str());
}

JNIEXPORT jstring JNICALL
Java_com_redmadrobot_ndkdemo_MainActivity_decrypt(JNIEnv *env, jobject instance,
                                                  jstring ciphertext_) {
    const char *ciphertext = env->GetStringUTFChars(ciphertext_, 0);

    string sequence =  base64::decode(ciphertext);
    size_t sequenceSize = sequence.size();

    int code;
    for (int i = 0; i < sequenceSize; i++) {
        code = sequence[i] - 5;
        sequence[i] = (char) code;
    }

    reverse(sequence.begin(), sequence.end());

    env->ReleaseStringUTFChars(ciphertext_, ciphertext);

    return env->NewStringUTF(applyXor(sequence).c_str());
}
}


Полностью объяснять все происходящее я не вижу смысла, но несколько пояснений дам. Во-первых extern «C»{...}. Если не обернуть им интерфейс, то компилятор просто поменяет имена функций, и в результате ваше приложение упадет в рантайме, обратившись к несуществующему (к тому моменту) имени функции. Подробнее здесь. Вторая засада поджидает нас с функциями GetStringUTFChars и NewStringUTF. Они очень плохо переваривают ту кашу, которая получается в результате шифрования, что опять же приводит к падениям приложения. Подробности здесь.

Теперь осталось только применить эти функции в своем коде.

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        final EditText inputText = (EditText) findViewById(R.id.input_text);
        final Button encryptButton = (Button) findViewById(R.id.encrypt_button);
        final TextView cipherText = (TextView) findViewById(R.id.cipher_text);
        final TextView plainText = (TextView) findViewById(R.id.plain_text);

        encryptButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(final View v) {
                String cipher = encrypt(inputText.getText().toString());

                cipherText.setText(cipher);
                plainText.setText(decrypt(cipher));
            }
        });
    }

Далее просто запускаем, и все просто работает. Или не работает. В этом случае нам пригодится удобный отладчик, который пока немного тормозной, но интегрирован в экосистему, и это главное. Чтобы отлаживать нативный код, необходимо создать новую конфигурацию Android Native, расставить точки останова наслаждаться удобной отладкой.



Подведение итогов


Ребята из Google проделали большую работу для того, чтобы дать нам еще один удобный инструмент. Конечно, он еще не совершенен и порой приходится очень активно стучать в бубен, но в целом это та вещь, которой уже можно пользоваться. Код демо-проекта можно найти на GitHub. Дополнительную информацию можно найти здесь. Быстрой компиляции всем!

Читайте также:
Библиотека Chronos: облегчаем написание долгих операций
Сажаем контроллеры на диету: Android
Архитектурный дизайн мобильных приложений: часть 1
Автор: @Fi5t
REDMADROBOT
рейтинг 123,44
№1 в разработке мобильных решений для бизнеса

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

  • –6
    имхо связка MS Visual Studio и VisualGDB и удобнее и проще
  • +1
    Интересно, как Android Studio будет стыковаться с нынешней системой сборки NDK, основанной на Android.mk / Application.mk? Будет грустно, если ребята из Google на нее просто забьют.
  • 0
    Любопытная схема на КПДВ…
  • 0
    Доступ к устройствам только через java?
    Например такие вещи как GPU и /dev/video?
    • 0
      Это на ваше усмотрение. Некоторые любят делать все через нативный код.
      • 0
        Стоп, прямой доступ к экрану доступен в NDK? Да и не только экрану (но 3D-ускорение обычно проблема из проблем) а к примеру сенсорному экрану, карте памяти и т.п. Что там пишется в требованиях у NDK приложениях?
  • 0
    Спасибо, пригодилось. Пожалуй самый элегантный способ интеграции NDK на текущий момент

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

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