Решение проблемы с нехваткой памяти при загрузке текстур в Android

Как известно, в популярной нынче мобильной ОС Android есть фатальный недостаток — это язык Java. Более того, каждому приложению выделяется кусок памяти размером от 32-х мегабайт до бесконечности в зависимости от желания производителя мобильного телефона.

Если вы делаете большое приложение с использованием OpenGLES и пачкой больших текстур размером 2048×2048 (а это 16 мегабайт для формата RGBA), то памяти для загрузки у вас не будет. После загрузки второй-третьей, пятой текстуры с помощью BitmapFactory приложение упадет с OutOfMemoryError, что есть очень плохо.

Хочу поделиться решением этой проблемы. Она состоит из двух пунктов: загружать изображения из PNG с помощью libpng и исключить работу с большими блоками данных из Java части.

Решение первой части и исходники для прямой загрузки изображения из PNG файла можно найти на stackoverflow.com.

Для работы с памятью создадим вот такой Java класс:

package com.example.memory;
 
import java.nio.IntBuffer;
 
public class MemoryManager {
    public static native IntBuffer create(int sizeInBytes);
 
 
    public static native void release(IntBuffer buffer);
}

И нативную часть (язык cpp):

#include <jni.h>
 
#ifdef __cplusplus
extern "C" {
#endif
JNIEXPORT jobject JNICALL Java_com_example_memory_MemoryManager_create(
    JNIEnv *env,
    jclass cls,
    jint sizeInBytes)
{
    // Выделяем блок памяти
    void *buf = malloc(sizeInBytes);
    // Создаем буфер 
    return env->NewDirectByteBuffer(buf, 0);
}
 
JNIEXPORT void JNICALL Java_com_example_memory_MemoryManager_release(
    JNIEnv *env,
    jclass cls,
    jobject buffer)
{
    // Освобождаем память
    free(env->GetDirectBufferAddress(buffer));
}
#ifdef __cplusplus
}
#endif

Далее алгоритм работы такой:

  1. Читаем высоту и ширины изображения из первых 24 байт PNG (подробности в статье на хабре: Получаем тип и размеры изображения без скачивания его целиком, используя Python)
  2. Выделяем IntBuffer с помощью MemoryManager размером W*H*4 (32bit RGBA, 4 байта на пиксель)
  3. Считываем в него PNG файл
  4. Загружаем эти данные в текстуру с помощью glTexImage2d
  5. Освобождаем IntBuffer в MemoryManager

Честно говоря, я слабо понимаю — почему в нативе вызывается функция NewDirectByteBuffer, а в Java приходит IntBuffer, а не ByteBuffer. Загадка какая-то.

UPD: Если вы решили пойти стандартным путем с загрузкой Bitmap и GLUtil.texImage2D, то не забывайте вызывать bitmap.recycle() после загрузки изображения в текстуру, дабы освободить нативную память.

Социальные нормы потребления электричества

Отличную идею придумало правительство во главе с незабвенным нашим Дмитрием Анатольевичем — социальные нормы потребления электричества. Сама идея проста как дважды два — смотрим среднее потребление электричества по региону, берем за него текущую цену, а остальное, что народ нажжет, в полтора-четыре раза больше.

По замыслу этих славных товарищей, бесконечно далеких от народа, большая часть людей будет получать электричество по обычной цене, а остальные за тройной счет. Ибо нефиг электричество тратить, мало его осталось, на всех не хватает. Не ты, чувак, и не твои предки электростанции строили, а лично Чубайс собака в поте лица вкалывал по 30 часов в сутки сразу после войны под чутким руководством Сталина, затем Хрущева, а после Брежнева с Горбачевым.

Социальные нормы потребления электричества

Как определять пайку каждому человеку российскому, чтоб не обидно никому было — непонятно. Идея-то пришла, а палец, откуда они эту идею высосали, кончился, видимо. Напрягли тогда тугой череп власть держащие и решили поставить ЭКСПЕРИМЕНТ над народом: установить норму потребления электричества в 50-70КВт на пару месяц в самых нищих районах России-матушки и посмотреть кто сколько потребляет.

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

