Skip to content

Instantly share code, notes, and snippets.

@tprochazka
Last active February 10, 2024 07:44
Show Gist options
  • Save tprochazka/d91d89ec54bd6c3c1cb46f62faf3c12c to your computer and use it in GitHub Desktop.
Save tprochazka/d91d89ec54bd6c3c1cb46f62faf3c12c to your computer and use it in GitHub Desktop.
ANR free implementation of SharedPreference
/*
* Copyright 2020 AVAST Software s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.avast.android.utils
import android.content.SharedPreferences
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.annotation.VisibleForTesting
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
/**
* ANR free implementation of SharedPreferences.
*
* Fast fix for ANR caused by writing all non written changes on main thread during activity/service start/stop.
*
* Disadvantage of current implementation:
* - OnSharedPreferenceChangeListener is called after all changes are written to disk.
* - If somebody will call edit() apply() several times after each other it will also several times write whole prefs file.
*
* Usage:
*
* Override this method in your Application class.
*
* public SharedPreferences getSharedPreferences(String name, int mode) {
* return NoMainThreadWriteSharedPreferences.getInstance(super.getSharedPreferences(name, mode), name);
* }
*
* You need to override also parent activity, because if somebody will use activity context instead
* of the application one, he will get a different implementation, you can do something like
*
* public SharedPreferences getSharedPreferences(String name, int mode) {
* return getApplicationContext().getSharedPreferences(name, mode);
* }
*
* @author Tomáš Procházka (prochazka)
*/
@RequiresApi(11)
class NoMainThreadWriteSharedPreferences private constructor(private val sysPrefs: SharedPreferences, val name: String) :
SharedPreferences {
private val preferencesCache: MutableMap<String, Any?> = HashMap()
companion object {
private val executor: ExecutorService = Executors.newSingleThreadExecutor()
private val INSTANCES: MutableMap<String, NoMainThreadWriteSharedPreferences> = HashMap()
@JvmStatic
fun getInstance(sharedPreferences: SharedPreferences, name: String): SharedPreferences {
return INSTANCES.getOrPut(name, { NoMainThreadWriteSharedPreferences(sharedPreferences, name) })
}
/**
* Remove all instances for testing purpose.
*/
@VisibleForTesting
@JvmStatic
fun reset() {
INSTANCES.clear()
}
}
init {
preferencesCache.putAll(sysPrefs.all)
}
override fun contains(key: String?) = preferencesCache[key] != null
override fun getAll() = HashMap(preferencesCache)
override fun getBoolean(key: String, defValue: Boolean): Boolean {
return preferencesCache[key] as Boolean? ?: defValue
}
override fun getInt(key: String, defValue: Int): Int {
return preferencesCache[key] as Int? ?: defValue
}
override fun getLong(key: String, defValue: Long): Long {
return preferencesCache[key] as Long? ?: defValue
}
override fun getFloat(key: String, defValue: Float): Float {
return preferencesCache[key] as Float? ?: defValue
}
override fun getStringSet(key: String, defValues: MutableSet<String>?): MutableSet<String>? {
@Suppress("UNCHECKED_CAST")
return preferencesCache[key] as MutableSet<String>? ?: defValues
}
override fun getString(key: String, defValue: String?): String? {
return preferencesCache[key] as String? ?: defValue
}
override fun edit(): SharedPreferences.Editor {
return Editor(sysPrefs.edit())
}
override fun registerOnSharedPreferenceChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) {
sysPrefs.registerOnSharedPreferenceChangeListener(listener)
}
override fun unregisterOnSharedPreferenceChangeListener(listener: SharedPreferences.OnSharedPreferenceChangeListener) {
sysPrefs.unregisterOnSharedPreferenceChangeListener(listener)
}
inner class Editor(private val sysEdit: SharedPreferences.Editor) : SharedPreferences.Editor {
private val modifiedData: MutableMap<String, Any?> = HashMap()
private var keysToRemove: MutableSet<String> = HashSet()
private var clear = false
override fun commit(): Boolean {
submit()
return true
}
override fun apply() {
submit()
}
private fun submit() {
synchronized(preferencesCache) {
storeMemCache()
queuePersistentStore()
}
}
private fun storeMemCache() {
if (clear) {
preferencesCache.clear()
clear = false
} else {
preferencesCache.keys.removeAll(keysToRemove)
}
keysToRemove.clear()
preferencesCache.putAll(modifiedData)
modifiedData.clear()
}
private fun queuePersistentStore() {
try {
executor.submit {
sysEdit.commit()
}
} catch (ex: Exception) {
Log.e("NoMainThreadWritePrefs", "NoMainThreadWriteSharedPreferences.queuePersistentStore(), submit failed for $name")
}
}
override fun remove(key: String): SharedPreferences.Editor {
keysToRemove.add(key)
modifiedData.remove(key)
sysEdit.remove(key)
return this
}
override fun clear(): SharedPreferences.Editor {
clear = true
sysEdit.clear()
return this
}
override fun putLong(key: String, value: Long): SharedPreferences.Editor {
modifiedData[key] = value
sysEdit.putLong(key, value)
return this
}
override fun putInt(key: String, value: Int): SharedPreferences.Editor {
modifiedData[key] = value
sysEdit.putInt(key, value)
return this
}
override fun putBoolean(key: String, value: Boolean): SharedPreferences.Editor {
modifiedData[key] = value
sysEdit.putBoolean(key, value)
return this
}
override fun putStringSet(key: String, values: MutableSet<String>?): SharedPreferences.Editor {
modifiedData[key] = values
sysEdit.putStringSet(key, values)
return this
}
override fun putFloat(key: String, value: Float): SharedPreferences.Editor {
modifiedData[key] = value
sysEdit.putFloat(key, value)
return this
}
override fun putString(key: String, value: String?): SharedPreferences.Editor {
modifiedData[key] = value
sysEdit.putString(key, value)
return this
}
}
}
@tprochazka
Copy link
Author

I will think about it if there is no synchronization issue. But generally, I think that it will bring no difference. Because system shared preference itself loading whole properties file to memory anyway. So preferencesCache.putAll(sysPrefs.all) is just an in-memory operation that will be much faster than loading and parsing files from the storage.

@pawan52tiwari
Copy link

how do u make sure all the apps are using custom SharedPreferences,
You also need to implement file creating and management part

@tprochazka
Copy link
Author

how do u make sure all the apps are using custom SharedPreferences,
You also need to implement file creating and management part

It is not enough explained in the class-level documentation. You need to use a custom Application class and all 3rd party libraries that will use a standard way context.getSharedPreferences() will then use the implementation provided by your application class.

@dimaslanjaka
Copy link

convert to java pls

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment