Запись ZIP файла

Запись ZIP файла практически операция чтения наоборот:

  • для каждого файла в архиве необходимо записать LocalFileHeader, затем содержимое файла, предварительно посчитав контрольную сумму
  • для каждой записи LocalFileHeader, записываем CentralDirectoryFileHeader
  • записываем EOCD

Теперь подробнее по пунктам

Запись Local File Header

Для каждого файла в архиве существуют две записи — это LocalFileHeader и CentralDirectoryFileHeader. При записи необходимо создавать эти структуры одновременно, только LocalFileHeader записывать сразу, а для CentralDirectoryFileHeader сохранять информацию во временный список для использования ее потом. Также для данных необходимо посчитать контрольную сумму с помощью функции crc32(), которую предоставляет библиотека zlib.

Пример кода

struct LocalFileHeader
{
    uint16_t versionToExtract;
    uint16_t generalPurposeBitFlag;
    uint16_t compressionMethod;
    uint16_t modificationTime;
    uint16_t modificationDate;
    uint32_t crc32;
    uint32_t compressedSize;
    uint32_t uncompressedSize;
    uint16_t filenameLength;
    uint16_t extraFieldLength;
 
} __attribute__((packed));
 
struct FileInfo
{
    uint32_t compressedSize;
    uint32_t uncompressedSize;
    uint16_t compressionMethod;
    uint32_t crc32;
    uint32_t offset;
};
 
// Буфер для чтения
std::vector<uint8_t> readBuffer;
// Буфер для сжатых данных
std::vector<uint8_t> dataBuffer;
// Информация для создания Central directory file header
std::vector<FileInfo> fileInfoList;
// Список файлов
std::vector<std::string> filenames;
 
for (size_t i = 0; i < filenames.size(); ++i)
{
    const std::string filename = filenames[i];
 
    // Открываем файл для чтения
    std::ifstream f(filename.c_str(), std::ios::binary | std::ios::in);
 
    // Структура типа Local File Header
    LocalFileHeader lfh;
    memset(&lfh, 0, sizeof(lfh));
 
    // Узнаем размер файла
    f.seekg(0, f.end);
    lfh.uncompressedSize = f.tellg();
    f.seekg(0, f.beg);
 
    // Считаем весь файл целиком в readBuffer
    readBuffer.resize(lfh.uncompressedSize);
    f.read((char *) readBuffer.data(), lfh.uncompressedSize);
 
    // Считаем контрольную сумму
    lfh.crc32 = crc32(0, readBuffer.data(), lfh.uncompressedSize);
 
    // Выделим память для сжатых данных
    dataBuffer.resize(lfh.uncompressedSize);
 
    // Структура для сжатия данных
    z_stream zStream;
    memset(&zStream, 0, sizeof(zStream));
    deflateInit2(
        &zStream, 
        Z_BEST_SPEED,
        Z_DEFLATED,
        -MAX_WBITS,
        8,
        Z_DEFAULT_STRATEGY);
 
    // Сжимаем данные
    zStream.avail_in = lfh.uncompressedSize;
    zStream.next_in = readBuffer.data();
    zStream.avail_out = lfh.uncompressedSize;
    zStream.next_out = dataBuffer.data();
    deflate(&zStream, Z_FINISH);
 
    // Размер сжатых данных
    lfh.compressedSize = zStream.total_out;
    lfh.compressionMethod = Z_DEFLATED;
 
    // Очистка
    deflateEnd(&zStream);
 
    // Длина имени файла
    lfh.filenameLength = filename.size();
 
    // Сохраним смещение к записи Local File Header внутри архива
    const uint32_t lfhOffset = os.tellp();
 
    // Запишем сигнатуру Local File Header
    const uint32_t signature = 0x04034b50;
    os.write((char *) &signature, sizeof(signature));
 
    // Запишем Local File Header
    os.write((char *) &lfh, sizeof(lfh));
    // Запишем имя файла
    os.write(filename.c_str(), filename.size());
    // Запишем данные
    os.write((char *) dataBuffer.data(), lfh.compressedSize);
 
    // Сохраним все данные для Central directory file header
    FileInfo fileInfo;
    fileInfo.compressedSize = lfh.compressedSize;
    fileInfo.uncompressedSize = lfh.uncompressedSize;
    fileInfo.compressionMethod = lfh.compressionMethod;
    fileInfo.crc32 = lfh.crc32;
    fileInfo.offset = lfhOffset;
    fileInfoList.push_back(fileInfo);
}

Запись Central directory file header

Используя данные, полученные на предыдущем шаге, записываем для каждого файла структуру типа CentralDirectoryFileHeader. Предварительно необходимо сохранить смещение в файле для финальной записи EOCD.

Пример кода

struct CentralDirectoryFileHeader
{
    uint16_t versionMadeBy;
    uint16_t versionToExtract;
    uint16_t generalPurposeBitFlag;
    uint16_t compressionMethod;
    uint16_t modificationTime;
    uint16_t modificationDate;
    uint32_t crc32;
    uint32_t compressedSize;
    uint32_t uncompressedSize;
    uint16_t filenameLength;
    uint16_t extraFieldLength;
    uint16_t fileCommentLength;
    uint16_t diskNumber;
    uint16_t internalFileAttributes;
    uint32_t externalFileAttributes;
    uint32_t localFileHeaderOffset;
 
} __attribute__((packed));
 
// Смещение первой записи для EOCD    
const uint32_t firstOffsetCDFH = os.tellp();
 
for (int i = 0; i < filenames.size(); ++i)
{
    const std::string &filename = filenames[i];
    const FileInfo &fileInfo = fileInfoList[i];
    CentralDirectoryFileHeader cdfh;
    memset(&cdfh, 0, sizeof(cdfh));
 
    cdfh.compressedSize = fileInfo.compressedSize;
    cdfh.uncompressedSize = fileInfo.uncompressedSize;
    cdfh.compressionMethod = fileInfo.compressionMethod;
    cdfh.crc32 = fileInfo.crc32;
    cdfh.localFileHeaderOffset = fileInfo.offset;
    cdfh.filenameLength = filename.size();
 
    // Запишем сигнатуру
    const uint32_t signature = 0x02014b50;
    os.write((char *) &signature, sizeof(signature));
 
    // Запишем структуру
    os.write((char *) &cdfh, sizeof(cdfh));
 
    // Имя файла
    os.write(filename.c_str(), cdfh.filenameLength);
}
 
// Посчитаем размер данных для следующего шага
const uint32_t lastOffsetCDFH = os.tellp();

Запись EOCD

Самый простой этап — сформировать и записать структуру типа EOCD.

Пример кода

EOCD eocd;
memset(&eocd, 0, sizeof(eocd));
eocd.centralDirectoryOffset = firstOffsetCDFH;
eocd.numberCentralDirectoryRecord = filenames.size();
eocd.totalCentralDirectoryRecord = filenames.size();
eocd.sizeOfCentralDirectory = lastOffsetCDFH - firstOffsetCDFH;
 
// Пишем сигнатуру
const uint32_t signature = 0x06054b50;
os.write((char *) &signature, sizeof(signature));
 
// Пишем EOCD
os.write((char *) &eocd, sizeof(eocd));

Замечания по коду:

  • ничего не раскрыто по поводу структуру DataDescriptor (ни разу не сталкивался)
  • иногда размер сжатых данных превышает размер несжатых, поэтому выделяйте память по науке (смотри документацию zlib)
  • если размер данных очень большой, то придется воспользоваться структурой DataDescriptor
Чтение ZIP файла

Алгоритм получения данных из ZIP файла:

  • находим запись EOCD
  • загружаем записи CentralDirectoryFileHeader
  • для каждой CentralDirectoryFileHeader, находим и загружаем LocalFileHeader
  • данные располагаются сразу после LocalFileHeader, иногда размер данных записывается после самих данных в структуру DataDescriptor, об этом сигнализирует флаг generalPurposeBitFlag

Теперь каждый шаг подробнее.

Находим запись EOCD

Эта запись располагается в конце файла. Ее поиск производится от конца файла к началу по сигнатуре. Стартовое место поиска равно размеру файла минус размер структуры EOCD. Если сигнатура не найдена, то смещаемся к началу файла по одному байту за раз.

Пример кода

struct EOCD 
{
    uint16_t diskNumber;
    uint16_t startDiskNumber;
    uint16_t numberCentralDirectoryRecord;
    uint16_t totalCentralDirectoryRecord;
    uint32_t sizeOfCentralDirectory;
    uint32_t centralDirectoryOffset;
    uint16_t commentLength;
 
} __attribute__((packed));
 
// Вычисляем размер файла
is.seekg(0, is.end);
const size_t fileSize = is.tellg();
 
// Ищем сигнатуру EOCD
for (size_t offset = fileSize - sizeof(EOCD); offset != 0; --offset)
{
    uint32_t signature = 0;
 
    // Считываем четыре байта сигнатуры
    is.seekg(offset, is.beg);
    is.read((char *) &signature, sizeof(signature));
 
    if (0x06054b50 == signature)
    {
        // Есть контакт!
        break;
    }
}
 
EOCD eocd;
is.read((char *) &eocd, sizeof(eocd));
 
// Считываем комментарий, если есть
if (eocd.commentLength)
{
    // Длина комментария
    uint8_t *comment = new uint8_t[eocd.commentLength + 1];
    is.read((char *) comment, eocd.commentLength);
 
    // Выведем комментарий в консоль
    comment[eocd.commentLength] = 0;
    std::cout << comment;
 
    delete [] comment;
}

Загружаем записи Central directory file header

Смещение для поиска первой записи Central directory file header находится в EOCD, который мы загрузили на предыдущем шаге. Записи располагаются последовательно одна за другой. Загрузка происходит следующим образом: считываем поля, размер которых известен, затем загружаем остальные поля (filename, extraField, fileComment).

Пример кода

struct CentralDirectoryFileHeader
{
    uint32_t signature;
    uint16_t versionMadeBy;
    uint16_t versionToExtract;
    uint16_t generalPurposeBitFlag;
    uint16_t compressionMethod;
    uint16_t modificationTime;
    uint16_t modificationDate;
    uint32_t crc32;
    uint32_t compressedSize;
    uint32_t uncompressedSize;
    uint16_t filenameLength;
    uint16_t extraFieldLength;
    uint16_t fileCommentLength;
    uint16_t diskNumber;
    uint16_t internalFileAttributes;
    uint32_t externalFileAttributes;
    uint32_t localFileHeaderOffset;
 
} __attribute__((packed));
 
// Смещение первой записи
is.seekg(eocd.centralDirectoryOffset, is.beg);
 
for (uint16_t i = 0; i < eocd.numberCentralDirectoryRecord; ++i)
{
    CentralDirectoryFileHeader cdfh;
 
    is.read((char *) &cdfh, sizeof(cdfh));
 
    if (0x02014b50 != cdfh.signature)
    {
        // Ошибка
        break;
    }
 
    // Считываем имя файла/папки
    if (cdfh.filenameLength)
    {
        uint8_t *filename = new uint8_t[cdfh.filenameLength + 1];
        is.read((char *) filename, cdfh.filenameLength);
 
        filename[cdfh.filenameLength] = 0;
        std::cout << filename << "\n";
 
        delete [] filename;
    }
 
    // Пропускаем дополнительные поля
    if (cdfh.extraFieldLength)
    {
        is.seekg(cdfh.extraFieldLength, is.cur);
    }
 
    // Пропускаем комментарий
    if (cdfh.fileCommentLength)
    {
        is.seekg(cdfh.fileCommentLength, is.cur);
    }
 
}

Загружаем записи Local File Header

Для каждой записи типа CentralDirectoryFileHeader существует запись типа Local File Header. Ее расположение в файле описывается полем CentralDirectoryFileHeader.localFileHeaderOffset. Переходим по смещению, считываем поля известной длины, затем остальные поля (filename, extraField).

Пример кода

struct LocalFileHeader
{
    uint32_t signature;
    uint16_t versionToExtract;
    uint16_t generalPurposeBitFlag;
    uint16_t compressionMethod;
    uint16_t modificationTime;
    uint16_t modificationDate;
    uint32_t crc32;
    uint32_t compressedSize;
    uint32_t uncompressedSize;
    uint16_t filenameLength;
    uint16_t extraFieldLength;
 
} __attribute__((packed));
 
// Переходим к началу записи
is.seekg(cdfh.localFileHeaderOffset, is.beg);
 
// Считываем ее целиком
LocalFileHeader lfh;
is.read((char *) &lfh, sizeof(lfh));
 
if (0x04034b50 != lfh.signature)
{
    // Ошибка
    break;
}
 
// Пропускаем название файла
is.seekg(lfh.filenameLength, is.cur);
// Пропускаем дополнительную информацию
is.seekg(lfh.extraFieldLength, is.cur);

Загружаем данные

Для этого шага нам понадобится библиотека zlib, потому что данные сжимаются методом deflate. Как правило, сразу после LocalFileHeader идут данные размером compressedSize. Если данные сжаты, то флаг compressionMethod равен 8 (или Z_DEFLATED), иначе он нулевой (или Z_STORED). Сжатые данные необходимо считать во временный буфер размером compressedSize и распаковать с помощью библиотеки zlib в буфер размером uncompressedSize.

// Выделяем буфер для чтения данных
uint8_t *readBuffer = new uint8_t[lfh.compressedSize];
// Считываем данные в буфер
is.read((char *) readBuffer, lfh.compressedSize);
 
// Если сжатие не применяется
if (0 == lfh.compressionMethod)
{
    // То необходимые данные уже лежат в буфере для чтения
}
// Данные сжаты методом deflate
else if (Z_DEFLATED == lfh.compressionMethod)
{
    // Выделяем буфер для распакованных данных
    uint8_t *result = new uint8_t[lfh.uncompressedSize];
 
    // Инициализация структуры для распаковки
    z_stream zs;
    memset(&zs, 0, sizeof(zs));
    inflateInit2(&zs, -MAX_WBITS);
 
    // Размер входного буфера
    zs.avail_in = lfh.compressedSize;
    // Входной буфер
    zs.next_in = readBuffer;
    // Размер выходного буфера
    zs.avail_out = lfh.uncompressedSize;
    // Выходной буфер
    zs.next_out = result;
 
    // Распаковка
    inflate(&zs, Z_FINISH);
 
    // Очищаем структуру для распаковки
    inflateEnd(&zs);
 
    // Удаляем буфер для распакованных данных
    delete [] result;
}
// Удаляем буфер для чтения
delete [] readBuffer;

Общие комментарии по коду

  • странная конструкция __attribute__((packed)) всего лишь означает команду для GCC не использовать выравнивание внутри структуры, для Visual Studio читайте про #pragma push(pack/pop)
  • обратите внимание, что поток типа z_stream инициализируется с помощью функции inflateInit2(). Это сделано потому, что стандартная функция inflateInit() ожидает в начале потока данных специальный заголовок, который отсутствует в ZIP файле
  • is — это объект типа std::istream (например std::ifstream)
  • не стоит выделять буфер для чтения каждый раз, лучше вычислить его заранее и выделить один раз в начале программы
  • проверьте метод сжатия перед чтением данных, тогда при отсутствии сжатия, можно читать данные сразу в нужный буфер
  • если размер файла внутри ZIP архива неизвестен и может быть очень большим, то распаковывайте его частями
  • структуру типа z_stream можно инициализировать и удалять только один раз, а перед работой с ней вызвать функцию inflateReset2()
  • в коде совсем не учитывается порядок байтов, так что будьте внимательны при переносе на другую платформу

P.S. Есть способ чтения быстрее и короче: с начала файла считываем все структуры типа LocalFileHeader, проверяя сигнатуру, если сигнатуры неправильная, значит все файлы считаны.

Описание формата ZIP файла. Часть 1

ZIP файл состоит из трех областей:

  • сжатые/несжатые данные, (последовательность структур типа LocalFileHeader, сами данные и необязательных DataDescriptor)
  • центральный каталог (последовательность структур CentralDirectoryFileHeader)
  • описание центрального каталога (End of central directory record (EOCD))

С начала файла идет набор из LocalFileHeader, непосредственно данные и (необязательно) структура Data descriptor. Затем структуры типа CentralDirectoryFileHeader для каждого файла и папки в ZIP архиве и завершает все это структура End of central directory record.

Local File Header

Используется для описания метаданных файла (имя файла, контрольная сумма, время и дата модификации, сжатый/несжатый размер). Как правило сразу после этой структуры следует содержимое файла.

LocalFileHeader

struct LocalFileHeader
{
    // Обязательная сигнатура, равна 0x04034b50
    uint32_t signature;
    // Минимальная версия для распаковки
    uint16_t versionToExtract;
    // Битовый флаг
    uint16_t generalPurposeBitFlag;
    // Метод сжатия (0 - без сжатия, 8 - deflate)
    uint16_t compressionMethod;
    // Время модификации файла
    uint16_t modificationTime;
    // Дата модификации файла
    uint16_t modificationDate;
    // Контрольная сумма
    uint32_t crc32;
    // Сжатый размер
    uint32_t compressedSize;
    // Несжатый размер
    uint32_t uncompressedSize;
    // Длина название файла
    uint16_t filenameLength;
    // Длина поля с дополнительными данными
    uint16_t extraFieldLength;
    // Название файла (размером filenameLength)
    uint8_t *filename;
    // Дополнительные данные (размером extraFieldLength)
    uint8_t *extraField;
};

Сразу после этой структуры идут данные размером compressedSize при использовании сжатия или размером uncompressedSize в противном случае.

Иногда бывает невозможно вычислить данные на момент записи LocalFileHeader, тогда в crc32, compressedSize и uncompressedSize записываются нули, третий бит в generalPurposeBitFlag ставится в единицу, а после LocalFileHeader добавляется структура типа DataDescriptor.

Data descriptor

Если по какой-то причине содержимое файла невозможно создать одновременно с заголовком типа LocalFileHeader, то сразу после него следует структура DataDescriptor, где идет находится дополнение метаданных для LocalFileHeader (контрольная сумма, сжатый/несжатый размер).

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

DataDescriptor

struct DataDescriptor
{
    // Необязательная сигнатура, равна 0x08074b50
    uint32_t signature;
    // Контрольная сумма
    uint32_t crc32;
    // Сжатый размер
    uint32_t compressedSize;
    // Несжатый размер
    uint32_t uncompressedSize;
};

Central directory file header

Расширенное описание метаданных файла. Содержит дополненную версию LocalFileHeader (добавляются поля номер диска, файловые атрибуты, смещение до LocalFileHeader от начала ZIP файла).

CenterDirectoryFileHeader

struct CentralDirectoryFileHeader
{
    // Обязательная сигнатура, равна 0x02014b50 
    uint32_t signature;
    // Версия для создания
    uint16_t versionMadeBy;
    // Минимальная версия для распаковки
    uint16_t versionToExtract;
    // Битовый флаг
    uint16_t generalPurposeBitFlag;
    // Метод сжатия (0 - без сжатия, 8 - deflate)
    uint16_t compressionMethod;
    // Время модификации файла
    uint16_t modificationTime;
    // Дата модификации файла
    uint16_t modificationDate;
    // Контрольная сумма
    uint32_t crc32;
    // Сжатый размер
    uint32_t compressedSize;
    // Несжатый размер
    uint32_t uncompressedSize;
    // Длина название файла
    uint16_t filenameLength;
    // Длина поля с дополнительными данными
    uint16_t extraFieldLength;
    // Длина комментариев к файлу
    uint16_t fileCommentLength;
    // Номер диска
    uint16_t diskNumber;
    // Внутренние аттрибуты файла
    uint16_t internalFileAttributes;
    // Внешние аттрибуты файла
    uint32_t externalFileAttributes;
    // Смещение до структуры LocalFileHeader
    uint32_t localFileHeaderOffset;
    // Имя файла (длиной filenameLength)
    uint8_t *filename;
    // Дополнительные данные (длиной extraFieldLength)
    uint8_t *extraField;
    // Комментарий к файла (длиной fileCommentLength)
    uint8_t *fileComment;
};

