Calling java from с android

Вызов Java методов из C/C++ кода при помощи JNI на Android.

В прошлой статье посвященной JNI на Android я описывал как начать работу с Android NDK, в том числе, как вызывать нативные методы из Java, на этот раз я опишу как вызывать Java-методы из нативного кода.

Для лучшего понимания этой статьи, рекомендую предварительно ознакомиться с основами JNI в Android. Сам процесс вызова Java-метода из нативного кода далее мы будем называть обратным вызовом, так как такие вызовы чаще всего используются для оповещения Java-стороны приложения о некоторых событиях, просходящих «в нативе». Код доступен на нашем репозитории на GitHub. Для нетерпеливых есть быстрое решение

Общее описание механизма

И так, перед нами стала задача «дернуть» Java-метод из нативного кода. Каков будет порядок действий?

  1. Опеределить какой метод и у какого класса вы хотите вызвать. Банально, да.
  2. Получить дескриптор нужного класса.
  3. Описать сигнатуру метода на языке JNI-примитивов (не так страшно, как звучит).
  4. Получить идентификатор метода (что-то типа ссылки на метод).
  5. Вызвать метод у нужного объекта (если метод экземплярный) или класса (если метод статический).

Определяемя с методами

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

Пусть у нас есть следующий интерфейс NativeCallListener :

Данному интерфейсу в нативном коде на C++ будет соответстововать следующий класс JniNativeCallListener :

Заранее хочу обратить внимание, что действия по инциализации объекта будут местами избыточны для случая однопоточного приложения, но обязательны при использовании многопоточности и pthread.

Теперь перейдем к непосредственной инициализации и получению дескрипторов.

Получаем дескриптор класса и методов

К получению дескриптора класса есть два подхода. Первый мне нравится:

jclass clazz = pJniEnv->GetObjectClass(pWrappedInstance);

Второй не очень:

jclass clazz = pEnv->FindClass(«by/idev/jni/javacall/MainActivity»);

Преимущества первого подхода в отсутсвии хард-кода, второй подход позволяет получить дескриптор (jclass) не имея ссылки на объект. Так что решение о том, какой из них использоват зависит от ситуации.

Полностью инициализация будет выглядеть так:

В данном случая, лаконичности ради, я провожу инициализацию в конструкторе, что является плохим тоном. Гораздо лучшим вариантом был бы отдельный метод инициализации, потому что мы помним, что логика и понетциальные ошибки в конструкторе — это плохо, а при вызовах GetMethodID и GetObjectClass и им подобных могут возникать ошибки (они могут вернуть NULL , хотя со мной такого не случалось).

Логирование определено среди утилит в файле Util.h

Вы можете спросить, а зачем нужны эти строки:

А я напомню, что ссылки на объекты, переданные в JNI-метод, действительны только в пределах времени выполнения этого метода. То есть при попытки обратится к mObjectRef или pJniEnv после выполнения метода мы потерпим фиаско. Поэтому мы создаем глобальную ссылку на mObjectRef , чтобы потому вызвать у него нужный метод, и получаем ссылку на Java-машину mJVM , что при помощи нее в будещем получать JNIEnv . Вот так все запутанно, но походу дела все станет понятнее Чтобы получать JNIEnv используется метод getJniEnv():

Определение сигнатуры и получение идентификатора метода

Этим целом служат строки которые вы уже видели:

В качестве параметров GetMethodID служат дескриптор класса, имя метода и дикое, на первый взгляд, описание списка параметров и возвращаемого типа «()V» и «(Ljava/lang/String;)V». В скобках указаны входные параметры, после них — возвращаемый тип. Таким образом первый метод не имеет входных параметров и ничего не возвращает, а второй принимает строку и тоже ничего не возвращает. Ниже приведена таблица типов параметров и пару примеров описания методов.

Поддерживаемые JNI типы данных и их коды

