Нативные библиотеки для Android

от автора

В этой статье мы рассмотрим работу с нативными библиотеками, которые могут содержаться в приложениях для Android. Нативные библиотеки — это код, который разработчик написал, а затем скомпилировал для конкретной архитектуры компьютера. Чаще всего этот код написан на C или C++. Наиболее распространенными причинами, по которым разработчик может это сделать, являются математически сложные или требующие больших затрат времени операции, такие как работа с графическими библиотеками.

Стоит отметить, что разработчики вредоносных программ также начали переходить на машинный код, поскольку обратная инженерия скомпилированных двоичных файлов, как правило, менее распространена, чем анализ байт-кода DEX. Во многом это связано с тем, что байт-код DEX может быть декомпилирован на Java, в то время как нативный, скомпилированный код часто приходится анализировать как ассемблер.

В этой статье мы будем говорить об использовании нативных библиотек для ARM и x86, чтобы каждый пользователь мог выбрать ту архитектуру, с которой ему удобнее работать.

Введение в Java Native Interface

JNI (Java Native Interface) — это программный интерфейс в языке программирования Java, который позволяет взаимодействовать с приложениями и библиотеками, написанными на других языках программирования, таких как C, C++ или ассемблер. Данный интерфейс позволяет разработчикам объявлять методы Java, которые реализованы в машинном коде (обычно также скомпилированном на C/C++). Интерфейс JNI не специфичен для Android, но в целом доступен для Java-приложений, работающих на разных платформах.

Android Native Development Kit (NDK) — это набор инструментов для Android, разработанный специально для JNI. Данный набор инструментов позволяет разработчикам писать код на C и C++ для своих приложений для Android.

Вместе JNI и NDK позволяют разработчикам Android реализовывать некоторые функциональные возможности своих приложений в машинном коде. Код Java (или Kotlin) будет вызывать объявленный в Java собственный метод, который реализуется в скомпилированной собственной библиотеке.

Наиболее важным преимуществом JNI является то, что он не накладывает ограничений на реализацию базовой виртуальной машины Java. Следовательно, провайдеры Java VM могут добавить поддержку JNI, не затрагивая другие части виртуальной машины. Программисты могут написать одну версию собственного приложения или библиотеки и ожидать, что она будет работать со всеми виртуальными машинами Java, поддерживающими JNI.

Анализируем нативные библиотеки Android

Далее мы сосредоточимся на том, как реконструировать функциональность приложения, которая была реализована в нативных библиотеках Android. Прежде всего, давайте разберемся, что мы имеем в виду, когда говорим «нативные библиотеки Android»?

Если рассматривать структуру приложения под Android, то собственные библиотеки Android включены в APK-файлы как .so, библиотеки общих объектов, в формате файла ELF. Если вы ранее анализировали двоичные файлы Linux, это тот же формат.

Эти библиотеки по умолчанию включены в APK-файл по пути к файлу /lib/<cpu>/lib<name>.so. Это путь по умолчанию, но разработчики могут также включить встроенную библиотеку в /assets/<имя_пользователя>, если они того пожелают.

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

Ниже приведены пути для каталогов по умолчанию для разных процессоров:

  • CPU — Native Library Path

  • “generic” 32-bit ARM — lib/armeabi/libcalc.so

  • x86 — lib/x86/libcalc.so

  • x64 — lib/x86_64/libcalc.so

  • ARMv7 — lib/armeabi-v7a/libcalc.so

  • ARM64 — lib/arm64-v8a/libcalc.so

Выполняем код

Прежде чем приложение для Android сможет вызвать и выполнить любой код, реализованный в собственной библиотеке, приложение (Java-код) должно загрузить библиотеку в память. Для этого есть два разных вызова API:

System.loadLibrary("calc")

или

System.load("lib/armeabi/libcalc.so")

Разница между двумя представленными вызовами API заключается в том, что LoadLibrary принимает только краткое имя библиотеки в качестве аргумента (т. е. libcalc.so = “calc” & libinit.so = “init”), и система правильно определит архитектуру, на которой она работает в данный момент, и, следовательно, правильный файл для использования. С другой стороны, для загрузки требуется полный путь к библиотеке. Это означает, что разработчик приложения должен сам определить архитектуру и, следовательно, правильный файл библиотеки для загрузки.

