web-dev-qa-db-ja.com

RAMからzipファイルを完全に解析する方法は?

バックグラウンド

さまざまなタイプのいくつかのZipファイルを解析する必要があります(名前を取得するなど、何らかの目的でいくつかの内部ファイルのコンテンツを取得します)。

Androidに到達するためのUriがあり、Zipファイルが別のZipファイル内にある場合があるため、一部のファイルはfile-path経由で到達できません。SAFを使用するプッシュでは、場合によっては、ファイルパスを使用する可能性が低くなります。

このため、処理する2つの主な方法があります: ZipFile クラスと ZipInputStream クラス。

問題

ファイルパスがある場合、ZipFileは完璧なソリューションです。速度の面でも非常に効率的です。

ただし、残りのケースでは、ZipInputStreamが this one などの問題に到達し、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ライブラリ( here )があり、 Zipファイル、および何らかの理由でそのInputStreamソリューション(「ZipArchiveInputStream」と呼ばれます)は、ネイティブのZipInputStreamクラスよりもさらに効率的に見えます。

ネイティブフレームワークにあるものとは対照的に、ライブラリはもう少し柔軟性を提供します。たとえば、Zipファイル全体をバイト配列にロードして、ライブラリに通常どおりに処理させることができます。これは、前述の問題のある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依存関係:

_// 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に依存するためです。ヒープサイズが比較的小さい可能性があるためです(ヒープは100MBになる可能性がありますが、ファイルは200MBです)巨大なヒープメモリを設定できるPCとは対照的に、Androidの場合、まったく柔軟性がありません。

そこで、代わりにJNIを使​​用して、Zipファイル全体をバイト配列にロードし、ヒープに(少なくとも完全にではなく)ロードするソリューションを検索しました。 ZipがヒープではなくデバイスのRAM)に収まる場合、余分なファイルを必要とせずにOOMに到達できないため、これはより良い回避策になる可能性があります。

私は "larray" と呼ばれるこのライブラリを見つけました。完全なJVM、つまりAndroidには適さない。

編集:ライブラリや組み込みクラスが見つからないことを知り、JNIを自分で使用しようとしました。悲しいことに、私は非常に錆びており、ビットマップでいくつかの操作を実行するためにずっと前に作成した古いリポジトリを見ました( here =)。これは私が思いついたものです:

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%)")
    }
_

jniByteArrayHolder.allocate(1L * 1024L * 1024L * 1024L)を使用して1GBのバイト配列を作成しようとすると、例外やエラーログなしでクラッシュするため、これは正しくないようです。

質問

  1. ApacheのライブラリにJNIを使​​用して、JNIの「ワールド」に含まれるZipファイルのコンテンツを処理できるようにすることは可能ですか?

  2. もしそうなら、どうすればいいですか?それを行う方法のサンプルはありますか?クラスはありますか?それとも自分で実装する必要がありますか?もしそうなら、JNIでそれがどのように行われるかを示していただけますか

  3. それが不可能な場合、それを行うには他にどのような方法がありますか?たぶんApacheが持っているものに代わるものですか?

  4. JNIの解決策として、なぜうまくいかないのですか?ストリームからJNIバイト配列にバイトを効率的にコピーするにはどうすればよいですか(私の推測では、バッファーを介することになると思います)。

4

私はあなたが投稿したJNIコードを見て、いくつかの変更を加えました。ほとんどの場合、NewDirectByteBufferのサイズ引数を定義し、malloc()を使用しています。

800mbを割り当てた後のログの出力は次のとおりです。

D/AppLog:合計:1.57 GB空き:1.03 GB使用:541 MB(34%)
D/AppLog:合計:1.57 GBの空き容量:247 MB​​の使用容量:1.32 GB(84%)

また、割り当て後のバッファは次のようになります。ご覧のとおり、デバッガーは800 MBの制限を報告しています。

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")
        }
    }
}

このコードとBotjeが提供するByteBufferChannelクラス here が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リポジトリのサンプルは here です。

1
Cheticamp

LWJGLの ネイティブメモリ管理関数 を盗むことができます。これはBSD3ライセンスであるので、そこからコードを使用していることをどこかにのみ言及する必要があります。

ステップ1:InputStream isとファイルサイズZip_SIZEを指定して、LWJGLのorg.lwjgl.system.MemoryUtilヘルパークラスによって作成されたダイレクトバイトバッファーにストリームをスラップします。

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:ByteBufferByteChannelでラップします。 this Gist から取得。おそらく、筆記用の部品を取り除きたいでしょう。

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);
0
Botje