This article focuses on encapsulating MMKV to learn about some of Kotlin’s features. It is recommended to read the article against the sample code, which is as follows:

MMKVDemo

Brief introduction to MMKV

In fact, MMKV Wiki has a very detailed introduction, the address is as follows:

MMKV for Android Wiki

MMKV is based on mMAP memory mapping key-value component, the underlying serialization/deserialization using Protobuf implementation, high performance, strong stability, and Android side also supports multi-process.

Single thread performance comparison

  • Write performance

    MMKV is far superior to SharedPreferences and SQLite.

  • Read performance

    MMKV is similar to SharedPreferences and better than SQLite.

Multi-process performance comparison

  • Write performance

    MMKV far beyond MultiProcessSharedPreferences and SQLite.

  • Read performance

    MMKV far beyond MultiProcessSharedPreferences and SQLite.

Brief introduction to MMAP

Mmap is a kind of memory mapping method, which can be the object into the address space, or file mapping implementation file disk address and processes in the virtual address space of a virtual address a couple relationship, implements the mapping relations after the processes of Pointers can be used to read and write operations for a period of memory, the system will automatically back to write dirty pages to the corresponding files on disk, In this way, the operation of files is completed without the need to call write, read and other system call functions. Meanwhile, the modification of this section of the kernel space directly reflects the user space, thus realizing file sharing between different processes.

Encapsulation MMKV

Preferences. Kt, the code is as follows:

package com.tanjiajun.mmkvdemo.utils

import android.os.Parcelable
import com.tencent.mmkv.MMKV
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty

/** * Created by TanJiaJun on 2020-01-11. */
private inline fun <T> MMKV.delegate(
    key: String? = null,
    defaultValue: T.crossinline getter: MMKV. (String, T) -> T,
    crossinline setter: MMKV.(String, T) -> Boolean
): ReadWriteProperty<Any, T> =
    object : ReadWriteProperty<Any, T> {
        override fun getValue(thisRef: Any, property: KProperty< * >): T = getter(key ? : property.name, defaultValue)override fun setValue(thisRef: Any, property: KProperty<*>, value: T){ setter(key ? : property.name, value) } }fun MMKV.boolean(
    key: String? = null,
    defaultValue: Boolean = false
): ReadWriteProperty<Any, Boolean> =
    delegate(key, defaultValue, MMKV::decodeBool, MMKV::encode)

fun MMKV.int(key: String? = null, defaultValue: Int = 0): ReadWriteProperty<Any, Int> =
    delegate(key, defaultValue, MMKV::decodeInt, MMKV::encode)

fun MMKV.long(key: String? = null, defaultValue: Long = 0L): ReadWriteProperty<Any, Long> =
    delegate(key, defaultValue, MMKV::decodeLong, MMKV::encode)

fun MMKV.float(key: String? = null, defaultValue: Float = 0.0F): ReadWriteProperty<Any, Float> =
    delegate(key, defaultValue, MMKV::decodeFloat, MMKV::encode)

fun MMKV.double(key: String? = null, defaultValue: Double = 0.0): ReadWriteProperty<Any, Double> =
    delegate(key, defaultValue, MMKV::decodeDouble, MMKV::encode)

private inline fun <T> MMKV.nullableDefaultValueDelegate(
    key: String? = null,
    defaultValue: T? .crossinline getter: MMKV. (String, T?) -> T,
    crossinline setter: MMKV.(String, T) -> Boolean
): ReadWriteProperty<Any, T> =
    object : ReadWriteProperty<Any, T> {
        override fun getValue(thisRef: Any, property: KProperty< * >): T = getter(key ? : property.name, defaultValue)override fun setValue(thisRef: Any, property: KProperty<*>, value: T){ setter(key ? : property.name, value) } }fun MMKV.byteArray(
    key: String? = null,
    defaultValue: ByteArray? = null
): ReadWriteProperty<Any, ByteArray> =
    nullableDefaultValueDelegate(key, defaultValue, MMKV::decodeBytes, MMKV::encode)

fun MMKV.string(key: String? = null, defaultValue: String? = null): ReadWriteProperty<Any, String> =
    nullableDefaultValueDelegate(key, defaultValue, MMKV::decodeString, MMKV::encode)

fun MMKV.stringSet(
    key: String? = null,
    defaultValue: Set<String>? = null
): ReadWriteProperty<Any, Set<String>> =
    nullableDefaultValueDelegate(key, defaultValue, MMKV::decodeStringSet, MMKV::encode)

inline fun <reified T : Parcelable> MMKV.parcelable(
    key: String? = null,
    defaultValue: T? = null
): ReadWriteProperty<Any, T> =
    object : ReadWriteProperty<Any, T> {
        override fun getValue(thisRef: Any, property: KProperty< * >): T = decodeParcelable(key ? : property.name, T::class.java.defaultValue)

        override fun setValue(thisRef: Any, property: KProperty<*>, value: T){ encode(key ? : property.name, value) } }Copy the code

