From c03dcf6d2efce251cb9ac9a2c1e67ba2f2a03f52 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Wed, 4 Jan 2023 12:38:49 +0200 Subject: [PATCH] Fix memory leaks --- .../kotatsu/core/cache/ContentCache.kt | 5 ++- .../kotatsu/core/cache/DeferredLruCache.kt | 7 ++-- .../kotatsu/core/cache/MemoryContentCache.kt | 10 +++--- .../kotatsu/core/cache/SafeDeferred.kt | 20 ++++++++++++ .../kotatsu/core/cache/StubContentCache.kt | 5 ++- .../core/parser/RemoteMangaRepository.kt | 32 ++++++++++++------- .../details/service/MangaPrefetchService.kt | 11 ++----- 7 files changed, 54 insertions(+), 36 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/core/cache/SafeDeferred.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/cache/ContentCache.kt b/app/src/main/java/org/koitharu/kotatsu/core/cache/ContentCache.kt index f9c37b2c3..791ce9358 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/cache/ContentCache.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/cache/ContentCache.kt @@ -1,6 +1,5 @@ package org.koitharu.kotatsu.core.cache -import kotlinx.coroutines.Deferred import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaSource @@ -11,11 +10,11 @@ interface ContentCache { suspend fun getDetails(source: MangaSource, url: String): Manga? - fun putDetails(source: MangaSource, url: String, details: Deferred) + fun putDetails(source: MangaSource, url: String, details: SafeDeferred) suspend fun getPages(source: MangaSource, url: String): List? - fun putPages(source: MangaSource, url: String, pages: Deferred>) + fun putPages(source: MangaSource, url: String, pages: SafeDeferred>) data class Key( val source: MangaSource, diff --git a/app/src/main/java/org/koitharu/kotatsu/core/cache/DeferredLruCache.kt b/app/src/main/java/org/koitharu/kotatsu/core/cache/DeferredLruCache.kt index 7202543fd..f92561198 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/cache/DeferredLruCache.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/cache/DeferredLruCache.kt @@ -1,15 +1,14 @@ package org.koitharu.kotatsu.core.cache import androidx.collection.LruCache -import kotlinx.coroutines.Deferred -class DeferredLruCache(maxSize: Int) : LruCache>(maxSize) { +class DeferredLruCache(maxSize: Int) : LruCache>(maxSize) { override fun entryRemoved( evicted: Boolean, key: ContentCache.Key, - oldValue: Deferred, - newValue: Deferred?, + oldValue: SafeDeferred, + newValue: SafeDeferred?, ) { super.entryRemoved(evicted, key, oldValue, newValue) oldValue.cancel() diff --git a/app/src/main/java/org/koitharu/kotatsu/core/cache/MemoryContentCache.kt b/app/src/main/java/org/koitharu/kotatsu/core/cache/MemoryContentCache.kt index 4fb146d22..ffa9a904e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/cache/MemoryContentCache.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/cache/MemoryContentCache.kt @@ -3,12 +3,10 @@ package org.koitharu.kotatsu.core.cache import android.app.Application import android.content.ComponentCallbacks2 import android.content.res.Configuration -import kotlinx.coroutines.Deferred import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaSource -@Suppress("DeferredResultUnused") class MemoryContentCache(application: Application) : ContentCache, ComponentCallbacks2 { init { @@ -21,18 +19,18 @@ class MemoryContentCache(application: Application) : ContentCache, ComponentCall override val isCachingEnabled: Boolean = true override suspend fun getDetails(source: MangaSource, url: String): Manga? { - return detailsCache[ContentCache.Key(source, url)]?.await() + return detailsCache[ContentCache.Key(source, url)]?.awaitOrNull() } - override fun putDetails(source: MangaSource, url: String, details: Deferred) { + override fun putDetails(source: MangaSource, url: String, details: SafeDeferred) { detailsCache.put(ContentCache.Key(source, url), details) } override suspend fun getPages(source: MangaSource, url: String): List? { - return pagesCache[ContentCache.Key(source, url)]?.await() + return pagesCache[ContentCache.Key(source, url)]?.awaitOrNull() } - override fun putPages(source: MangaSource, url: String, pages: Deferred>) { + override fun putPages(source: MangaSource, url: String, pages: SafeDeferred>) { pagesCache.put(ContentCache.Key(source, url), pages) } diff --git a/app/src/main/java/org/koitharu/kotatsu/core/cache/SafeDeferred.kt b/app/src/main/java/org/koitharu/kotatsu/core/cache/SafeDeferred.kt new file mode 100644 index 000000000..24b2c48d4 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/core/cache/SafeDeferred.kt @@ -0,0 +1,20 @@ +package org.koitharu.kotatsu.core.cache + +import kotlinx.coroutines.Deferred + +class SafeDeferred( + private val delegate: Deferred>, +) { + + suspend fun await(): T { + return delegate.await().getOrThrow() + } + + suspend fun awaitOrNull(): T? { + return delegate.await().getOrNull() + } + + fun cancel() { + delegate.cancel() + } +} diff --git a/app/src/main/java/org/koitharu/kotatsu/core/cache/StubContentCache.kt b/app/src/main/java/org/koitharu/kotatsu/core/cache/StubContentCache.kt index faf9a7237..4750deb98 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/cache/StubContentCache.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/cache/StubContentCache.kt @@ -1,6 +1,5 @@ package org.koitharu.kotatsu.core.cache -import kotlinx.coroutines.Deferred import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaSource @@ -11,9 +10,9 @@ class StubContentCache : ContentCache { override suspend fun getDetails(source: MangaSource, url: String): Manga? = null - override fun putDetails(source: MangaSource, url: String, details: Deferred) = Unit + override fun putDetails(source: MangaSource, url: String, details: SafeDeferred) = Unit override suspend fun getPages(source: MangaSource, url: String): List? = null - override fun putPages(source: MangaSource, url: String, pages: Deferred>) = Unit + override fun putPages(source: MangaSource, url: String, pages: SafeDeferred>) = Unit } diff --git a/app/src/main/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt index be7b3b6a5..439d06494 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/parser/RemoteMangaRepository.kt @@ -1,8 +1,10 @@ package org.koitharu.kotatsu.core.parser +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async -import kotlinx.coroutines.coroutineScope import org.koitharu.kotatsu.core.cache.ContentCache +import org.koitharu.kotatsu.core.cache.SafeDeferred import org.koitharu.kotatsu.core.prefs.SourceSettings import org.koitharu.kotatsu.parsers.MangaParser import org.koitharu.kotatsu.parsers.MangaParserAuthProvider @@ -14,6 +16,8 @@ import org.koitharu.kotatsu.parsers.model.MangaPage import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.parsers.model.MangaTag import org.koitharu.kotatsu.parsers.model.SortOrder +import org.koitharu.kotatsu.utils.ext.processLifecycleScope +import org.koitharu.kotatsu.utils.ext.runCatchingCancellable class RemoteMangaRepository( private val parser: MangaParser, @@ -42,20 +46,20 @@ class RemoteMangaRepository( override suspend fun getDetails(manga: Manga): Manga { cache.getDetails(source, manga.url)?.let { return it } - return coroutineScope { - val details = async { parser.getDetails(manga) } - cache.putDetails(source, manga.url, details) - details - }.await() + val details = asyncSafe { + parser.getDetails(manga) + } + cache.putDetails(source, manga.url, details) + return details.await() } override suspend fun getPages(chapter: MangaChapter): List { cache.getPages(source, chapter.url)?.let { return it } - return coroutineScope { - val pages = async { parser.getPages(chapter) } - cache.putPages(source, chapter.url, pages) - pages - }.await() + val pages = asyncSafe { + parser.getPages(chapter) + } + cache.putPages(source, chapter.url, pages) + return pages.await() } override suspend fun getPageUrl(page: MangaPage): String = parser.getPageUrl(page) @@ -71,4 +75,10 @@ class RemoteMangaRepository( } private fun getConfig() = parser.config as SourceSettings + + private fun asyncSafe(block: suspend CoroutineScope.() -> T) = SafeDeferred( + processLifecycleScope.async(Dispatchers.Default) { + runCatchingCancellable { block() } + }, + ) } diff --git a/app/src/main/java/org/koitharu/kotatsu/details/service/MangaPrefetchService.kt b/app/src/main/java/org/koitharu/kotatsu/details/service/MangaPrefetchService.kt index 6af5e3258..cc5f5b974 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/service/MangaPrefetchService.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/service/MangaPrefetchService.kt @@ -4,9 +4,7 @@ import android.content.Context import android.content.Intent import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.EntryPointAccessors -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.launch import org.koitharu.kotatsu.base.ui.CoroutineIntentService import org.koitharu.kotatsu.core.cache.ContentCache import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga @@ -16,7 +14,6 @@ import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.MangaChapter import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.utils.ext.getParcelableExtraCompat -import org.koitharu.kotatsu.utils.ext.processLifecycleScope import org.koitharu.kotatsu.utils.ext.runCatchingCancellable import javax.inject.Inject @@ -46,16 +43,12 @@ class MangaPrefetchService : CoroutineIntentService() { private suspend fun prefetchDetails(manga: Manga) = coroutineScope { val source = mangaRepositoryFactory.create(manga.source) - processLifecycleScope.launch(Dispatchers.Default) { - runCatchingCancellable { source.getDetails(manga) } - }.join() + runCatchingCancellable { source.getDetails(manga) } } private suspend fun prefetchPages(chapter: MangaChapter) { val source = mangaRepositoryFactory.create(chapter.source) - processLifecycleScope.launch(Dispatchers.Default) { - runCatchingCancellable { source.getPages(chapter) } - }.join() + runCatchingCancellable { source.getPages(chapter) } } companion object {