Kotlin系列——封裝MMKV及其相關Kotlin特性

這篇文章主要是對MMKV進行封裝,由此瞭解一些Kotlin特性,建議對着示例代碼閱讀文章,示例代碼如下:

MMKVDemo

MMKV簡單介紹

其實在MMKVWiki中已經有很詳細的介紹了,地址如下:

MMKV for Android官方Wiki

MMKV是基於mmap內存映射key-value組件,底層序列化/反序列化使用protobuf實現,性能高穩定性強,而且Android這邊還支持多進程

單線程性能對比

在這裏插入圖片描述

  • 寫入性能

    MMKV遠超於SharedPreferencesSQLite

  • 讀取性能

    MMKVSharedPreferences相近,好於SQLite

多進程性能對比

在這裏插入圖片描述

  • 寫入性能

    MMKV遠超於MultiProcessSharedPreferencesSQLite

  • 讀取性能

    MMKV遠超於MultiProcessSharedPreferencesSQLite

mmap簡單介紹

mmap是一種內存映射的方法,它可以將對象或者文件映射到地址空間,實現文件磁盤地址進程虛擬地址空間中的一段虛擬地址的一一對映關係,實現了這種映射關係後,進程可以採用指針的方式讀寫操作一段內存,而系統自動回寫髒頁面到對應的文件磁盤上,這樣就完成了對文件的操作,而不需要再去調用writeread系統調用函數,同時內核空間對這段區域的修改也直接反映用戶空間,從而可以實現不同進程間文件共享

封裝MMKV

Preferences.kt,代碼如下:

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)
        }
    }

用法如下:

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 = "譚嘉俊"
        stringSet = HashSet<String>().apply {
            for (i in 0 until 100) {
                add("第($i)個")
            }
        }
        parcelable = UserData(name = "譚嘉俊", gender = "男", 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"
    }

}

Kotlin特性

挑幾個語法講解一下:

內聯函數

示例代碼中我創建幾個內聯代理函數,那什麼是內聯函數呢?爲什麼要用內聯函數

內聯函數的原理是編譯器把實現內聯函數字節碼動態插入到每次的調用處

使用高階函數會帶來一些運行時效率損失,因爲在Kotlin中,每一個函數都是一個對象,並且會捕獲一個閉包,即那些在函數體內會訪問到的變量。內存分配虛擬調用都會增加開銷,在很多情況下,使用內聯化Lambda表達式可以消除這類開銷,舉個例子,有這樣一個函數:

fun add(list: MutableList<String>, block: () -> String): String {
    list.add("譚嘉俊")
    return block()
}

然後是這樣調用的:

add(mutableListOf("MutableList")) { "譚嘉俊" }

剛剛也說了,每一個函數都是一個對象,所以後面這段Lambda表達式它也是一個對象,所以調用的時候,其實它會調用block方法,Kotlin是基於JVM的編程語言,所以調用一個方法,其實就是將這個方法入棧的操作,調用結束後就會將這個方法出棧入棧出棧都會有性能的開銷,所以我們可以使用內聯函數,代碼如下:

inline fun add(list: MutableList<String>, block: () -> String): String {
    list.add("譚嘉俊")
    return block()
}

用上內聯函數後,編譯器就會將block方法裏的代碼內聯到調用的地方,而不會再去調用block方法,從而減少了性能的開銷,就像如下代碼:

inline fun add(list: MutableList<String>, block: () -> String): String {
    list.add("譚嘉俊")
    return "譚嘉俊"
}

crossinline

示例代碼中,我用crossinline修飾了gettersetter這兩個參數,crossinline修飾符關鍵字,它要在內聯函數中使用,可以禁止傳遞內聯函數的Lambda表達式中的非局部返回

那什麼是非局部返回呢?在Kotlin中,我們只能對具名函數或者匿名函數使用非限定return來退出,所以我們在退出一個Lambda表達式就必須使用一個標籤,並且在Lambda表達式內部禁止使用裸return,因爲Lambda表達式不能使包含它的函數return,代碼如下:

fun function(block: () -> Unit) =
    print("譚嘉俊")

fun add(list: MutableList<String>) {
    list.add("譚嘉俊")
    function {
        // 不能使add函數在此處return
        return
    }
}

但是如果Lambda表達式傳給的函數是內聯的,return也可以是內聯的,代碼如下:

inline fun function(block: () -> Unit) =
    print("譚嘉俊")

fun add(list: MutableList<String>) {
    list.add("譚嘉俊")
    function {
        // 可以使add函數在此處return
        return
    }
}

這種位於Lambda表達式中,但退出的是包含它的函數叫做非局部返回,就像我們經常用到的forEach就是個內聯函數,代碼如下:

fun function(list: List<String>): Boolean {
    list.forEach {
        if (it == "譚嘉俊") return true // function函數return
    }
    return false
}

如果只是想局部返回forEach的話,可以像如下那樣寫:

fun function(list: List<String>): Boolean {
    list.forEach {
        if (it == "譚嘉俊") return@forEach // 使用forEach隱式標籤,局部返回到forEach
    }
    return false
}

一些內聯函數可能調用的參數不是直接來自函數體,而是來自另一個執行上下文的Lambda表達式,例如:來自局部對象或者嵌套函數,在這種情況下,這個Lambda表達式中也不允許非局部返回,爲了標識這種情況,這個Lambda表達式需要用crossinline修飾符標記,在上面的Preferences.kt文件中,getter參數和setter參數就用到crossinline修飾符,因爲是局部對象ReadWritePropertygetValue方法和setValue方法調用了getter參數和setter參數,代碼就不再貼出來了。

具體化的類型參數

示例代碼中,我用到了Kotlinreified修飾符,在說這個之前,我們大概瞭解下Java泛型

我們知道Java泛型是**”僞泛型“,它會在編譯階段進行類型擦除**。

泛型類型擦除的原則有以下幾點:

  • 擦除類型參數,即擦除**<>**和裏面的內容。
  • 根據類型參數上下界推斷並且替換成原生態類型,例如List原生態類型List
  • 爲了保證類型安全,必要時插入強制類型轉換代碼。
  • Java編譯器自動產生橋接方法來保證類型擦除後仍然具有泛型多態性

類型參數無限制

或者方法定義中的類型參數沒有限制時,例如:或者<?>都被替換成Object示例代碼如下:

類型擦除前:

public class Generic<T> {

    private T value;
    private List<?> list;

    public T getValue() {
        return value;
    }

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

    public void setList(List<?> list) {
        this.list = list;
    }

}

類型擦除後:

public class Generic {

    // T替換成Object
    private Object value;
    // List<?>替換成原生態類型List
    private List list;

    public Object getValue() {
        return value;
    }

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

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

}

類型參數有限制

或者方法定義中的類型參數存在上界的時候,都被替換成它的上界,例如:<? extends Number>都會被替換成Number;當或者方法定義中的類型參數存在下界的時候,都被替換成它的下界,例如:<? super Number>會被替換成Object示例代碼如下:

類型擦除前:

public class Generic<T extends Number> {

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

    public T getValue() {
        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;
    }

}

類型擦除後:

public class Generic {

    // <T extends Number>替換成Number
    private Number value;
    // <? extends Number>替換成Number
    private List<Number> extendsList;
    // <? super Number>替換成Object
    private List<Object> superList;

    public Number getValue() {
        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;
    }

}

以上就是Java泛型類型擦除的大概內容,現在說下Kotlinreified修飾符:

reified修飾符可以保證泛型類型參數在運行時得到保留,要注意的是這個函數必須是內聯函數,原理就是基於內聯函數的工作機制,上面有提及到,每次調用帶有reified的函數,編譯器都知道這次調用中的泛型類型參數類型,然後就會生成對應的不同類型的類型實參字節碼,並且動態插入到調用處,由於生成的字節碼類型實參引用了具體類型,而不是類型參數,所以不會被編譯器擦除。示例代碼如下:

內聯函數startActivity:

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

調用處:

startActivity<MainActivity>()

反編譯後的部分代碼:

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));
}

調用處:

// 被編譯器替換成如下代碼
this.startActivity(new Intent((Context)this, MainActivity.class));

要注意的是,Java代碼不可以調用具體化的類型參數內聯函數,但是可以調用失去內聯特性的普通的內聯函數,因爲具體化類型參數得益於內聯特性,上面也提到,這裏不再贅述了。

委託屬性

示例代碼中,繼承了ReadWriteProperty,並且實現了getValue方法和setValue方法,這裏用到了Kotlin委託屬性

語法:val/var <屬性名>: <類型> by <表達式>

屬性委託不必實現任何的接口,如果是var屬性需要提供getValue方法和setValue方法,如果是val屬性需要提供getValue方法,by後面的表達式就是該委託,屬性對應的get()方法被委託給它的getValue方法,屬性對應的set()的方法被委託給它的setValue方法。示例代碼如下:

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.")
    
}

這裏用到了operator修飾符,可以重載操作符,我們也可以實現ReadWriteProperty接口,它是用於實現讀寫屬性委託的基本接口,這個只是爲了方便我們實現委託屬性,如果你有相同簽名方法,就不必實現這個接口,代碼如下:

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.")

}

除了ReadWriteProperty外,還有另外一個接口:ReadOnlyProperty,這個是爲了委託只讀屬性,只需要重寫它的getValue方法就可以了。

Kotlin標準庫爲幾種委託提供了工廠方法,例如以下說的延遲屬性Lazy就是其中一種:

延遲屬性Lazy

調用延遲屬性有這樣的特徵,第一次拿到屬性的值(調用get()方法)會執行已傳遞給函數的Lambda表達式並且記錄結果,後續調用get()方法只是返回記錄的結果

我們可以看下源碼,提供了三個函數。

lazy(initializer: () -> T)

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

這個函數接受一個Lambda表達式,並且返回Lazy,並且調用SynchronizedLazyImpl函數,而且我們可以得知多個線程去調用這個lazy函數是安全的,代碼如下:

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 val lock = 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 {
                    val typedValue = initializer!!()
                    _value = typedValue
                    initializer = null
                    typedValue
                }
            }
        }

    override fun isInitialized(): Boolean = _value !== UNINITIALIZED_VALUE

    override fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet."

    private fun writeReplace(): Any = InitializedLazyImpl(value)
}

我們可以看到用的是**雙重檢查鎖(Double Checked Locking)**來保證線程安全。

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)
    }

這個函數接受兩個參數,一個是LazyThreadSafetyMode,另外一個是Lambda表達式,並且返回LazyLazyThreadSafetyMode是個枚舉類,代碼如下:

public enum class LazyThreadSafetyMode {

    SYNCHRONIZED,
    PUBLICATION,
    NONE,

}

使用SYNCHRONIZED可以保證只有一個線程初始化實例,實現細節在上面也說過了;使用PUBLICATION允許多個線程併發初始化值,但是只有第一個返回值用作實例的值;使用NONE不會有任何線程安全的保證以及的相關的開銷,所以你如果你確認初始化總是發生在同一個線程的話可以用此模式,減少一些性能上的開銷

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

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

這個函數接受兩個參數,一個是你使用指定的對象(lock),目的是進行同步,另外一個是Lambda表達式,返回的是Lazy,調用的是SynchronizedLazyImpl函數,上面也說過,這裏不再贅述了。

我的GitHub:TanJiaJunBeyond

Android通用框架:Android通用框架

我的掘金:譚嘉俊

我的簡書:譚嘉俊

我的CSDN:譚嘉俊

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章