Usage:

package com.tanjiajun.mmkvdemo.ui.activity

import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import com.tanjiajun.mmkvdemo.R
import com.tanjiajun.mmkvdemo.data.model.UserData
import com.tanjiajun.mmkvdemo.utils.*
import com.tencent.mmkv.MMKV

/** * Created by TanJiaJun on 2020-01-14. */
class MainActivity : AppCompatActivity() {

    private val mmkv: MMKV by lazy { MMKV.defaultMMKV() }

    private var boolean by mmkv.boolean(key = "boolean", defaultValue = false)
    private var int by mmkv.int(key = "int", defaultValue = 0)
    private var long by mmkv.long("long".0L)
    private var float by mmkv.float(key = "float", defaultValue = 0.0F)
    private var double by mmkv.double(key = "double", defaultValue = 0.0)
    private var byteArray by mmkv.byteArray(key = "byteArray")
    private var string by mmkv.string(key = "string")
    private var stringSet by mmkv.stringSet(key = "stringSet")
    private var parcelable by mmkv.parcelable<UserData>("parcelable")

    override fun onCreate(savedInstanceState: Bundle?). {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        boolean = true
        int = 100
        long = 100L
        float = 100F
        double = 100.0
        byteArray = ByteArray(100).apply {
            for (i in 0 until 100) {
                set(i, i.toByte())
            }
        }
        string = "Tan Ka Chun"
        stringSet = HashSet<String>().apply {
            for (i in 0 until 100) {
                add("The first ($i)")
            }
        }
        parcelable = UserData(name = "Tan Ka Chun", gender = "Male", age = 26)

        Log.i(TAG, "boolean:$boolean")
        Log.i(TAG, "int:$int")
        Log.i(TAG, "long:$long")
        Log.i(TAG, "float:$float")
        Log.i(TAG, "double:$double")
        Log.i(TAG, "byteArray:$byteArray")
        Log.i(TAG, "string:$string")
        Log.i(TAG, "stringSet:$stringSet")
        Log.i(TAG, "parcelable:$parcelable")}private companion object {
        const val TAG = "TanJiaJun"}}Copy the code

Kotlin features

Pick out some grammar to explain:

Inline function

In the sample code I created several inline proxy functions. What is an inline function? Why use inline functions?

The idea behind inline functions is that the compiler dynamically inserts the bytecode that implements the inline function at each call.

Using higher-order functions has some run-time efficiency costs, because in Kotlin, every function is an object and captures a closure, that is, variables that can be accessed within the function body. Memory allocation and virtual calls add overhead, and in many cases, such overhead can be eliminated using inline Lambda expressions. For example, a function like this:

fun add(list: MutableList<String>, block: () -> String): String {
    list.add("Tan Ka Chun")
    return block()
}
Copy the code

This is then called like this:

add(mutableListOf("MutableList")) { "Tan Ka Chun" }
Copy the code

Just also said, every function is an object, so behind the Lambda expressions, it is also an object, so called, it invokes the block method, Kotlin is a programming language based on the JVM, so call a method, is put this method into the stack operation, the end of the call will be the way out of the stack, There is a performance overhead for both pushing and pushing, so we can use inline functions as follows:

inline fun add(list: MutableList<String>, block: () -> String): String {
    list.add("Tan Ka Chun")
    return block()
}
Copy the code

With the inline function, the compiler inlines the code in the block method to the call instead of calling the block method, thus reducing the performance overhead, as shown in the following code:

inline fun add(list: MutableList<String>, block: () -> String): String {
    list.add("Tan Ka Chun")
    return "Tan Ka Chun"
}
Copy the code

crossinline

In the sample code, I decorated the getter and setter parameters with Crossinline, a modifier keyword that is used in inline functions to disallow nonlocal returns in Lambda expressions passing inline functions.

So what is a nonlocal return? In Kotlin, we can only exit a named or anonymous function with an unqualified return, so we must use a tag to exit a Lambda expression, and we forbid naked returns inside Lambda expressions. Because Lambda expressions do not return the containing function, the code looks like this:

fun function(block: () -> Unit) =
    print("Tan Ka Chun")

fun add(list: MutableList<String>) {
    list.add("Tan Ka Chun")
    function {
        // Cannot make the add function return here
        return}}Copy the code

But if the function passed by the Lambda expression is inline, the return can also be inline as follows:

inline fun function(block: () -> Unit) =
    print("Tan Ka Chun")

fun add(list: MutableList<String>) {
    list.add("Tan Ka Chun")
    function {
        // You can make the add function return here
        return}}Copy the code

This type of function, which is in a Lambda expression but exits the function that contains it, is called a nonlocal return. ForEach, as we often use, is an inline function, and the code looks like this:

fun function(list: List<String>): Boolean {
    list.forEach {
        if (it == "Tan Ka Chun") return true // function function return
    }
    return false
}
Copy the code

If you just want to partially return to forEach, you can write it like this:

fun function(list: List<String>): Boolean {
    list.forEach {
        if (it == "Tan Ka Chun") return@forEach // Use forEach implicit tags to partially return to forEach
    }
    return false
}
Copy the code

Some inline functions may call arguments not directly from the function body, but from a Lambda expression in another execution context, for example: In this case, non-local returns are also not allowed in the Lambda expression. To mark this case, the Lambda expression needs to be marked with the Crossinline modifier. In the preferences.kt file above, Getter and setter parameters use crossinline modifiers, because the getValue and setValue methods of the local object ReadWriteProperty call getter and setter parameters, and the code is no longer posted.

Externalized type parameters

In the sample code, I use Kotlin’s reified modifier. Before WE get there, let’s have a quick look at Java generics:

We know that Java generics are ** “pseudo-generics”, which do type erasure at compile time **.

The principles of type erasure for generics are as follows:

  • Erases type arguments, that is, erases **<>** and the contents inside.
  • Deduce from the upper and lower bounds of the type parameter and replace it with the primitive type, for example, the primitive type of List is List.
  • To ensure type safety, insert cast code if necessary.
  • The Java compiler automatically generates bridge methods to ensure that the polymorphism of generics remains after type erasure.

Type parameter unrestricted

When there are no restrictions on type parameters in a class or method definition, such as: or
are replaced with Object, as shown in the following example:

Before type erasure:

public class Generic<T> {

    private T value;
    privateList<? > list;public T getValue(a) {
        return value;
    }

    public void setValue(T value) {
        this.value = value;
    }

    public void setList(List
        list) {
        this.list = list; }}Copy the code

After type erasure:

public class Generic {

    // replace T with Object
    private Object value;
    // List
       replace with a List of native ecological types
    private List list;

    public Object getValue(a) {
        return value;
    }

    public void setValue(Object value) {
        this.value = value;
    }

    public void setList(List list) {
        this.list = list; }}Copy the code

Type parameters are limited

When type parameters in a class or method definition have upper bounds, they are replaced with their upper bounds, e.g., and
is replaced with Number; When a type parameter in a class or method definition has a lower bound, it is replaced with its lower bound, e.g.
will be replaced with Object. Example code is as follows:

Before type erasure:

public class Generic<T extends Number> {

    private T value;
    private List<? extends Number> extendsList;
    private List<? super Number> superList;

    public T getValue(a) {
        return value;
    }

    public void setValue(T value) {
        this.value = value;
    }

    public void setExtendsList(List<? extends Number> extendsList) {
        this.extendsList = extendsList;
    }

    public void setSuperList(List<? super Number> superList) {
        this.superList = superList; }}Copy the code

After type erasure:

public class Generic {

    // 
      
        replaces Number
      
    private Number value;
    / / 
       is replaced with Number
    private List<Number> extendsList;
    / / 
       is replaced with Object
    private List<Object> superList;

    public Number getValue(a) {
        return value;
    }

    public void setValue(Number value) {
        this.value = value;
    }

    public void setExtendsList(List<Number> extendsList) {
        this.extendsList = extendsList;
    }

    public void setSuperList(List<Object> superList) {
        this.superList = superList; }}Copy the code

That’s about it. Now for Kotlin’s reified modifier:

The reified modifier ensures that the type parameter of a generic type is preserved at runtime. Note that the function must be inline. As mentioned above, every time a function with reified is called, the compiler knows the type parameter of the generic type. The bytecode of the corresponding type argument is then generated and dynamically inserted into the call. Since the generated bytecode type argument references the specific type, not the type parameter, it is not erased by the compiler. Example code is as follows:

Inline function startActivity:

inline fun <reified T : AppCompatActivity> Activity.startActivity(a) =
    startActivity(Intent(this, T::class.java))
Copy the code

Call:

startActivity<MainActivity>()
Copy the code

Decompiled part of the code:

StartActivity:

public static final void startActivity(@NotNull Activity $this$startActivity) {
   Intrinsics.checkParameterIsNotNull($this$startActivity, "$this$startActivity");
   Context var10003 = (Context)$this$startActivity;
   Intrinsics.reifiedOperationMarker(4."T");
   $this$startActivity.startActivity(new Intent(var10003, AppCompatActivity.class));
}
Copy the code

Call:

// The compiler replaces it with the following code
this.startActivity(new Intent((Context)this, MainActivity.class));
Copy the code

Note that Java code cannot call inline functions of externalized type parameters, but it can call normal inline functions without the inlining feature, because externalized type parameters benefit from the inlining feature, as mentioned above and won’t be repeated here.