Java JNI JNI array Код Код массива
boolean jboolean jbooleanArray Z [Z
byte jbyte jbyteArray B [B
char jchar jcharArray C [C
double jdouble jdoubleArray D [D
float jfloat jfloatArray F [F
int jint jintArray I [I
long jlong jlongArray J [J
short jshort jshortArray S [S
Object jobject jobjectArray L [L
Class jclass нет L [L
String jstring нет L [L
void void нет V нет

Вызов метода обратного вызова

Дескрипторы получены, теперь можно написать код который используя их и JNIEnv произведет нужные действия:

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

На Java-стороне код будет таким:

Все вышесказанное, но очень быстро

Весь код можно сократить до жалких трех строк логики:

Если заморачиваться не нужно — то такой подход сработает, для больших систем я бы все-таки рекомендовал реализовать все на должном уровне Так же не очень разумно делать «циклические вызовы», когда Java-код вызывает С++ код, который вызывает Java-код, который снова вызывает C++ код. Четвертый шаг тут явно избыточный.

Заключение

JNI — замечательный, но требующий бережного отношения к себе механизм, дающий понимание работы многих аспектов Java и программирования в целом. В следующий раз мы с вами займемся доступом к полям объекта. Код для данной статьи тут.

Источник

Calling java from с android

The Vuforia Android samples are set up such that application lifecycle events are handled in Java, but tracking events and rendering are handled natively in C++. Additionally, users may want to leverage Android SDK functionality (such as touch handling or networking logic) while doing the low-level graphics work natively. All this requires that we have a means of communicating between Java and C++. This is provided by the JNI (Java Native Interface).

For a practical example of using the JNI to respond to native tracking events in Java see this article: AndroidUIChanges.xml

We have the following example in the ImageTargets.java class:

Note this method doesn’t include any special JNI syntax. We can call it natively by looking up the class that our ImageTargets Java object belongs to, and then by looking up the getTextureCount method in that class:

JNIEXPORT void JNICALL
Java_com_qualcomm_QCARSamples_ImageTargets_ImageTargets_initApplicationNative(
JNIEnv* env, jobject obj, jint width, jint height)
<
.

jclass activityClass = env->GetObjectClass(obj);

jmethodID getTextureCountMethodID = env->GetMethodID(activityClass,
«getTextureCount», «()I»);
if (getTextureCountMethodID == 0)
<
LOG(«Function getTextureCount() not found.»);
return;
>

textureCount = env->CallIntMethod(obj, getTextureCountMethodID);

The last argument to the GetMethodID call — «()I» — needs some explanation. Inside the parentheses you place the argument types, in order, and after the parentheses you place the return type. So «(IF)Z» would represent a method that takes an int and a float and return a boolean. For more info and a table of the field descriptors see the jni documentation here: http://java.sun.com/docs/books/jni/html/types.html

Note that to call Java methods from C++ we need a handle on the JNIEnv object and the jobject (in this case the ImageTargets object). In the Vuforia samples, getTextureCount is called directly from a method that is called from Java (initApplicationNative). We automatically get the JNIEnv and jobject as the first two arguments to this function. If you want to call a Java method at a later time, however, you’ll need to cache these values globally. It’s somewhat dangerous to store the JNIEnv globally, mainly because it isn’t thread safe. Here’s a safe way to obtain the JNIEnv for the current thread at a later time:

JavaVM* javaVM = NULL;
jclass activityClass;
jobject activityObj;

JNIEXPORT void JNICALL
Java_com_qualcomm_QCARSamples_ImageTargets_ImageTargets_initApplicationNative(
JNIEnv* env, jobject obj, jint width, jint height)
<
env->GetJavaVM(&javaVM);
jclass cls = env->GetObjectClass(obj);
activityClass = (jclass) env->NewGlobalRef(cls);
activityObj = env->NewGlobalRef(obj);
>

void myNativeMethod()
<
JNIEnv *env;
javaVM->AttachCurrentThread(&env, NULL);
jmethodID method = env->GetMethodID(activityClass, «myJavaMethod», «()V»);
env->CallVoidMethod(activityObj, method);
>

Also note the use of NewGlobalRef to make safe global references to the jclass and jobject objects.

Источник

Как Java 8 поддерживается в Android

Привет, Хабр! Предлагаю вашему вниманию перевод замечательной статьи из цикла статей небезызвестного Джейка Вортона о том, как происходит поддержка Андроидом Java 8.

Оригинал статьи лежит тут

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

Это довольно сложная тема. Для начала нужно определиться, что мы вообще подразумеваем под «поддержкой Java в Android», ведь в одной версии языка может быть много всего: фичи (лямбды, например), байткод, тулзы, APIs, JVM и так далее.

Когда говорят о поддержке Java 8 в Android, обычно подразумевают поддержку фичей языка. Итак, начнем с них.

Лямбды

Одним из главных нововведений Java 8 были лямбды.
Код стал более лаконичным и простым, лямбды избавили нас от необходимости писать громоздкие анонимные классы, используя интерфейс с единственным методом внутри.

После компиляции этого, используя javac и легаси dx tool , мы получим следующую ошибку:

Эта ошибка происходит из-за того, что лямбды используют новую инструкцию в байткоде — invokedynamic , которая была добавлена в Java 7. Из текста ошибки можно увидеть, что Android поддерживает ее только начиная с 26 API (Android 8).

Звучит не очень, ведь вряд ли кто-то будет выпускать приложение с 26 minApi. Чтобы это обойти, используется так называемый процесс десахаризации (desugaring), который делает возможным поддержку лямбд на всех версиях API.

История десахаризации

Она довольно красочна в мире Android. Цель десахаризации всегда одна и та же — позволить новым языковым фичам работать на всех устройствах.

Изначально, например, для поддержки лямбд в Android разработчики подключали плагин Retrolambda. Он использовал тот же встроенный механизм, что и JVM, конвертируя лямбды в классы, но делал это в рантайме, а не во время компиляции. Сгенерированные классы были очень дорогими с точки зрения количества методов, но со временем, после доработок и улучшений, этот показатель снизился до чего-то более-менее разумного.

Затем команда Android анонсировала новый компилятор, который поддерживал все фичи Java 8 и был более производительным. Он был построен поверх Eclipse Java компилятора, но вместо генерации Java-байткода генерировал Dalvik-байткод. Однако его производительность все равно оставляла желать лучшего.

Когда новый компилятор (к счастью) забросили, трансформатор Java байткода в Java байткод, который и выполнял дешугаринг, был интегрирован в Android Gradle Plugin из Bazel — системы сборки Google. И его производительность все равно была невелика, поэтому параллельно продолжался поиск более хорошего решения.

И вот нам представили новый dexer — D8, который должен был заменить dx tool . Десахаризация теперь выполнялась во время конвертации скомпилированных JAR-файлов в .dex (dexing). D8 сильно выигрывает в производительности по сравнению с dx , и, начиная с Android Gradle Plugin 3.1 он стал dexer’ом по умолчанию.

Теперь, используя D8, у нас получится скомпилировать приведенный выше код.

Чтобы посмотреть, как D8 преобразовал лямбду, можно использовать dexdump tool , который входит в Android SDK. Она выведет довольно много всего, но мы заострим внимание только на этом:

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

В первом блоке наш main метод с индексом 0000 получает ссылку от поля INSTANCE на класс Java8$1 . Этот класс был сгенерирован во время десахаризации . Байткод метода main тоже нигде не содержит упоминаний о теле нашей лямбды, поэтому, скорее всего, она связана с классом Java8$1 . Индекс 0002 затем вызывает static-метод sayHi , используя ссылку на INSTANCE . Методу sayHi требуется Java8$Logger , поэтому, похоже, Java8$1 имплементирует этот интерфейс. Мы можем убедиться в этом тут:

Флаг SYNTHETIC означает, что класс Java8$1 был сгенерирован и список интерфейсов, которые он включает, содержит Java8$Logger .
Этот класс и представляет собой нашу лямбду. Если вы посмотрите на реализацию метода log , то не увидите тело лямбды.

Вместо этого внутри вызывается static метод класса Java8 — lambda$main$0 . Повторюсь, этот метод представлен только в байткоде.

Флаг SYNTHETIC снова говорит нам, что этот метод был сгенерирован, и его байткод как раз содержит тело лямбды: вызов System.out.println . Причина, по которой тело лямбды находится внутри Java8.class, простая — ей может понадобиться доступ к private членам класса, к которым сгенерированный класс иметь доступа не будет.

Все, что нужно для понимания того, как работает десахаризация, описано выше. Однако, взглянув на это в байткоде Dalvik, можно увидеть, что там все намного более сложно и пугающе.

Преобразование исходников — Source Transformation

Чтобы лучше понимать, как происходит десахаризация, давайте попробуем шаг за шагом преобразовывать наш класс в то, что будет работать на всех версиях API.

Возьмем за основу тот же класс с лямбдой:

Сначала тело лямбды перемещается в package private метод.

Затем генерируется класс, имплементирующий интерфейс Logger , внутри которого выполняется блок кода из тела лямбды.

Далее создается синглтон инстанс Java8$1 , который хранится в static переменной INSTANCE .

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

Если вы посмотрите на сгенерированный класс в байткоде Dalvik, то не найдете имен по типу Java8$1 — там будет что-то вроде -$$Lambda$Java8$QkyWJ8jlAksLjYziID4cZLvHwoY . Причина, по которой для класса генерируется такой нейминг, и в чем его плюсы, тянет на отдельную статью.

Нативная поддержка лямбд

Когда мы использовали dx tool , чтобы скомпилировать класс, содержащий лямбды, сообщение об ошибке говорило, что это будет работать только с 26 API.

Поэтому кажется логичным, что если мы попробуем скомпилировать это с флагом —min-api 26 , то десахаризации происходить не будет.

Однако если мы сдампим .dex файл, то в нем все равно можно будет обнаружить -$$Lambda$Java8$QkyWJ8jlAksLjYziID4cZLvHwoY . Почему так? Это баг D8?

Чтобы ответить на этот вопрос, а также почему десахаризация происходит всегда, нам нужно заглянуть внутрь Java-байткода класса Java8 .

Внутри метода main мы снова видим invokedynamic по индексу 0 . Второй аргумент в вызове — 0 — индекс ассоциируемого с ним bootstrap метода.

Вот список bootstrap методов:

Здесь bootstrap метод назван metafactory в классе java.lang.invoke.LambdaMetafactory . Он живет в JDK и занимается созданием анонимных классов налету (on-the-fly) в рантайме для лямбд так же, как и D8 генерит их в компайлтайме.

Если взглянуть на документацию Android к java.lang.invoke
или на исходники AOSP к java.lang.invoke , увидим, что в рантайме этого класса нет. Вот поэтому дешугаринг всегда происходит во время компиляции, независимо от того, какой у вас minApi. VM поддерживает байткод инструкцию, похожую на invokedynamic , но встроенный в JDK LambdaMetafactory недоступен для использования.

Method References

Вместе с лямбдами в Java 8 добавили ссылки на методы — это эффективный способ создать лямбду, тело которой ссылается на уже существующий метод.

Наш интерфейс Logger как раз является таким примером. Тело лямбды ссылалось на System.out.println . Давайте превратим лямбду в метод референc:

Когда мы это скомпилируем и взглянем на байткод, то увидим одно различие с предыдущей версией:

Вместо вызова сгенерированного Java8.lambda$main$0 , который содержит вызов System.out.println , теперь System.out.println вызывается напрямую.

Класс с лямбдой больше не static синглтон, а по индексу 0000 в байткоде видно, что мы получаем ссылку на PrintStream — System.out , который затем используется для того, чтобы вызвать на нем println .

В итоге наш класс превратился в это:

Default и static методы в интерфейсах

Еще одним важным и серьезным изменением, которое принесла Java 8, стала возможность объявлять default и static методы в интерфейсах.

Все это тоже поддерживается D8. Используя те же инструменты, что и ранее, несложно увидеть задешугаренную версию Logger’a с default и static методами. Одно из различий с лямбдами и method references в том, что дефолтные и статик методы реализованы в Android VM и, начиная с 24 API, D8 не будет дешугарить их.

Может, просто использовать Kotlin?

Читая статью, большинство из вас, наверное, подумали о Kotlin. Да, он поддерживает все фичи Java 8, но реализованы они kotlinc точно так же, как и D8, за исключением некоторых деталей.

Поэтому поддержка Андроидом новых версий Java до сих пор очень важна, даже если ваш проект на 100% написан на Kotlin.

Не исключено, что в будущем Kotlin перестанет поддерживать байткод Java 6 и Java 7. IntelliJ IDEA, Gradle 5.0 перешли на Java 8. Количество платформ, работающих на более старых JVM, сокращается.

Desugaring APIs

Все это время я рассказывал про фичи Java 8, но ничего не говорил о новых API — стримы, CompletableFuture , date/time и так далее.

Возвращаясь к примеру с Logger’ом, мы можем использовать новый API даты/времени, чтобы узнать, когда сообщения были отправлены.

Снова компилируем это с помощью javac и преобразуем его в байткод Dalvik с D8, который дешугарит его для поддержки на всех версиях API.

Можете даже запушить это на свой девайс, чтобы убедиться, что оно работает.

Если на этом устройстве API 26 и выше, появится месседж Hello. Если нет — увидим следующее:

D8 справился с лямбдами, метод референсами, но не сделал ничего для работы с LocalDateTime , и это очень печально.

Разработчикам приходится использовать свои собственные реализации или обертки над date/time api, либо использовать библиотеки по типу ThreeTenBP для работы со временем, но почему то, что ты можешь написать руками, не может сделать D8?

Эпилог

Отсутствие поддержки всех новых API Java 8 остается большой проблемой в экосистеме Android. Ведь вряд ли каждый из нас может позволить указать 26 min API в своем проекте. Библиотеки, поддерживающие и Android и JVM, не могут позволить себе использовать API, представленный нам 5 лет назад!

И даже несмотря на то, что саппорт Java 8 теперь является частью D8, каждый разработчик все равно должен явно указывать source и target compatibility на Java 8. Если вы пишете собственные библиотеки, то можете усилить эту тенденцию, выкладывая библиотеки, которые используют Java 8 байткод (даже если вы не используете новые фичи языка).

Над D8 ведется очень много работ, поэтому, кажется, в будущем с поддержкой фичей языка все будет ок. Даже если вы пишете только на Kotlin, очень важно заставлять команду разработки Android поддерживать все новые версии Java, улучшать байткод и новые API.

Этот пост — письменная версия моего выступления Digging into D8 and R8.

Источник

Читайте также:  Обновление самсунг 2021 новое андроид
Оцените статью