perf: fewer allocations when decoding images
This commit is contained in:
parent
b5d2ac3377
commit
e1362fc40c
9 changed files with 75 additions and 48 deletions
|
@ -12,6 +12,7 @@ import com.bumptech.glide.Glide
|
||||||
import com.bumptech.glide.load.DecodeFormat
|
import com.bumptech.glide.load.DecodeFormat
|
||||||
import com.bumptech.glide.request.RequestOptions
|
import com.bumptech.glide.request.RequestOptions
|
||||||
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
|
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
|
||||||
|
import deckers.thibault.aves.channel.calls.Coresult.Companion.safesus
|
||||||
import deckers.thibault.aves.model.FieldMap
|
import deckers.thibault.aves.model.FieldMap
|
||||||
import deckers.thibault.aves.utils.BitmapUtils.getBytes
|
import deckers.thibault.aves.utils.BitmapUtils.getBytes
|
||||||
import deckers.thibault.aves.utils.LogUtils
|
import deckers.thibault.aves.utils.LogUtils
|
||||||
|
@ -30,7 +31,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
|
||||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||||
when (call.method) {
|
when (call.method) {
|
||||||
"getPackages" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getPackages) }
|
"getPackages" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getPackages) }
|
||||||
"getAppIcon" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getAppIcon) }
|
"getAppIcon" -> GlobalScope.launch(Dispatchers.IO) { safesus(call, result, ::getAppIcon) }
|
||||||
"edit" -> {
|
"edit" -> {
|
||||||
val title = call.argument<String>("title")
|
val title = call.argument<String>("title")
|
||||||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||||
|
@ -109,7 +110,7 @@ class AppAdapterHandler(private val context: Context) : MethodCallHandler {
|
||||||
result.success(ArrayList(packages.values))
|
result.success(ArrayList(packages.values))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getAppIcon(call: MethodCall, result: MethodChannel.Result) {
|
private suspend fun getAppIcon(call: MethodCall, result: MethodChannel.Result) {
|
||||||
val packageName = call.argument<String>("packageName")
|
val packageName = call.argument<String>("packageName")
|
||||||
val sizeDip = call.argument<Double>("sizeDip")
|
val sizeDip = call.argument<Double>("sizeDip")
|
||||||
if (packageName == null || sizeDip == null) {
|
if (packageName == null || sizeDip == null) {
|
||||||
|
|
|
@ -31,8 +31,8 @@ class ImageFileHandler(private val activity: Activity) : MethodCallHandler {
|
||||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||||
when (call.method) {
|
when (call.method) {
|
||||||
"getEntry" -> GlobalScope.launch(Dispatchers.IO) { safesus(call, result, ::getEntry) }
|
"getEntry" -> GlobalScope.launch(Dispatchers.IO) { safesus(call, result, ::getEntry) }
|
||||||
"getThumbnail" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getThumbnail) }
|
"getThumbnail" -> GlobalScope.launch(Dispatchers.IO) { safesus(call, result, ::getThumbnail) }
|
||||||
"getRegion" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getRegion) }
|
"getRegion" -> GlobalScope.launch(Dispatchers.IO) { safesus(call, result, ::getRegion) }
|
||||||
"rename" -> GlobalScope.launch(Dispatchers.IO) { safesus(call, result, ::rename) }
|
"rename" -> GlobalScope.launch(Dispatchers.IO) { safesus(call, result, ::rename) }
|
||||||
"rotate" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::rotate) }
|
"rotate" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::rotate) }
|
||||||
"flip" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::flip) }
|
"flip" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::flip) }
|
||||||
|
@ -61,7 +61,7 @@ class ImageFileHandler(private val activity: Activity) : MethodCallHandler {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getThumbnail(call: MethodCall, result: MethodChannel.Result) {
|
private suspend fun getThumbnail(call: MethodCall, result: MethodChannel.Result) {
|
||||||
val uri = call.argument<String>("uri")
|
val uri = call.argument<String>("uri")
|
||||||
val mimeType = call.argument<String>("mimeType")
|
val mimeType = call.argument<String>("mimeType")
|
||||||
val dateModifiedSecs = call.argument<Number>("dateModifiedSecs")?.toLong()
|
val dateModifiedSecs = call.argument<Number>("dateModifiedSecs")?.toLong()
|
||||||
|
@ -93,7 +93,7 @@ class ImageFileHandler(private val activity: Activity) : MethodCallHandler {
|
||||||
).fetch()
|
).fetch()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getRegion(call: MethodCall, result: MethodChannel.Result) {
|
private suspend fun getRegion(call: MethodCall, result: MethodChannel.Result) {
|
||||||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||||
val mimeType = call.argument<String>("mimeType")
|
val mimeType = call.argument<String>("mimeType")
|
||||||
val pageId = call.argument<Int>("pageId")
|
val pageId = call.argument<Int>("pageId")
|
||||||
|
|
|
@ -28,6 +28,7 @@ import com.drew.metadata.png.PngDirectory
|
||||||
import com.drew.metadata.webp.WebpDirectory
|
import com.drew.metadata.webp.WebpDirectory
|
||||||
import com.drew.metadata.xmp.XmpDirectory
|
import com.drew.metadata.xmp.XmpDirectory
|
||||||
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
|
import deckers.thibault.aves.channel.calls.Coresult.Companion.safe
|
||||||
|
import deckers.thibault.aves.channel.calls.Coresult.Companion.safesus
|
||||||
import deckers.thibault.aves.metadata.*
|
import deckers.thibault.aves.metadata.*
|
||||||
import deckers.thibault.aves.metadata.ExifInterfaceHelper.describeAll
|
import deckers.thibault.aves.metadata.ExifInterfaceHelper.describeAll
|
||||||
import deckers.thibault.aves.metadata.ExifInterfaceHelper.getSafeDateMillis
|
import deckers.thibault.aves.metadata.ExifInterfaceHelper.getSafeDateMillis
|
||||||
|
@ -88,7 +89,7 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
|
||||||
"getMultiPageInfo" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getMultiPageInfo) }
|
"getMultiPageInfo" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getMultiPageInfo) }
|
||||||
"getPanoramaInfo" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getPanoramaInfo) }
|
"getPanoramaInfo" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getPanoramaInfo) }
|
||||||
"getContentResolverProp" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getContentResolverProp) }
|
"getContentResolverProp" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getContentResolverProp) }
|
||||||
"getExifThumbnails" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::getExifThumbnails) }
|
"getExifThumbnails" -> GlobalScope.launch(Dispatchers.IO) { safesus(call, result, ::getExifThumbnails) }
|
||||||
"extractVideoEmbeddedPicture" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::extractVideoEmbeddedPicture) }
|
"extractVideoEmbeddedPicture" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::extractVideoEmbeddedPicture) }
|
||||||
"extractXmpDataProp" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::extractXmpDataProp) }
|
"extractXmpDataProp" -> GlobalScope.launch(Dispatchers.IO) { safe(call, result, ::extractXmpDataProp) }
|
||||||
else -> result.notImplemented()
|
else -> result.notImplemented()
|
||||||
|
@ -745,7 +746,7 @@ class MetadataHandler(private val context: Context) : MethodCallHandler {
|
||||||
result.success(value?.toString())
|
result.success(value?.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getExifThumbnails(call: MethodCall, result: MethodChannel.Result) {
|
private suspend fun getExifThumbnails(call: MethodCall, result: MethodChannel.Result) {
|
||||||
val mimeType = call.argument<String>("mimeType")
|
val mimeType = call.argument<String>("mimeType")
|
||||||
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
val uri = call.argument<String>("uri")?.let { Uri.parse(it) }
|
||||||
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
|
val sizeBytes = call.argument<Number>("sizeBytes")?.toLong()
|
||||||
|
|
|
@ -30,7 +30,7 @@ class RegionFetcher internal constructor(
|
||||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
.skipMemoryCache(true)
|
.skipMemoryCache(true)
|
||||||
|
|
||||||
fun fetch(
|
suspend fun fetch(
|
||||||
uri: Uri,
|
uri: Uri,
|
||||||
mimeType: String,
|
mimeType: String,
|
||||||
pageId: Int?,
|
pageId: Int?,
|
||||||
|
|
|
@ -45,7 +45,7 @@ class ThumbnailFetcher internal constructor(
|
||||||
private val multiTrackFetch = isHeic(mimeType) && pageId != null
|
private val multiTrackFetch = isHeic(mimeType) && pageId != null
|
||||||
private val customFetch = tiffFetch || multiTrackFetch
|
private val customFetch = tiffFetch || multiTrackFetch
|
||||||
|
|
||||||
fun fetch() {
|
suspend fun fetch() {
|
||||||
var bitmap: Bitmap? = null
|
var bitmap: Bitmap? = null
|
||||||
var exception: Exception? = null
|
var exception: Exception? = null
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ import org.beyka.tiffbitmapfactory.TiffBitmapFactory
|
||||||
class TiffRegionFetcher internal constructor(
|
class TiffRegionFetcher internal constructor(
|
||||||
private val context: Context,
|
private val context: Context,
|
||||||
) {
|
) {
|
||||||
fun fetch(
|
suspend fun fetch(
|
||||||
uri: Uri,
|
uri: Uri,
|
||||||
page: Int,
|
page: Int,
|
||||||
sampleSize: Int,
|
sampleSize: Int,
|
||||||
|
|
|
@ -76,7 +76,7 @@ class ImageByteStreamHandler(private val activity: Activity, private val argumen
|
||||||
// - Flutter (as of v1.20): JPEG, PNG, GIF, Animated GIF, WebP, Animated WebP, BMP, and WBMP
|
// - Flutter (as of v1.20): JPEG, PNG, GIF, Animated GIF, WebP, Animated WebP, BMP, and WBMP
|
||||||
// - Android: https://developer.android.com/guide/topics/media/media-formats#image-formats
|
// - Android: https://developer.android.com/guide/topics/media/media-formats#image-formats
|
||||||
// - Glide: https://github.com/bumptech/glide/blob/master/library/src/main/java/com/bumptech/glide/load/ImageHeaderParser.java
|
// - Glide: https://github.com/bumptech/glide/blob/master/library/src/main/java/com/bumptech/glide/load/ImageHeaderParser.java
|
||||||
private fun streamImage() {
|
private suspend fun streamImage() {
|
||||||
if (arguments !is Map<*, *>) {
|
if (arguments !is Map<*, *>) {
|
||||||
endOfStream()
|
endOfStream()
|
||||||
return
|
return
|
||||||
|
@ -114,7 +114,7 @@ class ImageByteStreamHandler(private val activity: Activity, private val argumen
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun streamImageByGlide(uri: Uri, pageId: Int?, mimeType: String, rotationDegrees: Int, isFlipped: Boolean) {
|
private suspend fun streamImageByGlide(uri: Uri, pageId: Int?, mimeType: String, rotationDegrees: Int, isFlipped: Boolean) {
|
||||||
val model: Any = if (isHeic(mimeType) && pageId != null) {
|
val model: Any = if (isHeic(mimeType) && pageId != null) {
|
||||||
MultiTrackImage(activity, uri, pageId)
|
MultiTrackImage(activity, uri, pageId)
|
||||||
} else if (mimeType == MimeTypes.TIFF) {
|
} else if (mimeType == MimeTypes.TIFF) {
|
||||||
|
@ -145,7 +145,7 @@ class ImageByteStreamHandler(private val activity: Activity, private val argumen
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun streamVideoByGlide(uri: Uri) {
|
private suspend fun streamVideoByGlide(uri: Uri) {
|
||||||
val target = Glide.with(activity)
|
val target = Glide.with(activity)
|
||||||
.asBitmap()
|
.asBitmap()
|
||||||
.apply(glideOptions)
|
.apply(glideOptions)
|
||||||
|
|
|
@ -19,6 +19,9 @@ import com.bumptech.glide.module.LibraryGlideModule
|
||||||
import com.bumptech.glide.signature.ObjectKey
|
import com.bumptech.glide.signature.ObjectKey
|
||||||
import deckers.thibault.aves.utils.BitmapUtils.getBytes
|
import deckers.thibault.aves.utils.BitmapUtils.getBytes
|
||||||
import deckers.thibault.aves.utils.StorageUtils.openMetadataRetriever
|
import deckers.thibault.aves.utils.StorageUtils.openMetadataRetriever
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.GlobalScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
|
@ -47,40 +50,42 @@ internal class VideoThumbnailLoader : ModelLoader<VideoThumbnail, InputStream> {
|
||||||
|
|
||||||
internal class VideoThumbnailFetcher(private val model: VideoThumbnail) : DataFetcher<InputStream> {
|
internal class VideoThumbnailFetcher(private val model: VideoThumbnail) : DataFetcher<InputStream> {
|
||||||
override fun loadData(priority: Priority, callback: DataCallback<in InputStream>) {
|
override fun loadData(priority: Priority, callback: DataCallback<in InputStream>) {
|
||||||
val retriever = openMetadataRetriever(model.context, model.uri)
|
GlobalScope.launch(Dispatchers.IO) {
|
||||||
if (retriever != null) {
|
val retriever = openMetadataRetriever(model.context, model.uri)
|
||||||
try {
|
if (retriever != null) {
|
||||||
var bytes = retriever.embeddedPicture
|
try {
|
||||||
if (bytes == null) {
|
var bytes = retriever.embeddedPicture
|
||||||
// try to match the thumbnails returned by the content resolver / Media Store
|
if (bytes == null) {
|
||||||
// the following strategies are from empirical evidence from a few test devices:
|
// try to match the thumbnails returned by the content resolver / Media Store
|
||||||
// - API 29: sync frame closest to the middle
|
// the following strategies are from empirical evidence from a few test devices:
|
||||||
// - API 26/27: default representative frame at any time position
|
// - API 29: sync frame closest to the middle
|
||||||
var timeMillis: Long? = null
|
// - API 26/27: default representative frame at any time position
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
var timeMillis: Long? = null
|
||||||
val durationMillis = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLongOrNull()
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
if (durationMillis != null) {
|
val durationMillis = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)?.toLongOrNull()
|
||||||
timeMillis = durationMillis / 2
|
if (durationMillis != null) {
|
||||||
|
timeMillis = durationMillis / 2
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
val frame = if (timeMillis != null) {
|
||||||
|
retriever.getFrameAtTime(timeMillis * 1000)
|
||||||
|
} else {
|
||||||
|
retriever.frameAtTime
|
||||||
|
}
|
||||||
|
bytes = frame?.getBytes(canHaveAlpha = false, recycle = false)
|
||||||
}
|
}
|
||||||
val frame = if (timeMillis != null) {
|
|
||||||
retriever.getFrameAtTime(timeMillis * 1000)
|
|
||||||
} else {
|
|
||||||
retriever.frameAtTime
|
|
||||||
}
|
|
||||||
bytes = frame?.getBytes(canHaveAlpha = false, recycle = false)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bytes != null) {
|
if (bytes != null) {
|
||||||
callback.onDataReady(ByteArrayInputStream(bytes))
|
callback.onDataReady(ByteArrayInputStream(bytes))
|
||||||
} else {
|
} else {
|
||||||
callback.onLoadFailed(Exception("failed to get embedded picture or any frame"))
|
callback.onLoadFailed(Exception("failed to get embedded picture or any frame"))
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
callback.onLoadFailed(e)
|
||||||
|
} finally {
|
||||||
|
// cannot rely on `MediaMetadataRetriever` being `AutoCloseable` on older APIs
|
||||||
|
retriever.release()
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
callback.onLoadFailed(e)
|
|
||||||
} finally {
|
|
||||||
// cannot rely on `MediaMetadataRetriever` being `AutoCloseable` on older APIs
|
|
||||||
retriever.release()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,14 +6,29 @@ import android.util.Log
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
import com.bumptech.glide.load.resource.bitmap.TransformationUtils
|
import com.bumptech.glide.load.resource.bitmap.TransformationUtils
|
||||||
import deckers.thibault.aves.metadata.Metadata.getExifCode
|
import deckers.thibault.aves.metadata.Metadata.getExifCode
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
|
|
||||||
object BitmapUtils {
|
object BitmapUtils {
|
||||||
private val LOG_TAG = LogUtils.createTag<BitmapUtils>()
|
private val LOG_TAG = LogUtils.createTag<BitmapUtils>()
|
||||||
|
private const val INITIAL_BUFFER_SIZE = 2 shl 17 // 256kB
|
||||||
|
|
||||||
fun Bitmap.getBytes(canHaveAlpha: Boolean = false, quality: Int = 100, recycle: Boolean = true): ByteArray? {
|
private val freeBaos = ArrayList<ByteArrayOutputStream>()
|
||||||
|
private val mutex = Mutex()
|
||||||
|
|
||||||
|
suspend fun Bitmap.getBytes(canHaveAlpha: Boolean = false, quality: Int = 100, recycle: Boolean): ByteArray? {
|
||||||
|
val stream: ByteArrayOutputStream
|
||||||
|
mutex.withLock {
|
||||||
|
// this method is called a lot, so we try and reuse output streams
|
||||||
|
// to reduce inner array allocations, and make the GC run less frequently
|
||||||
|
stream = if (freeBaos.isNotEmpty()) {
|
||||||
|
freeBaos.removeAt(0)
|
||||||
|
} else {
|
||||||
|
ByteArrayOutputStream(INITIAL_BUFFER_SIZE)
|
||||||
|
}
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
val stream = ByteArrayOutputStream()
|
|
||||||
// we compress the bitmap because Flutter cannot decode the raw bytes
|
// we compress the bitmap because Flutter cannot decode the raw bytes
|
||||||
// `Bitmap.CompressFormat.PNG` is slower than `JPEG`, but it allows transparency
|
// `Bitmap.CompressFormat.PNG` is slower than `JPEG`, but it allows transparency
|
||||||
if (canHaveAlpha) {
|
if (canHaveAlpha) {
|
||||||
|
@ -22,7 +37,12 @@ object BitmapUtils {
|
||||||
this.compress(Bitmap.CompressFormat.JPEG, quality, stream)
|
this.compress(Bitmap.CompressFormat.JPEG, quality, stream)
|
||||||
}
|
}
|
||||||
if (recycle) this.recycle()
|
if (recycle) this.recycle()
|
||||||
return stream.toByteArray()
|
val byteArray = stream.toByteArray()
|
||||||
|
stream.reset()
|
||||||
|
mutex.withLock {
|
||||||
|
freeBaos.add(stream)
|
||||||
|
}
|
||||||
|
return byteArray
|
||||||
} catch (e: IllegalStateException) {
|
} catch (e: IllegalStateException) {
|
||||||
Log.e(LOG_TAG, "failed to get bytes from bitmap", e)
|
Log.e(LOG_TAG, "failed to get bytes from bitmap", e)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue