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

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

MMKVDemoandroid

MMKV簡單介紹

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

MMKV for Android官方Wikigithub

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

單線程性能對比

MMKVSingleProcessPerformanceComparison.png

  • 寫入性能安全

    MMKV遠超於SharedPreferencesSQLite閉包

  • 讀取性能併發

    MMKVSharedPreferences相近,好於SQLiteapp

多進程性能對比

MMKVMultipleProcessPerformanceComparison.png

  • 寫入性能框架

    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通用框架(Kotlin-MVVM)

個人掘金:譚嘉俊

個人簡書:譚嘉俊

相關文章
相關標籤/搜索