Как полностью разобрать заархивированный файл из ОЗУ? - PullRequest
4 голосов
/ 07 мая 2020

Фон

Мне нужно проанализировать некоторые zip-файлы различных типов (получение содержимого некоторых внутренних файлов для той или иной цели, включая получение их имен).

Некоторые файлы недоступны по пути к файлу, поскольку Android имеет Uri для доступа к ним, а иногда zip-файл находится внутри другого zip-файла. С pu sh для использования SAF в некоторых случаях еще менее возможно использовать путь к файлу.

Для этого у нас есть 2 основных способа обработки: ZipFile class и ZipInputStream class.

Проблема

Когда у нас есть путь к файлу, ZipFile - идеальное решение. Он также очень эффективен с точки зрения скорости.

Однако в остальных случаях ZipInputStream может вызвать проблемы, такие как этот , у которого есть проблемы с c zip-файлом, и вызвать это исключение:

  java.util.zip.ZipException: only DEFLATED entries can have EXT descriptor
        at java.util.zip.ZipInputStream.readLOC(ZipInputStream.java:321)
        at java.util.zip.ZipInputStream.getNextEntry(ZipInputStream.java:124)

То, что я пробовал

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

Итак, я обнаружил, что Apache имеет красивый, чистый Java библиотека ( здесь ) для анализа Zip-файлов, и по какой-то причине ее решение InputStream (называемое «ZipArchiveInputStream») кажется даже более эффективным, чем собственный класс ZipInputStream.

В отличие от того, что есть в собственном фреймворке, библиотека предлагает немного больше гибкости. Я мог бы, например, загрузить весь zip-файл в массив байтов и позволить библиотеке обрабатывать его как обычно, и это работает даже для проблемных c Zip-файлов, о которых я упоминал:

org.apache.commons.compress.archivers.zip.ZipFile(SeekableInMemoryByteChannel(byteArray)).use { zipFile ->
    for (entry in zipFile.entries) {
      val name = entry.name
      ... // use the zipFile like you do with native framework

gradle dependency:

// http://commons.apache.org/proper/commons-compress/ https://mvnrepository.com/artifact/org.apache.commons/commons-compress
implementation 'org.apache.commons:commons-compress:1.20'

К сожалению, это не всегда возможно, потому что это зависит от того, что кучная память хранит весь zip-файл, а на Android он становится еще более ограниченным, потому что размер кучи может быть относительно небольшим (размер кучи может составлять 100 МБ, а размер файла - 200 МБ). В отличие от P C, в котором может быть установлена ​​огромная кучная память, для Android он совсем не гибкий.

Итак, я искал решение с JNI вместо этого, чтобы иметь все ZIP файл загружается туда в байтовый массив, не идя в кучу (по крайней мере, не полностью). Это может быть более приятным обходным путем, потому что, если бы ZIP-файл мог поместиться в ОЗУ устройства, а не в куче, это могло бы помешать мне добраться до OOM, а также не нужно было бы иметь дополнительный файл.

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

РЕДАКТИРОВАТЬ: видя, что я не могу найти ни одной библиотеки и какого-либо встроенного класса, я сам попытался использовать JNI. К сожалению, мне это очень надоело, и я посмотрел на старый репозиторий, который давно сделал go для выполнения некоторых операций с растровыми изображениями ( здесь ). Вот что я придумал:

native-lib. cpp

#include <jni.h>
#include <android/log.h>
#include <cstdio>
#include <android/bitmap.h>
#include <cstring>
#include <unistd.h>

class JniBytesArray {
public:
    uint32_t *_storedData;

    JniBytesArray() {
        _storedData = NULL;
    }
};

extern "C" {
JNIEXPORT jobject JNICALL Java_com_lb_myapplication_JniByteArrayHolder_allocate(
        JNIEnv *env, jobject obj, jlong size) {
    auto *jniBytesArray = new JniBytesArray();
    auto *array = new uint32_t[size];
    for (int i = 0; i < size; ++i)
        array[i] = 0;
    jniBytesArray->_storedData = array;
    return env->NewDirectByteBuffer(jniBytesArray, 0);
}
}

JniByteArrayHolder.kt

class JniByteArrayHolder {
    external fun allocate(size: Long): ByteBuffer

    companion object {
        init {
            System.loadLibrary("native-lib")
        }
    }
}
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        thread {
            printMemStats()
            val jniByteArrayHolder = JniByteArrayHolder()
            val byteBuffer = jniByteArrayHolder.allocate(1L * 1024L)
            printMemStats()
        }
    }

    fun printMemStats() {
        val memoryInfo = ActivityManager.MemoryInfo()
        (getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager).getMemoryInfo(memoryInfo)
        val nativeHeapSize = memoryInfo.totalMem
        val nativeHeapFreeSize = memoryInfo.availMem
        val usedMemInBytes = nativeHeapSize - nativeHeapFreeSize
        val usedMemInPercentage = usedMemInBytes * 100 / nativeHeapSize
        Log.d("AppLog", "total:${Formatter.formatFileSize(this, nativeHeapSize)} " +
                "free:${Formatter.formatFileSize(this, nativeHeapFreeSize)} " +
                "used:${Formatter.formatFileSize(this, usedMemInBytes)} ($usedMemInPercentage%)")
    }

Это кажется неправильным, потому что если я попытаюсь создать массив байтов размером 1 ГБ, используя jniByteArrayHolder.allocate(1L * 1024L * 1024L * 1024L), он выйдет из строя без каких-либо исключений или журналов ошибок.

Вопросы

  1. Можно ли использовать JNI для библиотеки Apache, чтобы он обрабатывал содержимое ZIP-файла, которое содержится в «мире» JNI?

  2. Если да, то как это сделать? Есть ли пример того, как это сделать? Есть ли для этого класс? Или надо самому реализовать? Если да, то не могли бы вы показать, как это делается в JNI?

  3. Если это невозможно, как еще можно это сделать? Может быть альтернатива тому, что есть у Apache?

  4. Почему решение JNI не работает? Как я могу эффективно скопировать байты из потока в байтовый массив JNI (я предполагаю, что это будет через буфер)?

Ответы [ 2 ]

1 голос
/ 13 мая 2020

Я взглянул на опубликованный вами код JNI и внес пару изменений. В основном это определение аргумента размера для NewDirectByteBuffer и использование malloc().

Вот результат журнала после выделения 800 МБ:

D / AppLog: всего: 1,57 ГБ свободно: 1,03 ГБ использовано: 541 МБ (34%)
D / AppLog: всего: 1,57 ГБ свободно: 247 МБ использовано: 1,32 ГБ (84%)

А вот как выглядит буфер после выделения. Как видите, отладчик сообщает об ограничении в 800 МБ, чего мы и ожидали.

enter image description here Мой C очень ржавый, поэтому я уверен, что есть некоторые работа должна быть сделана. Я обновил код, чтобы сделать его более надежным и освободить память.

native-lib. cpp

extern "C" {
static jbyteArray *_holdBuffer = NULL;
static jobject _directBuffer = NULL;
/*
    This routine is not re-entrant and can handle only one buffer at a time. If a buffer is
    allocated then it must be released before the next one is allocated.
 */
JNIEXPORT
jobject JNICALL Java_com_example_zipfileinmemoryjni_JniByteArrayHolder_allocate(
        JNIEnv *env, jobject obj, jlong size) {
    if (_holdBuffer != NULL || _directBuffer != NULL) {
        __android_log_print(ANDROID_LOG_ERROR, "JNI Routine",
                            "Call to JNI allocate() before freeBuffer()");
        return NULL;
    }

    // Max size for a direct buffer is the max of a jint even though NewDirectByteBuffer takes a
    // long. Clamp max size as follows:
    if (size > SIZE_T_MAX || size > INT_MAX || size <= 0) {
        jlong maxSize = SIZE_T_MAX < INT_MAX ? SIZE_T_MAX : INT_MAX;
        __android_log_print(ANDROID_LOG_ERROR, "JNI Routine",
                            "Native memory allocation request must be >0 and <= %lld but was %lld.\n",
                            maxSize, size);
        return NULL;
    }

    jbyteArray *array = (jbyteArray *) malloc(static_cast<size_t>(size));
    if (array == NULL) {
        __android_log_print(ANDROID_LOG_ERROR, "JNI Routine",
                            "Failed to allocate %lld bytes of native memory.\n",
                            size);
        return NULL;
    }

    jobject directBuffer = env->NewDirectByteBuffer(array, size);
    if (directBuffer == NULL) {
        free(array);
        __android_log_print(ANDROID_LOG_ERROR, "JNI Routine",
                            "Failed to create direct buffer of size %lld.\n",
                            size);
        return NULL;
    }
    // memset() is not really needed but we call it here to force Android to count
    // the consumed memory in the stats since it only seems to "count" dirty pages. (?)
    memset(array, 0xFF, static_cast<size_t>(size));
    _holdBuffer = array;

    // Get a global reference to the direct buffer so Java isn't tempted to GC it.
    _directBuffer = env->NewGlobalRef(directBuffer);
    return directBuffer;
}

JNIEXPORT void JNICALL Java_com_example_zipfileinmemoryjni_JniByteArrayHolder_freeBuffer(
        JNIEnv *env, jobject obj, jobject directBuffer) {

    if (_directBuffer == NULL || _holdBuffer == NULL) {
        __android_log_print(ANDROID_LOG_ERROR, "JNI Routine",
                            "Attempt to free unallocated buffer.");
        return;
    }

    jbyteArray *bufferLoc = (jbyteArray *) env->GetDirectBufferAddress(directBuffer);
    if (bufferLoc == NULL) {
        __android_log_print(ANDROID_LOG_ERROR, "JNI Routine",
                            "Failed to retrieve direct buffer location associated with ByteBuffer.");
        return;
    }

    if (bufferLoc != _holdBuffer) {
        __android_log_print(ANDROID_LOG_ERROR, "JNI Routine",
                            "DirectBuffer does not match that allocated.");
        return;
    }

    // Free the malloc'ed buffer and the global reference. Java can not GC the direct buffer.
    free(bufferLoc);
    env->DeleteGlobalRef(_directBuffer);
    _holdBuffer = NULL;
    _directBuffer = NULL;
}
}

Я также обновил держатель массива:

class JniByteArrayHolder {
    external fun allocate(size: Long): ByteBuffer
    external fun freeBuffer(byteBuffer: ByteBuffer)

    companion object {
        init {
            System.loadLibrary("native-lib")
        }
    }
}

Я могу подтвердить, что этот код вместе с ByteBufferChannel классом, предоставленным Botje здесь работает для Android версий до API 24. SeekableByteChannel интерфейс был представлен в API 24 и необходим для утилиты ZipFile.

Максимальный размер буфера, который может быть выделен, равен размеру jint и связан с ограничениями JNI. Можно разместить более крупные данные (если они доступны), но для этого потребуется несколько буферов и способ их обработки.

Вот основное действие для примера приложения. Более ранняя версия всегда предполагала, что буфер чтения InputStream всегда был заполнен, и вылетали ошибки при попытке поместить его в ByteBuffer. Это было исправлено.

MainActivity.kt

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }

    fun onClick(view: View) {
        button.isEnabled = false
        status.text = getString(R.string.running)

        thread {
            printMemStats("Before buffer allocation:")
            var bufferSize = 0L
            // testzipfile.zip is not part of the project but any zip can be uploaded through the
            // device file manager or adb to test.
            val fileToRead = "$filesDir/testzipfile.zip"
            val inStream =
                if (File(fileToRead).exists()) {
                    FileInputStream(fileToRead).apply {
                        bufferSize = getFileSize(this)
                        close()
                    }
                    FileInputStream(fileToRead)
                } else {
                    // If testzipfile.zip doesn't exist, we will just look at this one which
                    // is part of the APK.
                    resources.openRawResource(R.raw.appapk).apply {
                        bufferSize = getFileSize(this)
                        close()
                    }
                    resources.openRawResource(R.raw.appapk)
                }
            // Allocate the buffer in native memory (off-heap).
            val jniByteArrayHolder = JniByteArrayHolder()
            val byteBuffer =
                if (bufferSize != 0L) {
                    jniByteArrayHolder.allocate(bufferSize)?.apply {
                        printMemStats("After buffer allocation")
                    }
                } else {
                    null
                }

            if (byteBuffer == null) {
                Log.d("Applog", "Failed to allocate $bufferSize bytes of native memory.")
            } else {
                Log.d("Applog", "Allocated ${Formatter.formatFileSize(this, bufferSize)} buffer.")
                val inBytes = ByteArray(4096)
                Log.d("Applog", "Starting buffered read...")
                while (inStream.available() > 0) {
                    byteBuffer.put(inBytes, 0, inStream.read(inBytes))
                }
                inStream.close()
                byteBuffer.flip()
                ZipFile(ByteBufferChannel(byteBuffer)).use {
                    Log.d("Applog", "Starting Zip file name dump...")
                    for (entry in it.entries) {
                        Log.d("Applog", "Zip name: ${entry.name}")
                        val zis = it.getInputStream(entry)
                        while (zis.available() > 0) {
                            zis.read(inBytes)
                        }
                    }
                }
                printMemStats("Before buffer release:")
                jniByteArrayHolder.freeBuffer(byteBuffer)
                printMemStats("After buffer release:")
            }
            runOnUiThread {
                status.text = getString(R.string.idle)
                button.isEnabled = true
                Log.d("Applog", "Done!")
            }
        }
    }

    /*
        This function is a little misleading since it does not reflect the true status of memory.
        After native buffer allocation, it waits until the memory is used before counting is as
        used. After release, it doesn't seem to count the memory as released until garbage
        collection. (My observations only.) Also, see the comment for memset() in native-lib.cpp
        which is a member of this project.
    */
    private fun printMemStats(desc: String? = null) {
        val memoryInfo = ActivityManager.MemoryInfo()
        (getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager).getMemoryInfo(memoryInfo)
        val nativeHeapSize = memoryInfo.totalMem
        val nativeHeapFreeSize = memoryInfo.availMem
        val usedMemInBytes = nativeHeapSize - nativeHeapFreeSize
        val usedMemInPercentage = usedMemInBytes * 100 / nativeHeapSize
        val sDesc = desc?.run { "$this:\n" }
        Log.d(
            "AppLog", "$sDesc total:${Formatter.formatFileSize(this, nativeHeapSize)} " +
                    "free:${Formatter.formatFileSize(this, nativeHeapFreeSize)} " +
                    "used:${Formatter.formatFileSize(this, usedMemInBytes)} ($usedMemInPercentage%)"
        )
    }

    // Not a great way to do this but not the object of the demo.
    private fun getFileSize(inStream: InputStream): Long {
        var bufferSize = 0L
        while (inStream.available() > 0) {
            val toSkip = inStream.available().toLong()
            inStream.skip(toSkip)
            bufferSize += toSkip
        }
        return bufferSize
    }
}

