Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 6960a480 authored by Ricki Hirner's avatar Ricki Hirner
Browse files

Change static singletons to cached singletons with weak references to save...

Change static singletons to cached singletons with weak references to save memory when the objects are not needed anymore
parent 54d1b5ec
Loading
Loading
Loading
Loading
+0 −29
Original line number Diff line number Diff line
package at.bitfire.davdroid

import android.content.Context

abstract class AndroidSingleton<T> {

    var creatingSingleton = false
    var singleton: T? = null

    @Synchronized
    fun getInstance(context: Context): T {
        singleton?.let {
            return it
        }

        if (creatingSingleton)
            throw IllegalStateException("AndroidSingleton::getInstance() must not be called while createInstance()")
        creatingSingleton = true

        val newSingleton = createInstance(context.applicationContext)
        singleton = newSingleton

        creatingSingleton = false
        return newSingleton
    }

    protected abstract fun createInstance(context: Context): T

}
 No newline at end of file
+26 −4
Original line number Diff line number Diff line
@@ -3,8 +3,16 @@ package at.bitfire.davdroid
import android.content.Context
import java.lang.ref.WeakReference

/**
 * A singleton registry that guarantees that there is not more than one instance per class.
 *
 * It uses weak references so that as soon as the singletons are not used anymore, they can be
 * freed by GC.
 */
object Singleton {

    private val currentlyCreating = HashSet<Class<*>>()

    private val singletons = mutableMapOf<Any, WeakReference<Any>>()


@@ -17,6 +25,11 @@ object Singleton {
        }


    @Synchronized
    fun dropAll() {
        singletons.clear()
    }

    @Synchronized
    fun<T> getInstance(clazz: Class<T>, createInstance: () -> T): T {
        var cached = singletons[clazz]
@@ -30,10 +43,19 @@ object Singleton {
            @Suppress("UNCHECKED_CAST")
            return cached.get() as T

        // create new singleton
        // CREATE NEW SINGLETON
        // prevent recursive creation
        if (currentlyCreating.contains(clazz))
            throw IllegalStateException("Singleton.getInstance must not be called recursively")
        currentlyCreating += clazz
        // actually create the instance
        try {
            val newInstance = createInstance()
            singletons[clazz] = WeakReference(newInstance)
            return newInstance
        } finally {
            currentlyCreating -= clazz
        }
    }

}
 No newline at end of file
+29 −28
Original line number Diff line number Diff line
@@ -11,8 +11,8 @@ import androidx.core.database.getStringOrNull
import androidx.room.*
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import at.bitfire.davdroid.AndroidSingleton
import at.bitfire.davdroid.R
import at.bitfire.davdroid.Singleton
import at.bitfire.davdroid.TextTable
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.ui.AccountsActivity
@@ -41,9 +41,9 @@ abstract class AppDatabase: RoomDatabase() {
    abstract fun webDavDocumentDao(): WebDavDocumentDao
    abstract fun webDavMountDao(): WebDavMountDao

    companion object: AndroidSingleton<AppDatabase>() {
    companion object {

        override fun createInstance(context: Context): AppDatabase =
        fun getInstance(context: Context) = Singleton.getInstance<AppDatabase>(context) {
            Room.databaseBuilder(context.applicationContext, AppDatabase::class.java, "services.db")
                .addMigrations(*migrations)
                .fallbackToDestructiveMigration()   // as a last fallback, recreate database instead of crashing
@@ -68,6 +68,7 @@ abstract class AppDatabase: RoomDatabase() {
                    }
                })
                .build()
        }


        // migrations
+4 −3
Original line number Diff line number Diff line
@@ -11,7 +11,7 @@ package at.bitfire.davdroid.settings
import android.content.Context
import android.util.NoSuchPropertyException
import androidx.annotation.AnyThread
import at.bitfire.davdroid.AndroidSingleton
import at.bitfire.davdroid.Singleton
import at.bitfire.davdroid.log.Logger
import java.io.Writer
import java.lang.ref.WeakReference
@@ -26,8 +26,9 @@ class SettingsManager private constructor(
        context: Context
) {

    companion object: AndroidSingleton<SettingsManager>() {
        override fun createInstance(context: Context) = SettingsManager(context)
    companion object {
        fun getInstance(context: Context) =
            Singleton.getInstance(context) { SettingsManager(context) }
    }

    private val providers = LinkedList<SettingsProvider>()
+28 −11
Original line number Diff line number Diff line
package at.bitfire.davdroid

import org.junit.Assert.*
import org.junit.Before
import org.junit.Test
import java.lang.ref.WeakReference

class SingletonTest {

    @Before
    fun prepare() {
        Singleton.dropAll()
    }


    @Test
    fun test_Cache() {
        val obj1 = Singleton.getInstance { Object() }
        assertEquals(obj1, Singleton.getInstance<Object> {
            fail("No new Object must be created")
            Object()
    fun testCache() {
        val obj1 = Singleton.getInstance { Any() }
        assertEquals(obj1, Singleton.getInstance {
            fail("No new Any must be created")
            Any()
        })
    }

    @Test
    fun test_CacheUsesWeakReferences() {
        var obj1: Object? = Singleton.getInstance { Object() }
    fun testCacheUsesWeakReferences() {
        var obj1: Any? = Singleton.getInstance { Any() }
        val refObj1 = WeakReference(obj1)
        obj1 = null

        // no reference anymore, validate
        System.gc()
        Runtime.getRuntime().gc()
        assertNull(refObj1.get())

        // create a new instance
        val obj2 = Singleton.getInstance { Object() }
        assertEquals(obj2, Singleton.getInstance<Object> {
            fail("No new Object must be created")
            Object()
        val obj2 = Singleton.getInstance { Any() }
        assertEquals(obj2, Singleton.getInstance {
            fail("No new Any must be created")
            Any()
        })
    }

    @Test(expected = IllegalStateException::class)
    fun testRecursive() {
        Singleton.getInstance() {
            Singleton.getInstance() {
                Any()
            }
        }
    }

}
 No newline at end of file