ПРАВИЛЬНО! Он будет экономить на всем, вплоть до зажигания лучин и сидения в полной темноте по вечерам.

Представим, что будет дальше: эксперимент прошел, наверх ушла бумага о потреблении электричества в данном регионе. Правительство посмотрит на отчет и подумает — АГА, тут потребление электричества всего 10КВт на душу. Почешет задумчиво нимб и сделает 10КВт для всей России нормой. Ловко, а? Ну это же профит в чистом виде!

Поэтому, уважаемые жители тех регионов, где проводят сейчас этот эксперимент — жгите как можно больше электричества! Я вас очень прошу! Остальные регионы будут вам только благодарны.

Основы библиотеки PIXI.js

Библиотека PIXI.js предназначена для вывода 2D графики с помощью javascript. Она может работать с двумя типами рендеров: Canvas и WebGL.

В этой библиотеке вся сцена представлена деревом элементов, которые присоединены к объекту типа Stage. Это root для всех элементов сцены. Отрисовка начинается с него и проходит по всем детям этого объекта в прямом порядке.

Базовый «кирпичик» сцены — объект типа DisplayObject. Он содержит: 2D позицию, поворот, масштаб, прозрачность, цвет.

Набор элементов типа DisplayObject можно объединить с помощью DisplayObjectContainer. Это наследник от класса DisplayObject с поддержкой добавления и удаления детей.

Примитивное создание сцены PIXI.js выглядит так:

function createScene() {
    var stage = new PIXI.Stage();
    var container = new PIXI.DisplayObjectContainer();
    var object = new PIXI.DisplayObject();
 
    container.addChild(object);
    stage.addChild(container);
 
    return stage;
}

Класс DisplayObject, по сути, бесполезно использовать напрямую, он не содержит никакой визуальной информации. Самый часто используемый класс на базе DisplayObject — это класс Sprite. Он представляет собой наследника от DisplayObject и добавляет к нему текстуру и выбор режима отрисовки этой текстуры.

Для задания текстуры используется класс BaseTexture. Это другой базовый «кирпичик» библиотеки PIXI.js. Он является простой оберткой над изображением. Содержит в себе размер этого изображения, ссылку на источник и загруженные данные типа Image.

Таким образом создание простой сцены теперь выглядит так:

function createScene() {
    var stage = new PIXI.Stage();
    var texture = new PIXI.BaseTexture("image.png");
    var sprite = new PIXI.Sprite(texture);
    stage.addChild(sprite);
 
    return stage;
}

Следующим по списку, но не по значению, идет класс Texture. Он содержит в себе ссылку на BaseTexture и прямоугольник, который задает видимую область BaseTexture.

С помощью класса Texture можно создавать такой полезный объект как текстурный атлас. Для этого необходимо загрузить изображение с помощью BaseTexture, а затем создать на его основе множество мелких объектов типа Texture со ссылкой на базовую текстуру и прямоугольник. Таким образом достигается уменьшение объема используемой памяти и увеличение скорости работы.

function createTextures() {
    // загружаем изображение размером 1024x1024
    var atlas = new PIXI.BaseTexture("atlas.png");
    // левая верхняя часть размером 512x512
    var textureLT = new PIXI.Texture(atlas, new Rectangle(0, 0, 512, 512);
    // правая верхняя часть размером 512x512
    var textureRT = new PIXI.Texture(atlas, new Rectangle(512, 0, 512, 512);
    // левая нижняя часть размером 512x512
    var textureLB = new PIXI.Texture(atlas, new Rectangle(0, 512, 512, 512);
    // правая нижняя часть размером 512x512
    var textureRB = new PIXI.Texture(atlas, new Rectangle(512, 512, 512, 512);
}

В текстурный атлас при желании можно сохранить всю сцену, которая будет рисоваться за один проход WebGL рендера. Живой пример (без исходников) можно посмотреть справа, виджет YoWindow использует именно такой подход для быстрой отрисовки ландшафта.

На этой радостной ноте закончу описание PIXI.js. Спасибо за внимание. Как всегда ваши комментарии и угрозы вопросы вы можете оставлять прямо здесь.

Блог Евгения Жирнова