Пример репозитория GitHub: , здесь .

0 голосов
/ 08 мая 2020

Вы можете украсть собственные функции управления памятью LWJGL . Он имеет лицензию BSD3, поэтому вам нужно только где-то упомянуть, что вы используете код из него.

Шаг 1: учитывая InputStream is и размер файла ZIP_SIZE, скопируйте поток в буфер прямого байта создано вспомогательным классом org.lwjgl.system.MemoryUtil LWJGL:

ByteBuffer bb = MemoryUtil.memAlloc(ZIP_SIZE);
byte[] buf = new byte[4096]; // Play with the buffer size to see what works best
int read = 0;
while ((read = is.read(buf)) != -1) {
  bb.put(buf, 0, read);
}

Шаг 2: оберните ByteBuffer в ByteChannel. Взято из это суть . Возможно, вы захотите удалить записываемые части.

package io.github.ncruces.utils;

import java.nio.ByteBuffer;
import java.nio.channels.NonWritableChannelException;
import java.nio.channels.SeekableByteChannel;

import static java.lang.Math.min;

public final class ByteBufferChannel implements SeekableByteChannel {
    private final ByteBuffer buf;

    public ByteBufferChannel(ByteBuffer buffer) {
        if (buffer == null) throw new NullPointerException();
        buf = buffer;
    }