End of central directory record (EOCD)

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

EOCD

struct EOCD
{
    // Обязательная сигнатура, равна 0x06054b50
    uint32_t signature;
    // Номер диска
    uint16_t diskNumber;
    // Номер диска, где находится начало Central Directory
    uint16_t startDiskNumber;
    // Количество записей в Central Directory в текущем диске
    uint16_t numberCentralDirectoryRecord;
    // Всего записей в Central Directory
    uint16_t totalCentralDirectoryRecord;
    // Размер Central Directory
    uint32_t sizeOfCentralDirectory;
    // Смещение Central Directory
    uint32_t centralDirectoryOffset;
    // Длина комментария
    uint16_t commentLength;
    // Комментарий (длиной commentLength)
    uint8_t *comment;
};

Папки в ZIP файле представлены двумя структурами LocalFileHeader и CentralDirectoryFileHeader с нулевым размером и контрольной суммой. Название папки заканчивается слешем «/».

Конструктор, деструктор и автоматический стек в C++

С удивлением узнал об интересном способе использования стека в нестандартных целях. Посмотрите внимательно на код ниже.

Его фишка в том, что класс Sample в конструкторе сохраняет предыдущий инстанс типа Sample, а в деструкторе — восстанавливает. При этом метод Sample::instance() всегда будет возвращать текущий объект типа Sample.

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

class Sample
{
public:
    Sample()
        : prevInst_s(curInst_s)
    {
        curInst_s = this;
    }
 
    ~Sample()
    {
        curInst_s = prevInst_s;
    }
 
    static Sample *instance()
    {
        return curInst_s;
    }
 
private:
    Sample *prevInst_s;
    static Sample *curInst_s;
};
 
Sample *Sample::curInst_s = NULL;

Пример использования такого функционала под катом. (в комментариях указано — какой instance сейчас текущий)

#include <stdio.h>
 
class Sample
{
public:
    Sample(const char *name)
        : name_(name)
        , prevInst_s(curInst_s)
    {
        curInst_s = this;
        printf("current: %s\n", name_);
    }
 
    ~Sample()
    {
        curInst_s = prevInst_s;
 
        if (curInst_s)
        {
            printf("restore: %s\n", curInst_s->name_);
        }
    }
 
    static Sample *instance()
    {
        return curInst_s;
    }
 
private:
    const char *name_;
    Sample *prevInst_s;
    static Sample *curInst_s;
};
 
Sample *Sample::curInst_s = NULL;
 
 
int main()
{
    /*
     *
     * Sample::instance() => NULL
     *
     */
    {
        Sample sampleA("A");
        /*
         *
         * Sample::instance() => A
         *
         */
        {
            Sample sampleB("B");
            /*
             *
             *  Sample::instance() => B
             *
             */
        }
        /* 
         * 
         * Sample::instance() => A
         *
         */
        {
            Sample sampleC("C");
            /*
             *
             * Sample::instance() => C
             *
             */
            {
                Sample sampleD("D");
                /*
                 *
                 * Sample::instance() => D
                 *
                 */
            }
            /*
             *
             * Sample::instance() => C
             *
             */
        }
        /*
         *
         * Sample::instance() => A
         *
         */
    }
    /*
     *
     * Sample::instance() => NULL
     *
     */
}
Решение проблемы с нехваткой памяти при загрузке текстур в 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() после загрузки изображения в текстуру, дабы освободить нативную память.

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