preface

SharedPreferences is the lightest KeyValue persistence component available to Android developers, and everyone has their own package. And wechat based on MMAP MMKV high-performance KV components are not unfamiliar to everyone. So after Kotlin’s arrival, are you still using Java? Today to bring you have not seen a new version of the ship, fun. Oh no SharedPreferences writing method, accidentally I found an open source library PreferenceHolder, concise writing method suddenly attracted me, let me want to start to try, the result found a lot of small problems, and then a wave of polishing optimization. The final implementation of PowefulPreferenceHolder support multi-process, data encryption and decryption, MMKV, in addition to the native SharedPreferences Hook optimization to reduce ANR, all kinds of ultimate optimization.

Project Github address

Please start to show your support to me! Github.com/forJrking/P…

PreferenceHolder makes SharedPreferences simple

/ / initialization
PreferenceHolder.serializer = GsonSerializer(Gson())
PreferenceHolder.setContext(applicationContext)
// Define the operation class
object Pref: PreferenceHolder() {
    var isFirstInstall: Boolean by bindToPreferenceField(false)
    var time: Long? by bindToPreferenceFieldNullable()
}
/ / value
if(Pref.isFirstInstall) / /...
/ / value
Pref.isFirstInstall = true
// Clean up all data
Pref.clear()
// Supports complex objects, collections, and the like to be serialized to strings using GSON
var savedGame: Game? by bindToPreferenceFieldNullable()
Copy the code

How’s that? No need to write getString(..) at all “putString(..) The internal default is to use field name + “KEY” as the KEY, so that the code definition can avoid duplicate keys, in addition to support custom keys, as shown below

// Record the time of a single day operation
var tempStr: String by bindToPreferenceField("0"."temp_${day()}")
// Get today's time splice key
fun day(times: Long = System.currentTimeMillis()): String {
    val df = SimpleDateFormat("yyyyMMdd", Locale.US)
    return df.format(times)
}
Copy the code

Advantages and disadvantages of a PreferenceHolder

Advantages:

  • Avoid defining a large number of string keys and duplicate keys
  • The concise delegate pattern does not need to be writtenget(..) set(..)
  • Supports serialization and deserialization of object data
  • Support registerOnSharedPreferenceChangeListener (..)

Disadvantages:

  • Does not supportgetAll() remove(..)
  • Customization is not supportedxmlThe name of the
  • Encryption and decryption are not supported
  • Does not support MMKV
  • Multiple processes are not supported
  • Cannot get the commit return value
  • There is a bug with the collection serialization store

It looks very powerful and useful, but there is a bug with PreferenceHolder, which will be explained and fixed at the end of this article. As a perfectionist programmer, I sometimes use MMKV on projects to improve performance, or to communicate data across processes, and occasionally to encrypt user data to ensure security. As a geek, I need all of that. The next step is to hone the principles.

PreferenceHolder optimization polishing

  1. Source code analysis interpretation
bybindToPreferenceField(...) Method is actually calledclass PreferenceFieldBinder<T : Any>(...). : ReadWriteProperty<*, T>{ ... }Copy the code

Now, if you’re familiar with ReadWriteProperty, Kotlin’s property listener delegate uses an ObservableProperty inside, and that’s the implementation of ReadWriteProperty

 /** * Define an attribute delegated to the ReadWriteProperty object returned by Delegates. Vetoable method * Delegates. Vetoable will be modified if the condition is met */
 var listenerProperty: Int by Delegates.vetoable(0, { property, oldValue, newValue ->
     println("Listen to property change: property->${property.name} oldValue->$oldValue newValue->$newValue")
     newValue > 0// The modification succeeds
 })
Copy the code

The ReadWriteProperty interface’s implementation method getValue(..) setValue(..) SharedPreferences get and set

  1. Added MMKV to optimize SharedPreferences

MMKV itself implements SharedPreferences. This is easy. First, define an extension function that adds SharedPreferences and MMKV that support multiple processes. MultiProcessSharedPreferences is the use of ContentProvider MMKV performance contrast source implementation supports multiple processes SharedPreferences, some students may say SharedPreferences supports multiple processes, However, it has been deprecated in the higher version, and the actual use will also have the problem of retrieving data. SharedPreferencesHelper is provided by Teacher Zhang Shaowen in the development master course. It optimizes the ANR of SharedPreferences due to QueuedWork defects. There’s a link at the end of this article to learn more about the SharedPreferences slot. It’s very comprehensive.

