memory optimization

This commit is contained in:
mertalev
2026-01-17 00:52:24 -05:00
parent 957ee84636
commit 06467f2c0d
13 changed files with 201 additions and 81 deletions
+2
View File
@@ -8,3 +8,5 @@ project(native_buffer LANGUAGES C)
add_library(native_buffer SHARED
src/main/cpp/native_buffer.c
)
target_link_libraries(native_buffer jnigraphics)
+23 -24
View File
@@ -1,40 +1,39 @@
#include <jni.h>
#include <stdlib.h>
#include <android/bitmap.h>
JNIEXPORT jlong JNICALL
Java_app_alextran_immich_images_ThumbnailsImpl_00024Companion_allocateNative(
JNIEnv *env, jclass clazz, jint size) {
void *ptr = malloc(size);
return (jlong) ptr;
}
JNIEXPORT jlong JNICALL
Java_app_alextran_immich_images_ThumbnailsImpl_allocateNative(
Java_app_alextran_immich_images_LocalImagesImpl_allocateNative(
JNIEnv *env, jclass clazz, jint size) {
void *ptr = malloc(size);
return (jlong) ptr;
}
JNIEXPORT void JNICALL
Java_app_alextran_immich_images_ThumbnailsImpl_00024Companion_freeNative(
Java_app_alextran_immich_images_LocalImagesImpl_freeNative(
JNIEnv *env, jclass clazz, jlong address) {
free((void *) address);
}
JNIEXPORT jobject JNICALL
Java_app_alextran_immich_images_LocalImagesImpl_wrapAsBuffer(
JNIEnv *env, jclass clazz, jlong address, jint capacity) {
return (*env)->NewDirectByteBuffer(env, (void *) address, capacity);
}
JNIEXPORT jlong JNICALL
Java_app_alextran_immich_images_RemoteImagesImpl_lockBitmapPixels(
JNIEnv *env, jclass clazz, jobject bitmap) {
void *pixels = NULL;
int result = AndroidBitmap_lockPixels(env, bitmap, &pixels);
if (result != ANDROID_BITMAP_RESULT_SUCCESS) {
return 0;
}
return (jlong) pixels;
}
JNIEXPORT void JNICALL
Java_app_alextran_immich_images_ThumbnailsImpl_freeNative(
JNIEnv *env, jclass clazz, jlong address) {
free((void *) address);
}
JNIEXPORT jobject JNICALL
Java_app_alextran_immich_images_ThumbnailsImpl_00024Companion_wrapAsBuffer(
JNIEnv *env, jclass clazz, jlong address, jint capacity) {
return (*env)->NewDirectByteBuffer(env, (void *) address, capacity);
}
JNIEXPORT jobject JNICALL
Java_app_alextran_immich_images_ThumbnailsImpl_wrapAsBuffer(
JNIEnv *env, jclass clazz, jlong address, jint capacity) {
return (*env)->NewDirectByteBuffer(env, (void *) address, capacity);
Java_app_alextran_immich_images_RemoteImagesImpl_unlockBitmapPixels(
JNIEnv *env, jclass clazz, jobject bitmap) {
AndroidBitmap_unlockPixels(env, bitmap);
}
@@ -12,9 +12,6 @@ import android.provider.MediaStore.Images
import android.provider.MediaStore.Video
import android.util.Size
import androidx.annotation.RequiresApi
import app.alextran.immich.images.LocalImagesImpl.Companion.allocateNative
import app.alextran.immich.images.LocalImagesImpl.Companion.freeNative
import app.alextran.immich.images.LocalImagesImpl.Companion.wrapAsBuffer
import java.nio.ByteBuffer
import kotlin.math.*
import java.util.concurrent.Executors
@@ -47,9 +44,9 @@ inline fun ImageDecoder.Source.decodeBitmap(target: Size = Size(0, 0)): Bitmap {
fun Bitmap.toNativeBuffer(): Map<String, Long> {
val size = width * height * 4
val pointer = allocateNative(size)
val pointer = LocalImagesImpl.allocateNative(size)
try {
val buffer = wrapAsBuffer(pointer, size)
val buffer = LocalImagesImpl.wrapAsBuffer(pointer, size)
copyPixelsToBuffer(buffer)
recycle()
return mapOf(
@@ -58,7 +55,7 @@ fun Bitmap.toNativeBuffer(): Map<String, Long> {
"height" to height.toLong()
)
} catch (e: Exception) {
freeNative(pointer)
LocalImagesImpl.freeNative(pointer)
recycle()
throw e
}
@@ -49,6 +49,7 @@ private open class RemoteImagesPigeonCodec : StandardMessageCodec() {
interface RemoteImageApi {
fun requestImage(url: String, headers: Map<String, String>, requestId: Long, callback: (Result<Map<String, Long>>) -> Unit)
fun cancelRequest(requestId: Long)
fun releaseImage(requestId: Long)
companion object {
/** The codec used by RemoteImageApi. */
@@ -99,6 +100,24 @@ interface RemoteImageApi {
channel.setMessageHandler(null)
}
}
run {
val channel = BasicMessageChannel<Any?>(binaryMessenger, "dev.flutter.pigeon.immich_mobile.RemoteImageApi.releaseImage$separatedMessageChannelSuffix", codec)
if (api != null) {
channel.setMessageHandler { message, reply ->
val args = message as List<Any?>
val requestIdArg = args[0] as Long
val wrapped: List<Any?> = try {
api.releaseImage(requestIdArg)
listOf(null)
} catch (exception: Throwable) {
RemoteImagesPigeonUtils.wrapError(exception)
}
reply.reply(wrapped)
}
} else {
channel.setMessageHandler(null)
}
}
}
}
}
@@ -7,7 +7,6 @@ import android.graphics.ColorSpace
import android.graphics.ImageDecoder
import android.os.Build
import android.os.CancellationSignal
import android.util.Size
import app.alextran.immich.core.SSLConfig
import okhttp3.Call
import okhttp3.Callback
@@ -31,9 +30,9 @@ data class RemoteRequest(
class RemoteImagesImpl(context: Context) : RemoteImageApi {
private val requestMap = ConcurrentHashMap<Long, RemoteRequest>()
private val lockedBitmaps = ConcurrentHashMap<Long, Bitmap>()
init {
System.loadLibrary("native_buffer")
cacheDir = context.cacheDir
client = buildClient()
}
@@ -45,17 +44,23 @@ class RemoteImagesImpl(context: Context) : RemoteImageApi {
private const val CACHE_SIZE_BYTES = 1024L * 1024 * 1024
val CANCELLED = Result.success<Map<String, Long>>(emptyMap())
private val decodePool = Executors.newFixedThreadPool(
(Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(2)
)
private val decodePool =
Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() / 2 + 1)
private var cacheDir: File? = null
private var client: OkHttpClient? = null
init {
System.loadLibrary("native_buffer")
SSLConfig.addListener(::invalidateClient)
}
@JvmStatic
external fun lockBitmapPixels(bitmap: Bitmap): Long
@JvmStatic
external fun unlockBitmapPixels(bitmap: Bitmap)
private fun invalidateClient() {
client?.let {
it.dispatcher.cancelAll()
@@ -123,8 +128,20 @@ class RemoteImagesImpl(context: Context) : RemoteImageApi {
signal.throwIfCanceled()
val bitmap = decodeImage(bytes)
signal.throwIfCanceled()
val res = bitmap.toNativeBuffer()
callback(Result.success(res))
val pointer = lockBitmapPixels(bitmap)
if (pointer == 0L) {
bitmap.recycle()
return@execute callback(Result.failure(RuntimeException("Failed to lock bitmap pixels")))
}
lockedBitmaps[requestId] = bitmap
callback(Result.success(mapOf(
"pointer" to pointer,
"width" to bitmap.width.toLong(),
"height" to bitmap.height.toLong(),
"rowBytes" to bitmap.rowBytes.toLong()
)))
} catch (e: Exception) {
val result = if (signal.isCanceled) CANCELLED else Result.failure(e)
callback(result)
@@ -138,8 +155,14 @@ class RemoteImagesImpl(context: Context) : RemoteImageApi {
}
override fun cancelRequest(requestId: Long) {
val request = requestMap.remove(requestId) ?: return
request.cancellationSignal.cancel()
requestMap.remove(requestId)?.cancellationSignal?.cancel()
releaseImage(requestId)
}
override fun releaseImage(requestId: Long) {
val bitmap = lockedBitmaps.remove(requestId) ?: return
unlockBitmapPixels(bitmap)
bitmap.recycle()
}
private fun decodeImage(bytes: ByteArray): Bitmap {