Kotlin系列——封装MMKV及其相关Kotlin特性

这篇文章主要是对MMKV进行封装,由此了解一些Kotlin特性,建议对着示例代码阅读文章,示例代码如下:

MMKVDemo

MMKV简单介绍

其实在MMKVWiki中已经有很详细的介绍了,地址如下:

MMKV for Android官方Wiki

MMKV是基于mmap内存映射key-value组件,底层序列化/反序列化使用protobuf实现,性能高稳定性强,而且Android这边还支持多进程

单线程性能对比

MMKVSingleProcessPerformanceComparison.png
  • 写入性能

    MMKV远超于SharedPreferencesSQLite

  • 读取性能

    MMKVSharedPreferences相近,好于SQLite

多进程性能对比

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<String>原生态类型List
  • 为了保证类型安全,必要时插入强制类型转换代码。
  • Java编译器自动产生桥接方法来保证类型擦除后仍然具有泛型多态性

类型参数无限制

或者方法定义中的类型参数没有限制时,例如:<T>或者<?>都被替换成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;
    }

}

类型参数有限制

或者方法定义中的类型参数存在上界的时候,都被替换成它的上界,例如:<T extends Number><? 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<T>,并且调用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表达式,并且返回Lazy<T>LazyThreadSafetyMode是个枚举类,代码如下:

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<T>,调用的是SynchronizedLazyImpl函数,上面也说过,这里不再赘述了。

我的GitHub:TanJiaJunBeyond

Android通用框架:Android通用框架

我的掘金:谭嘉俊

我的简书:谭嘉俊

我的CSDN:谭嘉俊

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342