Решение проблемы с нехваткой памяти при загрузке текстур в 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 рендера.

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

Личный блог Евгения Жирнова