5

Background

I need to parse some zip files of various types (getting some inner files content for one purpose or another, including getting their names).

Some of the files are not reachable via file-path, as Android has Uri to reach them, and as sometimes the zip file is inside another zip file. With the push to use SAF, it's even less possible to use file-path in some cases.

For this, we have 2 main ways to handle: ZipFile class and ZipInputStream class.

The problem

When we have a file-path, ZipFile is a perfect solution. It's also very efficient in terms of speed.

However, for the rest of the cases, ZipInputStream could reach issues, such as this one, which has a problematic zip file, and cause this exception:

  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)

What I've tried

The only always-working solution would be to copy the file to somewhere else, where you could parse it using ZipFile, but this is inefficient and requires you to have free storage, as well as remove the file when you are done with it.

So, what I've found is that Apache has a nice, pure Java library (here) to parse Zip files, and for some reason its InputStream solution (called "ZipArchiveInputStream") seem even more efficient than the native ZipInputStream class.

As opposed to what we have in the native framework, the library offers a bit more flexibility. I could, for example, load the entire zip file into bytes array, and let the library handle it as usual, and this works even for the problematic Zip files I've mentioned:

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'

Sadly, this isn't always possible, because it depends on having the heap memory hold the entire zip file, and on Android it gets even more limited, because the heap size could be relatively small (heap could be 100MB while the file is 200MB). As opposed to a PC which can have a huge heap memory being set, for Android it's not flexible at all.

So, I searched for a solution that has JNI instead, to have the entire ZIP file loaded into byte array there, not going to the heap (at least not entirely). This could be a nicer workaround because if the ZIP could be fit in the device's RAM instead of the heap, it could prevent me from reaching OOM while also not needing to have an extra file.

I've found this library called "larray" which seems promising , but sadly when I tried using it, it crashed, because its requirements include having a full JVM, meaning not suitable for Android.

EDIT: seeing that I can't find any library and any built-in class, I tried to use JNI myself. Sadly I'm very rusty with it, and I looked at an old repository I've made a long time ago to perform some operations on Bitmaps (here). This is what I came up with :

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

This doesn't seem right, because if I try to create a 1GB byte array using jniByteArrayHolder.allocate(1L * 1024L * 1024L * 1024L) , it crashes without any exception or error logs.

The questions

  1. Is it possible to use JNI for Apache's library, so that it will handle the ZIP file content which is contained within JNI's "world" ?

  2. If so, how can I do it? Is there any sample of how to do it? Is there a class for it? Or do I have to implement it myself? If so, can you please show how it's done in JNI?

  3. If it's not possible, what other way is there to do it? Maybe alternative to what Apache has?

  4. For the solution of JNI, how come it doesn't work well ? How could I efficiently copy the bytes from the stream into the JNI byte array (my guess is that it will be via a buffer)?

android developer
  • 114,585
  • 152
  • 739
  • 1,270
  • One approach is to load the zip file into native memory and use `NewDirectByteBuffer` to wrap it into a `ByteBuffer` for Java. – Botje May 08 '20 at 07:34
  • @Botje Wait, you say there is already a nice class for that, to hold bytes in JNI only? Please show how to use it. – android developer May 08 '20 at 08:02
  • Edit your question to show where you get your zip file bytes from. Is it SAF? A ZipArchiveInputStream reading from an SAFInputstream? – Botje May 08 '20 at 08:11
  • It can be from anywhere. It doesn't matter where. It's an InputStream that you can re-create whenever you wish (well at least for the near time). As I wrote, could be Uri or even inside a zip file. It's not relevant – android developer May 08 '20 at 08:50
  • As for your question 4: `jniByteArrayHolder.allocate(1L * 1024L * 1024L * 1024L)` will allocate 4 GB, not 1 GB. You need to pass the address of the buffer (`array`) to `NewDirectByteBuffer`, not the address of the holder object (`JniBytesArray`). Finally, you need to pass `size` as the second argument, not 0. For the last part of that question, I'm afraid there's nothing available that will let you skip the intermediate "read into array" bit. – Botje May 11 '20 at 16:29
  • Why would it allocate 4 GB? it's 1024 bytes times 1024 which makes it MB, times 1024 which makes it GB. I also don't understand the rest of what you wrote. Can you please show how to fix this code? I think that if it's small enough, perhaps it's better than a huge library... Also please not here, but in the answer ... – android developer May 11 '20 at 18:02
  • `new uint32_t[size]` allocates 4 * `size` bytes because uint32_t is 4 bytes. – Botje May 11 '20 at 19:32
  • @Botje Oh makes sense. I forgot that I used this for pixel data for bitmaps and recklessly copy-paste. So what should I have used? Can you please show in your answer instead of comments? – android developer May 12 '20 at 07:06

2 Answers2

1

I took a look at the JNI code you posted and made a couple of changes. Mostly it is defining the size argument for NewDirectByteBuffer and using malloc().

Here is the output of the log after allocating 800mb:

D/AppLog: total:1.57 GB free:1.03 GB used:541 MB (34%)
D/AppLog: total:1.57 GB free:247 MB used:1.32 GB (84%)

And the following is what the buffer looks like after the allocation. As you can see, the debugger is reporting a limit of 800mb which is what we expect.

enter image description here My C is very rusty, so I am sure that there is some work to be done. I have updated the code to be a little more robust and to allow for the freeing of memory.

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;
}
}

I also updated the array holder:

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

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

I can confirm that this code along with the ByteBufferChannel class provided by Botje here works for Android versions before API 24. The SeekableByteChannel interface was introduced in API 24 and is needed by the ZipFile utility.

The maximum buffer size that can be allocated is the size of a jint and is due to the limitation of JNI. Larger data can be accommodated (if available) but would require multiple buffers and a way to handle them.

Here is the main activity for the sample app. An earlier version always assumed the the InputStream read buffer was was always filled and errored out when trying to put it to the ByteBuffer. This was fixed.

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

A sample GitHub repository is here.

Cheticamp
  • 61,413
  • 10
  • 78
  • 131
  • It would be very helpful. Please put here minimal code and show on Github. I don't understand what you meant about API 24 though. – android developer May 13 '20 at 20:46
  • 1
    @androiddeveloper Sample project is [here](https://github.com/Cheticamp/ZipFileInMemoryJNI). Android Studio tells me, as does the doc, that API 24 is needed for 'SeekableByteChannel '. Oh, the sample project reads a small APK file from the raw directory. – Cheticamp May 13 '20 at 21:26
  • It had a small issue building, but removing of the `buildToolsVersion` and `ndkVersion` fixed it. Seems to work very well! However, for some reason you've let it use 800MB for the zip file (to show that it takes RAM, probably), and if I set it to use the exact size of the ZIP file instead (2,688,742 bytes in your case), it crashes with BufferOverflowException. How come? Also, say, do you know what could be a good max buffer size I should allow it? It should probably be based on percentage of total RAM and yet always leave some hunderds of MB free too , right ? – android developer May 14 '20 at 06:50
  • @androiddeveloper 2,688,742 bytes is the size of the data but the size on disk is 2,691,072 due to blocking (at least on my machine). That is the size you want to use and it may be different on your setup. The 800 megs is just a large size I picked to test the limits. I think some max % of RAM would do but what's the max size of an APK anyway? 100MB before expansion files are needed? I'll also add here that there is no need to clear memory upon allocation (memset()). I did that because that is what you had in the original version, but it can come out and will speed things up quite a bit. – Cheticamp May 14 '20 at 10:05
  • But it's a file, and I checked the size of the bytes it has via code. When you load the data into a byte array, this is the number you'd be safe to use ( 2,688,742) . How come? How do I get the safe size if not from the stream? All I have as input is the InputStream (and Uri or zip entry in some cases) ... As for recommended size, I didn't say the input is of an APK file. The zip file can be of any type. The question is more general. – android developer May 14 '20 at 13:22
  • @androiddeveloper You will have to limit the number of bytes read and not count on the EOF condition. An another note, 'SeekableInMemoryByteChannel' uses 'SeekableByteChannel' so the app fails for API <24. I don't have a ready solution other than to duplicate the files locally. – Cheticamp May 14 '20 at 13:26
  • I don't understand. How can I get a safe number to use here? Given an inputStream that I can re-create freely (example by `contentResolver.openInputStream()`), and optionally a Uri or a zip-entry, how can I decide the safe number to allocate? Or even when having a File instance... – android developer May 14 '20 at 13:30
  • @androiddeveloper If you know the size of the file, you can allocate that size then read only that number of bytes. The read loop would have to be modified to potentially stop short of the EOF mark so you don't get the overflow condition. If you don't know the size of the file then it gets a little messy. – Cheticamp May 14 '20 at 13:38
  • I need to know this. It's important that before parsing the ZIP file from RAM, I need to decide how much RAM to allocate for it. How come the bytes count isn't what it needs? – android developer May 14 '20 at 14:54
  • @androiddeveloper The byte count is what is needed if you know the byte count. If you know the you will read x bytes to the end of file then read just x bytes and allocate just x bytes. It's when you don't know the byte count that things get messy. In that case, you will need to allocate additional buffers as needed and stitch them together for ZipFile. `ByteBufferChannel` can be made smarter to handle multiple buffers but there is probably a solution out there. – Cheticamp May 14 '20 at 15:28
  • Can you at least show how to address the cases I've mentioned? Meaning Uri from `contentResolver.openInputStream()` - ok to query using `MediaStore.MediaColumns.SIZE` ? And if File, ok to use `File.length()` ? And what about zip-entry ? OK to use the size that it tells me? And if none of these work, is it ok to just count the bytes myself by reading the entire file (using a buffer that doesn't do anything) ? – android developer May 14 '20 at 16:39
  • @androiddeveloper The `InputStream` doc says that one should not use the `available()` method to allocate buffer space, so for `InputStreams` you may not know the size beforehand, As for `MediaStore.MediaColumns.SIZE`, it looks like you could use that but I don't know. I think the `File.length()` will work as well as zip entry values as long as it is the uncompressed length-the doc should say if it is or not. If all else fails, pick a buffer size that works for the majority of cases. If you reach the end, read the rest to get the size, reallocate based upon the result and reread. – Cheticamp May 14 '20 at 17:03
  • I didn't say I will use `available()`. I wrote "reading" and I also wrote "using a buffer". Is that ok? I still don't understand though about your sample. How come when I've used this: `resources.openRawResource(R.raw.appapk).readBytes().size`, it wasn't good? I've also tested File, and it doesn't work with `File.length()`. It will crash. I don't get it – android developer May 14 '20 at 18:37
  • I think I've found the safe. You get the "clusterSize" using `statFs.blockSize`, and then you can use this: https://stackoverflow.com/a/3750658/878126 . However, this could be a problem with Uri, because you don't know what's going on there. It's still weird for me though, because the byte array using the heap is taking exactly as it needs, and should be fine here too. Can you please explain why it's different for JNI than Java's heap? Using `readBytes`, it returns the exact byte count as the size of the file using `File.length()`. – android developer May 14 '20 at 19:17
  • BTW, about `available()` you can use it with loop of skip. Meaning counting the bytes as you skip the amount you get from `available()` . It's almost the same as I wrote before, only that there is no need for a buffer. So it's a nice idea after all. – android developer May 14 '20 at 19:17
  • @androiddeveloper Updated answer with fix for incorrect size for "put" to `ByteBuffer`. I think that we can forget about my block size argument. – Cheticamp May 14 '20 at 19:30
  • I don't think you understood me. Getting the bytes count of the file (using `resources.openRawResource(R.raw.appapk).readBytes().size.toLong()` or `File.length` or the query I wrote about) seems to always fail (your updated answer will fail). To get a better number of the allocation, it seems to need the calculation I've found, assuming you know the block-size. Sadly I don't know how it can be done for some random InputStream, and I also don't get why it's even needed as the byte array size should be the same as the file size. Do you know why this is as such? – android developer May 14 '20 at 20:46
  • @androiddeveloper I was being too glib when I mentioned the block size. I now don't think that it is relevant. The code I posted most recently does work for me, but maybe I have adjusted something else. It would be helpful to have something specific to look at that fails since I don't seem to be getting the gist. – Cheticamp May 14 '20 at 21:42
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/213899/discussion-between-android-developer-and-cheticamp). – android developer May 15 '20 at 06:05
0

You can steal LWJGL's native memory management functions. It is BSD3 licensed, so you only have to mention somewhere that you are using code from it.

Step 1: given an InputStream is and a file size ZIP_SIZE, slurp the stream into a direct byte buffer created by LWJGL's org.lwjgl.system.MemoryUtil helper class:

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

Step 2: wrap the ByteBuffer in a ByteChannel. Taken from this gist. You possibly want to strip the writing parts out.

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() {}
}

Step 3: Use ZipFile as before:

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

Step 4: Manually release the native buffer (preferably in a finally block):

MemoryUtil.memFree(bb);
Botje
  • 26,269
  • 3
  • 31
  • 41
  • Sadly this doesn't work. I have heap of 200MB (got via ` Runtime.getRuntime().maxMemory()` ), and I tried to call `ByteBuffer.allocateDirect(300 * 1024 * 1024)` , and got the app to crash with OutOfMemoryError, which occurs only for heap OOM. Not with native RAM. So it's the same as creating a normal byte array. Plus it's weird that I can create up to Int max size, which is 2GB . Devices nowadays have more than that. – android developer May 08 '20 at 21:48
  • Can you `malloc` 300 MB in JNI, then? If that works you can use `env->NewDirectByteBuffer` to wrap it in a `ByteBuffer`, with the same results. – Botje May 09 '20 at 17:29
  • On JNI, I know that it's possible to do it. I just ask if there is a class for it already, instead of us needing to do it. And if there isn't I wonder what's the best way to implement it. – android developer May 09 '20 at 19:09
  • There is no pre-made class for it. My "can you" was more along the lines of: "can you check if malloc(300MB) succeeds". If that works you can create a small wrapper class that holds a `ByteBuffer` and can free the underlying memory on `finalize()` or `close()`. – Botje May 11 '20 at 07:50
  • OK if you show how it's all done, I will grant the bounty. Sadly I tried and failed doing it. I'm quite rusty on JNI. It would also be perfect if you publish a working sample on Github (of loading the ZIP into RAM memory via JNI and parsing it via Apache library). I will give you +1 for that effort. Sorry for all the trouble. – android developer May 11 '20 at 09:43
  • The LWJGL project has something that is close to what you need: [MemoryUtil](https://github.com/LWJGL/lwjgl3-wiki/wiki/1.3.-Memory-FAQ) with a basic `nmalloc` function that returns a pointer. This can be wrapped to [produce a ByteBuffer](https://github.com/LWJGL/lwjgl3/blob/master/modules/lwjgl/core/src/main/java/org/lwjgl/system/MemoryUtil.java#L269) – Botje May 11 '20 at 10:32
  • Not sure if it will work. Also about the license. Never saw this kind of license before. Think it's safe to use it even in closed source app? Have you tried using this library? Can you please show how it would work here for zip file loading into RAM ? – android developer May 11 '20 at 15:10
  • BSD3 is basically "do what you want with it but give us credit". – Botje May 11 '20 at 16:17
  • Wait it seems you've updated your code. Does it work? Can you please share the sample? I tried to get only the class but it has a lot of things it relies on, so I used gradle dependency instead. I've noticed that "memAlloc" function takes an integer and not long. This means it's limited to 2GB to allocate, right? Anyway, I tried this technique, but it failed right on `MemoryUtil.memAlloc` function, even with a relatively small ZIP file (73,304,051 bytes). Here's the sample: https://ufile.io/08r1k48a . About BSD3, thanks (reminded me of BSOD too for some reason). – android developer May 11 '20 at 18:02