    @Override
    public synchronized int read(ByteBuffer dst) {
        if (buf.remaining() == 0) return -1;

        int count = min(dst.remaining(), buf.remaining());
        if (count > 0) {
            ByteBuffer tmp = buf.slice();
            tmp.limit(count);
            dst.put(tmp);
            buf.position(buf.position() + count);
        }
        return count;
    }

    @Override
    public synchronized int write(ByteBuffer src) {
        if (buf.isReadOnly()) throw new NonWritableChannelException();

        int count = min(src.remaining(), buf.remaining());
        if (count > 0) {
            ByteBuffer tmp = src.slice();
            tmp.limit(count);
            buf.put(tmp);
            src.position(src.position() + count);
        }
        return count;
    }

    @Override
    public synchronized long position() {
        return buf.position();
    }

    @Override
    public synchronized ByteBufferChannel position(long newPosition) {
        if ((newPosition | Integer.MAX_VALUE - newPosition) < 0) throw new IllegalArgumentException();
        buf.position((int)newPosition);
        return this;
    }

    @Override
    public synchronized long size() { return buf.limit(); }

    @Override
    public synchronized ByteBufferChannel truncate(long size) {
        if ((size | Integer.MAX_VALUE - size) < 0) throw new IllegalArgumentException();
        int limit = buf.limit();
        if (limit > size) buf.limit((int)size);
        return this;
    }

    @Override
    public boolean isOpen() { return true; }

    @Override
    public void close() {}
}

Шаг 3: Используйте ZipFile как раньше:

ZipFile zf = new ZipFile(ByteBufferChannel(bb);
for (ZipEntry ze : zf) {
    ...
}

Шаг 4: Освободите собственный буфер вручную (желательно в finally блок):

MemoryUtil.memFree(bb);
...