Entrusted property

In the sample code, ReadWriteProperty is inherited and the getValue and setValue methods are implemented, using Kotlin’s delegate property.

Syntax: val/var < attribute name >: < type > by < expression >

The delegate of a property does not have to implement any interface. If it is a var property that provides getValue and setValue, and if it is a val property that provides getValue, the expression after BY is the delegate, and the corresponding get() method is delegated to its getValue method. Property’s set() method is delegated to its setValue method. Example code is as follows:

class Delegate {

    operator fun getValue(thisRef: Any? , property:KProperty< * >): String =
        "$thisRef, thank you for delegating '${property.name}' to me!"

    operator fun setValue(thisRef: Any? , property:KProperty<*>, value: String) =
        println("$value has been assigned to '${property.name}' in $thisRef.")}Copy the code

We can override the operator modifier. We can also implement the ReadWriteProperty interface, which is the basic interface for delegating read and write properties. This is just to make it easier to implement delegate properties.

class Delegate : ReadWriteProperty<Any, String> {

    override fun getValue(thisRef: Any, property: KProperty< * >): String =
        "$thisRef, thank you for delegating '${property.name}' to me!"

    override fun setValue(thisRef: Any, property: KProperty<*>, value: String) =
        println("$value has been assigned to '${property.name}' in $thisRef.")}Copy the code

In addition to ReadWriteProperty, there is another interface: ReadOnlyProperty, which delegates read-only properties by overriding its getValue method.

The Kotlin library provides factory methods for several delegates, such as the Lazy attribute:

The delay attribute Lazy

Calling delayed properties has the property that the first time the value of the property is retrieved (calling get()) executes the Lambda expression passed to the function and records the result, while subsequent calls to get() simply return the result of the record.

We can take a look at the source code, which provides three functions.

lazy(initializer: () -> T)

public actual fun <T> lazy(initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer)
Copy the code

This function takes a Lambda expression, returns Lazy, and calls SynchronizedLazyImpl. It is safe for multiple threads to call the Lazy function as follows:

private class SynchronizedLazyImpl<out T>(initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable {
    private var initializer: (() -> T)? = initializer
    @Volatile private var _value: Any? = UNINITIALIZED_VALUE
    // final field is required to enable safe publication of constructed instance
    private vallock = lock ? :this

    override val value: T
        get() {
            val _v1 = _value
            if(_v1 ! == UNINITIALIZED_VALUE) {@Suppress("UNCHECKED_CAST")
                return _v1 as T
            }

            return synchronized(lock) {
                val _v2 = _value
                if(_v2 ! == UNINITIALIZED_VALUE) {@Suppress("UNCHECKED_CAST") (_v2 as T)
                } else {
                    valtypedValue = initializer!! () _value = typedValue initializer =null
                    typedValue
                }
            }
        }

    override fun isInitialized(a): Boolean= _value ! == UNINITIALIZED_VALUEoverride fun toString(a): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet."

    private fun writeReplace(a): Any = InitializedLazyImpl(value)
}
Copy the code

Double Checked Locking ** is used to ensure thread safety.

lazy(mode: LazyThreadSafetyMode, initializer: () -> T)

public actual fun <T> lazy(mode: LazyThreadSafetyMode, initializer: () -> T): Lazy<T> =
    when (mode) {
        LazyThreadSafetyMode.SYNCHRONIZED -> SynchronizedLazyImpl(initializer)
        LazyThreadSafetyMode.PUBLICATION -> SafePublicationLazyImpl(initializer)
        LazyThreadSafetyMode.NONE -> UnsafeLazyImpl(initializer)
    }
Copy the code

LazyThreadSafetyMode this function takes two arguments, a LazyThreadSafetyMode and a Lambda expression, and returns Lazy. LazyThreadSafetyMode is an enumerated class.

public enum class LazyThreadSafetyMode {

    SYNCHRONIZED,
    PUBLICATION,
    NONE,

}
Copy the code

SYNCHRONIZED ensures that there is only one thread initialized instance, implementation details explained above; PUBLICATION allows multiple threads to initialize values concurrently, but only the first return value is used as the value of the instance; Using NONE does not have any thread-safety guarantees and associated overhead, so you can use this mode to reduce some performance overhead if you are sure that initialization always occurs on the same thread.

lazy(lock: Any? , initializer: () -> T)

public actual fun <T> lazy(lock: Any? , initializer: () -> T): Lazy<T> = SynchronizedLazyImpl(initializer, lock)
Copy the code

This function takes two arguments, a Lambda expression that returns Lazy and calls SynchronizedLazyImpl, using the specified object (lock) for synchronization, as described above.

My GitHub: TanJiaJunBeyond

Common Android Framework: Common Android framework

My nuggets: Tan Jiajun

My simple book: Tan Jiajun

My CSDN: Tan Jiajun