Когда Java-код вызывает один из этих двух API (LoadLibrary или load), собственная библиотека, передаваемая в качестве аргумента, выполняет свой JNI_OnLoad, если он был в ней реализован.

Напомним, что перед выполнением любых собственных методов необходимо загрузить свою библиотеку, вызвав System.LoadLibrary или System.load в коде Java. Когда выполняется любой из этих двух API, также выполняется функция JNI_OnLoad в собственной библиотеке.

Соединяем Java и нативный код

Теперь давайте рассмотрим взаимодействия Java и нативных библиотек. Чтобы выполнить функцию из собственной библиотеки, должен существовать объявленный в Java метод, который может быть вызван в Java‑коде. Когда вызывается этот метод, выполняется «парная» функция из собственной библиотеки (ELF/.so).

В коде появляется объявленный Java‑метод Native, как показано ниже. Он выглядит как любой другой Java‑метод, за исключением того, что включает ключевое слово native и не содержит кода в своей реализации, поскольку его код на самом деле находится в скомпилированной native-библиотеке.

public native String doThingsInNativeLibrary(int var0);

По идее, чтобы вызвать этот собственный метод, Java-код должен вызвать его так же, как и любой другой метод Java. Но в серверной части JNI и NDK необходимо выполнить соответствующую функцию в собственной библиотеке. Чтобы сделать это, он должен знать сопряжение между объявленным в Java собственным методом и функцией в собственной библиотеке.

Есть 2 разных способа выполнить это сопряжение или связывание:

  • Динамическое связывание с использованием разрешения имен собственного метода JNI

  • Статическое связывание с использованием вызова registerNatives API

Рассмотрим эти способы подробнее.

Динамическое связывание

Чтобы динамически связать объявленный в Java собственный метод и функцию в собственной библиотеке, разработчик называет метод и функцию в соответствии со спецификациями таким образом, чтобы система JNI могла динамически выполнять связывание.

Согласно спецификации, разработчик должен назвать функцию следующим образом, чтобы система могла динамически связывать собственный метод и функцию. Имя собственного метода составляется из следующих компонентов:

  • префикс Java_

  • полное имя класса

  • разделитель с подчеркиванием (“_”)

  • имя метода.

  • для переопределенных нативных методов используются два символа подчеркивания (“__”), за которыми следует сигнатура аргумента

Чтобы выполнить динамическую компоновку для описанного ниже нативного метода, объявленного на Java, и предположим, что он находится в классе com.android.interesting.Stuff

 public native String doThingsInNativeLibrary(int var0);

Функцию в нативной библиотеке можно назвать следующим образом

Java_com_android_interesting_Stuff_doThingsInNativeLibrary

 Если в собственной библиотеке нет функции с таким именем, это означает, что приложение должно выполнять статическое связывание, о котором речь пойдет далее.

Статическое связывание

Если разработчик не хочет или не может назвать собственные функции в соответствии со спецификацией (например, хочет удалить символы отладки), то он должен использовать статическое связывание с API registerNatives, чтобы выполнить сопряжение между объявленным в Java собственным методом и функцией в native библиотеке. Функция registerNatives вызывается из машинного кода, а не из кода Java, и чаще всего вызывается в функции JNI_OnLoad, поскольку registerNatives должны быть выполнены до вызова объявленного в Java машинного метода.

 jint RegisterNatives(JNIEnv *env, jclass clazz, const JNINativeMethod *methods, jint nMethods);     typedef struct {       char *name;       char *signature;       void *fnPtr;   } JNINativeMethod;

Таким образом, статическое и динамическое связывание используются в Java для определения того, какой метод будет вызван во время выполнения программы. Статическое связывание происходит во время компиляции кода на основе типа переменной или ссылки, а динамическое связывание происходит во время выполнения программы.

Заключение

Представленные в статье методы позволяют расширить функционал приложений под Android с помощью использования нативного кода. В результате приложения могут выполнять боле сложные или ресурсоемкие задачи.

Прокачать свои навыки разработки Android приложений до уровня Middle/Senior можно на онлайн‑курсе «Android Developer. Professional» под руководством экспертов сферы.


ссылка на оригинал статьи https://habr.com/ru/articles/844110/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *