admin管理员组

文章数量:1434014

I currently try to read a given inputStream as base64 encoded string in Kotlin. Sadly, it crashes the whole Android application with out of memory error as soon as the file is bigger than around 20 MegaBytes. I like to extend this for at least 40MB.

I think I need to read and convert the bytes of the stream in blocks like 8K for processing, but I can't find any Kotlin example about how to do this as I cannot simply convert the blocks to base64 and concatenate them. This does not work for base64 because of the encoding.

I think I need some sort of stream base64 encoder and I can't find any solution for this that works for me. The JAVA code I can find here in StackOverflow I'm not able to convert to Kotlin as I'm new to Kotlin and Java in general :-(

Also, I'm reading binary data and can't utilize any lineReader function I find every here and there. It should work on blocks with given byte size.

This is the function that currently crashes if the file is bigger than about 20MB:

private fun pushAttachmentToJS(uri: Uri) {
    try {
        val inputStream = contentResolver.openInputStream(uri)
        inputStream.use { stream ->
            
            // Next two lines need a replacement working in blocks
            val fileBytes = stream?.readBytes()
            val fB64 = Base64.encodeToString(fileBytes, Base64.NO_WRAP) // <-- CRASH

            // (... further processing of the fB64 string)
        }
    } catch (e: IOException) {
        e.printStackTrace()
    }
}

Is someone having something that works in Kotlin?

EDIT: I had some partial success in the meantime. But still issues I have to verify...

private fun pushAttachmentToJS(uri: Uri) {
    try {
        val inputStream = contentResolver.openInputStream(uri)
        inputStream.use { stream ->
            
            val encoder = Base64.getEncoder().withoutPadding()
            val buffer = ByteArray(12000) // bytes buffer (multiple of 3!)
            val builder = StringBuilder()

            var bytesRead: Int
            var lastSize = 0
            while (true) {
                bytesRead = stream!!.read(buffer)
                if (bytesRead == -1) break
                lastSize = bytesRead // remember to later get the padding

                val chunk = buffer.copyOfRange(0, bytesRead)
                val encodedChunk = encoder.encodeToString(chunk)

                // Append the encoded chunk to the StringBuilder
                builder.append(encodedChunk)
            }

            // Add necessary padding to the final string
            val remainder = lastSize % 4
            if (remainder != 0) {
                val paddingNeeded = 4 - remainder
                repeat(paddingNeeded) {
                    builder.append('=')
                }
            }

            val fB64 = builder.toString()

            // (... further processing of the fB64 string)
        }
    } catch (e: IOException) {
        e.printStackTrace()
    }
}

But now the Base64 encoded string seems not okay. At least, it does not work as intended. Will need further inspection tomorrow...

EDIT: I found a solution. See answer post below.

I currently try to read a given inputStream as base64 encoded string in Kotlin. Sadly, it crashes the whole Android application with out of memory error as soon as the file is bigger than around 20 MegaBytes. I like to extend this for at least 40MB.

I think I need to read and convert the bytes of the stream in blocks like 8K for processing, but I can't find any Kotlin example about how to do this as I cannot simply convert the blocks to base64 and concatenate them. This does not work for base64 because of the encoding.

I think I need some sort of stream base64 encoder and I can't find any solution for this that works for me. The JAVA code I can find here in StackOverflow I'm not able to convert to Kotlin as I'm new to Kotlin and Java in general :-(

Also, I'm reading binary data and can't utilize any lineReader function I find every here and there. It should work on blocks with given byte size.

This is the function that currently crashes if the file is bigger than about 20MB:

private fun pushAttachmentToJS(uri: Uri) {
    try {
        val inputStream = contentResolver.openInputStream(uri)
        inputStream.use { stream ->
            
            // Next two lines need a replacement working in blocks
            val fileBytes = stream?.readBytes()
            val fB64 = Base64.encodeToString(fileBytes, Base64.NO_WRAP) // <-- CRASH

            // (... further processing of the fB64 string)
        }
    } catch (e: IOException) {
        e.printStackTrace()
    }
}

Is someone having something that works in Kotlin?

EDIT: I had some partial success in the meantime. But still issues I have to verify...

private fun pushAttachmentToJS(uri: Uri) {
    try {
        val inputStream = contentResolver.openInputStream(uri)
        inputStream.use { stream ->
            
            val encoder = Base64.getEncoder().withoutPadding()
            val buffer = ByteArray(12000) // bytes buffer (multiple of 3!)
            val builder = StringBuilder()

            var bytesRead: Int
            var lastSize = 0
            while (true) {
                bytesRead = stream!!.read(buffer)
                if (bytesRead == -1) break
                lastSize = bytesRead // remember to later get the padding

                val chunk = buffer.copyOfRange(0, bytesRead)
                val encodedChunk = encoder.encodeToString(chunk)

                // Append the encoded chunk to the StringBuilder
                builder.append(encodedChunk)
            }

            // Add necessary padding to the final string
            val remainder = lastSize % 4
            if (remainder != 0) {
                val paddingNeeded = 4 - remainder
                repeat(paddingNeeded) {
                    builder.append('=')
                }
            }

            val fB64 = builder.toString()

            // (... further processing of the fB64 string)
        }
    } catch (e: IOException) {
        e.printStackTrace()
    }
}

But now the Base64 encoded string seems not okay. At least, it does not work as intended. Will need further inspection tomorrow...

EDIT: I found a solution. See answer post below.

Share Improve this question edited Nov 19, 2024 at 13:34 Volker asked Nov 18, 2024 at 11:43 VolkerVolker 5071 gold badge4 silver badges20 bronze badges 6
  • "I like to extend this for at least 40MB" -- you are not going to have enough heap space for that on many devices. What exactly does "further processing of the fB64 string" involve? – CommonsWare Commented Nov 18, 2024 at 13:19
  • @CommonsWare I hand over the b64 encoded string to a webview JS function. Due to my research, this should be fine for up to 2GB of data. I do not plan to go that big, but a bit more than 20MB would be very helpful. But the b64 encoder crashes, so I'm stuck before the hand over... – Volker Commented Nov 18, 2024 at 13:48
  • "Due to my research, this should be fine for up to 2GB of data" -- you absolutely will not have 2GB of heap space. I do not know what research you did that gave you the impression that this will work well. "But the b64 encoder crashes, so I'm stuck before the hand over" -- while I am reasonably certain you can base64-encode in chunks, you still will wind up with a single string measuring in tens of MB, and there is no guarantee that you will have a single contiguous free block of heap space for that. – CommonsWare Commented Nov 18, 2024 at 13:57
  • By the way, if contentResolver.openInputStream(uri) returns null, you typically don't want to run the block at all; so it's better then to use inputStream?.use { stream -> - this also makes stream non-nullable. – k314159 Commented Nov 18, 2024 at 14:29
  • I searched for maximum JS parameter size. Maybe that does not apply here, I agree… "you absolutely will not have 2GB of heap space" -> So what is the maximum String size I can handle? I think this is the most important question for the moment. A few MB work, but what is the limit (if that is limiting me?)? Also, see my code update in the original question. – Volker Commented Nov 18, 2024 at 15:26
 |  Show 1 more comment

1 Answer 1

Reset to default 0

I finally solved it this way to save as much memory as possible:

Encode Uri to base64 string:

private fun convertUriToB64(uri: Uri) {
    try {
        val inputStream = contentResolver.openInputStream(uri)
        inputStream?.use { stream ->
            val filename = getFileNameFromUri(uri)
            val encoder = Base64.getEncoder().withoutPadding()
            val buffer = ByteArray(480000) // bytes buffer (multiple of 3!)
            val builder = StringBuilder()
            var bytesRead: Int
            var sLength = 0
            while (true) {
                bytesRead = stream.read(buffer)
                if (bytesRead == -1) break

                val chunk = buffer.copyOfRange(0, bytesRead)
                val encodedChunk = encoder.encodeToString(chunk)
                builder.append(encodedChunk)
                // count b64 string length to later get padding remainder
                sLength += encodedChunk.length
            }

            // Add padding
            val remainder = sLength % 4
            if (remainder != 0) {
                val paddingNeeded = 4 - remainder
                repeat(paddingNeeded) {
                    builder.append('=')
                }
            }

            // handle the result here:
            return builder.toString()
        }
    } catch (e: IOException) {
        e.printStackTrace()
    }
}

Decode base64 string to file:

@OptIn(ExperimentalEncodingApi::class)
fun storeB64asFile(storageFilePath: File, base64binary: String): Boolean {
    val step = 40000 // multiple of 4!
    var pos = 0
    val os = FileOutputStream(storageFilePath)
    while (true) {
        var end = pos + step
        if (end > base64binary.length) {
            end = base64binary.length
        }
        os.write(kotlin.io.encoding.Base64.decode(base64binary, pos, end))
        pos += step
        if (pos > base64binary.length) {
            break;
        }
    }

    os.flush()
    os.close()

    return true
}

This is stripped down. You might need to add additional error handling and input validation.

本文标签: androidKotlin Convert binary InputStream to base64 string in blocksbufferedStack Overflow