/* generate MMKV and sp * @param name XML name default package name @param cryptKey Encryption key MMKV Encryption key SharedPreferences Internal methods do not support encryption * @param isMMKV Specifies whether to use MMKV * @param IsMultiProcess Indicates whether to use multiple processes. It is recommended that MMKV be used together. * This method does not provide MMKV initialization
@JvmOverloads
fun Context.createSharedPreferences(
    name: String? = null,cryptKey: String? = null,
    isMultiProcess: Boolean = false,isMMKV: Boolean = false
): SharedPreferences {
    val xmlName = "${if (name.isNullOrEmpty()) packageName else name}_kv"
    return if (isMMKV) {
        if (com.tencent.mmkv.MMKV.getRootDir().isNullOrEmpty()) {
            Log.e("MMKV"."You forgot to initialize MMKV")
            com.tencent.mmkv.MMKV.initialize(this)}// Using MMKV in this way does not introduce the use of classNotFound
        val mode = if (isMultiProcess) com.tencent.mmkv.MMKV.MULTI_PROCESS_MODE
        else com.tencent.mmkv.MMKV.SINGLE_PROCESS_MODE
        com.tencent.mmkv.MMKV.mmkvWithID(xmlName, mode, cryptKey)
    } else {
        val mode = Context.MODE_PRIVATE
        if (isMultiProcess) {
            MultiProcessSharedPreferences.getSharedPreferences(this, xmlName, mode)
        } else {
            SharedPreferencesHelper.getSharedPreferences(this, xmlName, mode)
        }
    }
}
Copy the code
  1. Add AES encryption and decryption

Since MMKV itself supports encryption, it can be turned on when initializing MMKV above. SharedPreferences does not provide an encryption/decrypt interface, so we can add encryption/decrypt methods to get() and set(), and only encrypt String and serialized data. Look at the final implementation, detailed code see the crypt package implementation

/********* Encryption and decryption extension method *********/
private funString? .encrypt(crypt: Crypt?).: String? = crypt? .encrypt(this) ?: this
private funString? .decrypt(crypt: Crypt?).: String? = crypt? .decrypt(this) ?: this
/********* Encryption and decryption *********/. Encrypted String: :class -> {
    val message = value asString? putString(key, message.encrypt(crypt)) } ... Decrypt the String: :class -> {
    val text = getString(key, default as? String)
    val result = text.decrypt(crypt) ?: default
    result as? T
}
Copy the code

The birth of PowefulPreferenceHolder

  1. Add the dependent
repositories {
    ...
    maven { url 'https://jitpack.io' }
}
dependencies {
   implementation 'com. Google. Code. Gson: gson: 2.8.5' // The need to store objects is not required
   implementation 'com. Tencent: MMKV -static: 1.2.1' // MMKV is not required
   implementation 'com. Making. ForJrking: Preferences: 1.0.5' / / must
}
 // Initialization in Application
  MMKV.initialize(this)...// If MMKV is used, initialize and configure it
 / / must
  PreferenceHolder.context = this.application
 // It is not necessary to serialize object data. Sp does not recommend storing large amounts of data
  PreferenceHolder.serializer = GsonSerializer()
Copy the code
  1. To write an object class, you must use KT
/* * @param name /* * @param name @param cryptKey Null indicates that encryption is not required. @param isMMKV Specifies whether to use MMKV. The default value is false It is recommended that MMKV be used with SP. The default value is false */
object TestSP : PreferenceHolder("name"."cyptyKey",isMMKV,isMultiProcess) {
    var testStr: String by bindToPreferenceField(default) = getString(key,default)
    var coin: Long by bindToPreferenceField(0L)
    var tes: String? by bindToPreferenceFieldNullable() // The default value is NULL or null can be assigned
    // You need to use GsonSerializer
    var savedGame: Game? by bindToPreferenceFieldNullable()
}
//getValue
val str = TestSP.testStr
val coin = TestSP.coin
println(str) //"" or "something"
//setValue
TestSP.testStr = "AX${Random().nextInt(20)}"
TestSP.coin = 100
Copy the code
  1. Java support Issues

This is easy because the delegate method only supports KT, so write the operation class in KT first, and then use getXXX() and setXXX() in Java

PreferenceHolder Bug

I needed to complete a function similar to 7-day check-in, and temporarily used a PreferenceHolder to store data, but found that the data did not change after check-in.

data class Daily(var dayIndex: String, var isClmiaed: Boolean)
/ / Sp action class
object ObjectTest : PreferenceHolder() {
	var taskDaily: MutableList<Daily>? by bindToPreferenceFieldNullable()
}
// Get the data
val items = ObjectTest.taskDaily
// Modify the data. ObjectTest.taskDaily = items items? .forEach { it.isClmiaed =true
}
// Store data
ObjectTest.taskDaily = items

// The data obtained later is not stored last
valItems = all in the ObjectTest. TaskDaily data (isClmiaed ==false)

// Cause The default cache mode is not compatible with collection
PreferenceHolder byBindToPreferenceFieldxxx Last cache parameter defaulttrue
override fun setValue(thisRef: PreferenceHolder, property: KProperty<*>, value: T) {
	// The objects in the collection are not changed after the data is modified
    if (value == field) return // Cached data is' identical 'to incoming data and will not be stored
	field = value
}

//1 Disable caching for collections. Caching = false
val xxx = by bindToPreferenceField(default,null.false)
//2 Modify source code type judgment set array intelligent close
if (caching) {
    if(value == field && ! (valueis Collection<*> || value is Map<*, *> || value is Array<*>)) {
        Log.d("PreferenceHolder"."value is the same as the cache")
        return
    }
    field = value
}
Copy the code

The resources

Count the slot points in SharedPreferences