Узнайте, как нативный код лучше влияет на производительность вашего приложения
Производительность является характеристикой большинства программных продуктов, но есть несколько программ, которые более чувствительны к производительности, чем другие. Я работаю над приложением Android Camera, и моя команда уделяет большое внимание производительности.
[Google обнаружил, что] создание страницы с 10 результатами заняло 0,4 секунды. Страница с 30 результатами заняла 0,9 секунды. Задержка в полсекунды привела к падению трафика на 20%. Задержка в полсекунды убила удовлетворенность пользователей.
Мне не нужно преувеличивать важность performance для моего менеджера или для вас, но я наткнулся на этот фрагмент, который усиливает конструкцию производительность — это функция — полезно знать факты.
Если вы пишете приложения, которые обрабатывают большие изображения, снятые камерой, или уже существующее изображение на устройстве, вам нужно быть особенно осторожным. В наши дни камеры на телефонах легко оснащаются сенсорами с высоким разрешением. Легко найти 13-мегапиксельные (мегапиксельные), 24-мегапиксельные, 48-мегапиксельные или даже 108-мегапиксельные камеры, которые сейчас поставляются на устройствах Android.
Давайте посмотрим на 13-мегапиксельное изображение. Он имеет 13 000 000 пикселей. Если вы хотите сделать только одно простое вычисление на изображении, скажем, увеличить экспозицию изображения, т.е. для каждого пикселя.
image(x, y) = std::clamp(alpha * image(x, y) + beta, 0, 255);
Вам нужно сделать это 13 миллионов раз.
Смартфоны в наши дни также оснащены многоядерными процессорами с поддержкой SIMD, поэтому есть способы сделать это быстрее, чем сериализованные 13 миллионов итераций, но в то же время типы алгоритмов, которые мы хотим запустить, обычно намного сложнее, чем тот, который я только что заявил.
По моему опыту, проще и лучше обрабатывать эти сложные операции обработки изображений с помощью собственного кода, особенно для поддержания его производительности.
Это очень простая статья, демонстрирующая, как выполнять обработку изображений с помощью собственного кода в Android. Я также покажу на примере, что производительность очень простого и неоптимизированного кода C++ очень близка к достаточно оптимизированному коду Java для той же постановки задачи. Если вы ищете «Быстрая обработка изображений с помощью Java Native Interface или JNI в Android» — я думаю, вы попали по адресу, эта статья поможет вам в этом.
Пример постановки задачи: преобразование YUV в RGB
Постановка задачи заключается в преобразовании 8-мегапиксельного (3264x2448) изображения в определенный формат под названием YUV_420_888, который имеет один планарный Y канал и два полуплоскостных субдискретизированных UV канала в формат ARGB_8888, который обычно поддерживается с Bitmap в Android. Подробнее о формате YUV можно прочитать в Википедии. Кроме того, приведенные ниже статьи содержат лучшее описание постановки задачи.
Причина, по которой я выбрал это в качестве формулировки проблемы, заключается в том, что YUV_420_888 является одним из наиболее распространенных форматов OUTPUT, поддерживаемых API-интерфейсами Android Camera, а изображения обычно используются как Bitmap в Android, что делает это довольно распространенной формулировкой проблемы для решения.
Я экспериментировал с производительностью различных фреймворков или технологий, чтобы понять производительность обработки изображений в Android, взяв это за постановку проблемы. Вот несколько примеров того же с использованием других методов, которые я тестировал:
- Как использовать RenderScript для преобразования изображения YUV_420_888 YUV в растровое изображение
- Ускоренная обработка изображений в Android Java с использованием многопоточности
Я планирую написать целую серию статей, объясняющих эффективность различных подходов. Вот цифры для подходов, опубликованных до сих пор:

Обработка изображений с помощью собственного кода
Если бы мы работали непосредственно в родном пространстве, базовая скелетная программа выглядела бы так:
Функция принимает значение канала y, u и v для определенного пикселя и возвращает соответствующее значение RGBA_8888.
Программы для Android по умолчанию написаны на языке Java или Kotlin, но инструментальные цепочки Android поставляются с родным комплектом разработки под названием Android NDK, который позволяет вам реализовывать разделы вашего приложения с использованием таких языков, как C и C++. Документация Android описывает, что NDK полезен для двух сценариев:
Выжимайте из устройства дополнительную производительность, чтобы снизить задержки или запускать ресурсоемкие приложения, такие как игры или физические симуляторы.
Повторно используйте свои собственные или библиотеки C или C++ других разработчиков.
В следующих нескольких разделах я попытаюсь вкратце объяснить, как преобразовать Java-изображение в формате YUV_420_888 в Java-объект Bitmap, используя интеграцию с собственным кодом.
Итак, в основном мы должны заполнить эту скелетную функцию Java:
Прежде чем я углублюсь, я хотел бы познакомить вас с JNI.
JNI
JNI расшифровывается как Java Native Interface. Он определяет способ взаимодействия байтового кода, скомпилированного из кода Java или Kotlin, с собственным кодом, написанным на C или C++. Как следует из названия, это помогает нам связать код Java с собственным кодом.
Если вы в конечном итоге будете больше работать с JNI, я бы порекомендовал прочитать советы Android по JNI — Советы JNI.
Начало работы с NDK и JNI
Чтобы избежать избыточности, я бы порекомендовал проверить и попробовать следующие статьи, чтобы настроить свое первое приложение для Android на основе JNI. Эти статьи также помогут вам настроить необходимые наборы инструментов для компиляции приложений Android вместе с NDK и собственным кодом.
Дайте мне знать в комментариях, если эти статьи недостаточно ясны. Перейдите к следующему разделу, если вы успешно создали и запустили приложение JNI или уже знакомы с конструкциями.
Собственный код для преобразования YUV в Bitmap
Создайте библиотеку и исходный файл yuv2rgb.h/cc в app/src/main/cpp.
Заголовочный файл
Исходный файл
В идеале для этого также следует написать модульный тест, но это выходит за рамки данной статьи. Вы можете прочитать больше о добавлении Native Tests в документации Android.
Далее мы напишем слой JNI, чтобы связать его со скелетом Java, с которого мы начали.
Интеграция Java + JNI
Теперь вам нужен файл JNI для подключения библиотеки Java к собственной библиотеке. Давайте добавим yuv2rgb-jni.cc в app/src/main/cpp.
Кроме того, предположим, что наша скелетная функция Java находится в пакете com.example.myproject и в статическом классе с именем YuvConvertor, ваш файл JNI должен выглядеть так.
Важное примечание. Здесь важны имя пакета, имя класса и имя собственного метода, поскольку они используются средой выполнения для вызова нужной встроенной функции. См. название метода в приведенном ниже коде JNI, чтобы получить больше информации.
И, наконец, вызовите это из библиотеки Java
Вот несколько важных моментов, на которые следует обратить внимание:
- Используя API-интерфейсы NDK, мы можем напрямую получить доступ к содержимому ByteBuffer в нативном коде, который одновременно очень полезен и опасен, если не обрабатывается правильно.
- Существуют NDK API для Bitmap, которые можно напрямую использовать в этом случае, что может помочь уменьшить дополнительное выделение памяти, мы можем напрямую обновлять память Bitmap из собственного кода.
Таким образом, у вас есть код Java, вызывающий JNI, который извлекает указатель на входные и выходные данные в собственном формате и передает их в собственную библиотеку для обработки. Нативная библиотека представляет собой довольно распространенный код C++ и может использоваться и за пределами Android.
Ваш make-файл (в данном случае он должен быть app/src/main/cpp/CMakeLists.txt) должен быть правильно настроен для поддержки сборки нативного кода с помощью Android APK. Для этого примера он должен иметь по крайней мере эти определения.
Производительность
При рассмотрении производительности имейте в виду, что это довольно простая форма кода C++, в ней явно не используются преимущества многопоточности или наборов инструкций SIMD, которые могут работать на устройствах Android. В лучшем случае некоторая часть кода оптимизируется компилятором (например, основной цикл for может автоматически векторизоваться). Код был скомпилирован с флагом оптимизации -O3.
Для изображения 8MP (3264x2448) этот код занимает около 76.4ms на том же эталонном устройстве.

Здесь вы можете видеть, что производительность этой версии нативного кода в 1,4 раза ниже, чем у очень оптимизированного многопоточного Java-кода, что не является плохой новостью. Стартовый Java-код потребовал 353 ms для запуска того же алгоритма (хотя, вероятно, у него есть причины, связанные с тем, что сложный ByteBuffer не дает прямого доступа к массиву — подробнее). Чтобы узнать больше о написании более оптимизированного нативного кода или воспользоваться преимуществами компилятора для языковых конструкций, я бы рекомендовал прочитать эту статью — Руководство компилятора C++ по автоматической векторизации кода
Заключительные примечания
Я бы еще раз повторил, что нативный код отлично подходит для запуска алгоритмов с интенсивными вычислениями на Android. И большинство алгоритмов обработки изображений обычно подпадают под эту категорию, вероятно, из-за необходимости обработки большого количества пикселей и, следовательно, большого количества итераций.
В этой же серии экспериментов, используя многопоточность, NEON API (SIMD) и некоторый ассемблерный код, я смог снизить задержку до 12.1 ms, что является победителем в этой серии (спойлер). Однако его довольно сложно как писать, так и поддерживать, и решение на основе Halide, которое требует около 28ms для одной и той же постановки задачи, является даже идеальным решением с точки зрения производительности, обслуживания и простоты написания.
Я напишу об обоих из них в моем следующем наборе статей, спасибо за чтение. Следите за обновлениями!!
Want to Connect With the Author? This article was originally published at https://blog.minhazav.dev.