查看原文
其他

“by” the way,探索Kotlin的委托机制

Omnipotent_7 郭霖 2022-12-14


/   今日科技快讯   /

近日,小米宣布投入100亿美元造车后,同航道对手OPPO也准备进场。据悉,OPPO集团也已经在筹备造车事项。与小米一样,OPPO造车计划的推动者也是创始人陈明永,目前陈明永已经在产业链资源和人才方面摸底、调研。

/   作者简介   /

本篇文章来自Omnipotent_7同学的投稿,和大家分享了通过解析ViewModel的创建过程来介绍委托机制的相关内容,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章!

Omnipotent_7的博客地址:
https://blog.csdn.net/weixin_43687181

/   正文   /
 
获取viewModel的新方法

旧方法

有新方法肯定要先介绍一下旧方法。

在传统的viewModel获取中,我们都有这样一个经验——不能在Activity里直接创建viewModel对象。因为ViewModel的生命周期是长于Activity的,如果在Activity的方法内直接创建对象,就失去了viewModel的设计意义了,即独立于activity的生命周期,存放activity数据的仓库。

因此,我们要采用一些外部的方法进行初始化。

class MainActivity : AppCompatActivity() {
    lateinit var viewModel:MainViewModel
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        // ❗ 看下面
        viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
        // ❗ 看上面
        findViewById<Button>(R.id.button).setOnClickListener {
            viewModel.printSomething(System.currentTimeMillis().toString())
        }
    }
}


我们把viewModel定义成一个延迟初始化变量,在onCreate里进行初始化。

有人可能要问了,你这不也是每次重新创建activity时都会调用创建viewModel语句吗?重新创建了里面的数据不也没了?

其实并非如此,而且答案正好回答了为什么不用直接的MainViewModel()来创建对象。

下面我们来看一下这句初始化代码,这个是摘录《第一行代码Android》(第三版)里的创建viewModel方式。

ViewModelProviders.of(this).get(MainViewModel::class.java)

但是实际上这种写法已经被废弃了,查看官网链接可以看到已经被ViewModelProvider取代了。


那么新的写法怎么写呢?

答案如下,这种写法能保证每次获取到的viewModel都是第一次初始化的,从而达到保存数据的作用。

viewModel = ViewModelProvider(this,
        ViewModelProvider.NewInstanceFactory())
        .get(MainViewModel::class.java)

这种写法又是怎么确保viewModel不会被多次初始化的?

这里引入一个知识点:activity会通过一些手段来存储viewModel到其一个ViewModelStore类型的对象里,但是这个存储对象我们一般不直接操作,而是交给ViewModelProvider来操作。如果你愿意,你当然可以直接操作这个ViewModelStore来获取viewModel,因为这就相当于你做了ViewModelProvider的工作了,而且大概率没它做得好(各种错误处理机制的实现)。

OK,回到问题本身。通过传入的参数this,可以获取到当前activity的ViewModelStore,从而获取到上一次初始化的viewModel,避免了多次初始化。

新方法

???怎么现在才到新方法?

是的,在新的版本中,聪明的开发者发现上一种写法太笨了,要写的代码太多了。我们要声明一个延时初始化变量,然后调用上述方法进行获取。能否对其进行精简?答案当然是可以的。而且很幸运,换汤不换药,只要你基本理解了上述获取ViewModel对象过程,就能理解新写法。为了体现新写法和后面要讲的内容重要性,我把两段代码都列了出来。

// 又长又麻烦
class MainActivity : AppCompatActivity() {
    lateinit var viewModel: MainViewModel
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        viewModel = ViewModelProvider(this, ViewModelProvider.NewInstanceFactory())
                .get(MainViewModel::class.java)
    }
}

新写法

// 对onCreate代码0侵入
class MainActivity : AppCompatActivity() {
    // ❗ 看下面
    private val mainViewModel by viewModels<MainViewModel>()
    // ❗ 看上面

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


这样的新写法是由kotlin提供的,viewModels函数实际上是一个扩展函数,想要使用这样的新写法,要在导入以下依赖:

implementation 'androidx.activity:activity-ktx:1.2.2'

很简洁不是吗?这就是我喜欢kotlin的原因,它真正地站在了开发者的角度,就像一个大神,用很高的代码水平帮你实现复杂的功能。坦白说这样有点偷懒的嫌疑,但四舍五入就是JetBrain帮我打工,这波不是血赚?

言归正传,我们注意到上面这一句核心代码就实现了之前的一大段。

private val mainViewModel by viewModels<MainViewModel>()
/*
等价于
lateinit var mainViewModel: MainViewModel
mainViewModel = ViewModelProvider(this, ViewModelProvider.NewInstanceFactory())
                .get(MainViewModel::class.java)
*/


下面就来剖析一下这行代码。但是心急不行,先了解一下什么是委托模式。

什么是委托模式?

从词义上看,by就是“通过”,”经由“的意思。这里是委托模式的一个应用。

那么委托模式到底是什么呢?

请求别人替我完成某些工作,这样的思想就是委托。在Kotlin里,委托有两种,分别是属性委托和类委托。

  • 类委托:一个类,定义时实现了某个接口,就可以把该接口委托给一个对象(该对象可以通过构造函数传入),委托会自动进行拦截,把调用这个接口的操作转发给该对象。

  • 属性委托:把一个变量通过by关键字,委托给别人操作。我们知道,对于一个变量而言,只有两个操作,分别是获取这个变量和修改这个变量,也就是get()和set()。委托就是把这两个操作进行拦截,然后分发给指定的委托对象进行执行。


以上是通俗的理解,下面用几个例子来说明一下。

类委托应用场景

设想有这样的场景:我需要设计一个类,它需要具有MutableMap接口的所有方法和功能,但是比较特别的一点是,这个类有一个支持撤销的功能,即调用recover方法,把上一次put进来的东西删除。

朴素的想法是,自己定义一个MyMap类,实现Map接口,内部维护一个实际的HashMap对象(因为我们实际上也是对其进行一点增强)。记录下每次put进来的key,然后添加一个recover()函数进行处理,调用recover()其实就是移除map中lastKey。好,我们来试试看。


可以看到,由于实现了Map接口,编译器会提示我们实现好几个函数。我们当然可以用编译器的自动生成来构建起需要的类,可实际上呢?我们只关心put()和一个自定义的recover()函数的执行。硬着头皮去实现就不优雅了,Map接口方法少可以,List接口呢?

于是就有了委托这一实现。

class MyMap(private val realMap: HashMap<Int, Int>) : MutableMap<Int, Int> by realMap {

    private var lastKey = 0

    override fun put(key: Int, value: Int): Int? {
        lastKey = key
        return realMap.put(key,value)
    }

    fun recover() {
        realMap.remove(lastKey)
    }
}


是不是很优雅?这里实际操作的map通过构造函数传入,通过by关键字把MutableMap的方法都委托给这个realMap,我们这个类中只关心自己的实现就可以了。看到这里也许就有同学说了,这个用扩展函数也可以完成啊!这个在代码里直接用remove也可以啊!我之前也是这么想的,后来发现所有设计模式本质上都可以通过最基础的代码完成,委托更像是一种思想、一种方式,你当然可以另辟蹊径,就像你当然可以自己重写一个map一样,只要最贴切、最完善地完成目标即可,这也是所有编码工作的核心要义。

属性委托的应用场景

理解了类委托,属性委托也就不难了。

先要做这样一个理解:本质上来说,对一个对象只有两个用法,一个是赋值,一个是取值。看以下代码。

var s = "1"

//取s指向的对象,然后调用方法
s.isBlank()

// 赋值给s
s = "2"


可以类比为一个类中的private成员变量,对于这个变量,无论你想调用它的什么函数,都要先调用它的get()方法,然后再操作。你想对它赋值,就要调用set(XX)操作。

Kotlin中的val也是同理,不过变成了只能读取,不能set的变量而已。也就是只有get方法,没有set方法。

有了这个理解,就可以看看属性委托了。

class Delegate {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        println("代理对象来喽,"+"当前时间:"+System.currentTimeMillis())
        return "abcdefg"
    }
}


fun main() {
    val s: String by Delegate()
    s.length
}

试想这样一个情况,我需要每次调用s的函数时都打印当前系统时间,在主函数中,我们没有直接给s赋值或者初始化,我们把这个工作交给了一个Delegate对象。Delegate对象内部重写了getValue这个方法。当我们每次调用s的时候(注意这里指的调用不仅仅是输出其具体内容,还包括s.length()等一系列方法的调用,原因见前文),就会被拦截,转而调用getValue方法。所以,以上代码的运行结果为。

代理对象来喽,当前时间:1618404268384

代理对象来喽,当前时间:1618404268384

Process finished with exit code 0


“by” 关键字到底是啥

说完了两种委托,我们发现其都有一个关键的地方,都是通过by这个关键字实现的。真的是这样吗?Java又加新关键字了?说加就加?我怎么没听说过?

这就是Kotlin的优点之一了,可以快速响应社区需求。by做了什么工作呢,我们尝试将上面的代码进行反编译成Java代码,也称得上是官方(JVM)对这个by关键字的理解了。

public final class TKt {
   // $FF: synthetic field
   static final KProperty[] $$delegatedProperties = new KProperty[]{(KProperty)Reflection.property0(new PropertyReference0Impl(TKt.class, "s", "<v#0>", 1))};

   public static final void main() {
      // 注释1
      Delegate var10000 = new Delegate();
      KProperty var1 = $$delegatedProperties[0];
      Delegate s = var10000;

      // 注释2
      s.getValue((Object)null, var1).length();
   }

   // $FF: synthetic method
   public static void main(String[] var0) {
      main();
   }
}


前面的Delegate可以理解为一个普通的类(实际上和普通的类也没区别,只是重载了一个getValue方法罢了),就没列出来。

关键在于这个main函数。可以看到,注释1处创建了一个Delegate对象,并且声明了一个s指向这个对象。

这不就是我们前面声明的s吗?它不是被委托给Delegate了吗?对对对,别急,下面就来。

看到注释2,可以发现调用s.length实质上调用了delegate对象里的getValue()方法(还传了一系列参数,但是这两个参数在这个例子的影响不大,不详细讨论),执行了方法里的打印时间,返回了“abcdefg”。然后调用字符串的length()函数。

一切都已经明了~

反编译之后理解了实际过程,那么我们也可以“多此一举”地尝试一下自己实现,就彻底了解“by”关键字的作用了。

class Delegate {
//    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
//        println("代理对象来喽,"+"当前时间:"+System.currentTimeMillis())
//        return "abcdefg"
//    }

    fun myDelegate():String{
        println("代理对象来喽,"+"当前时间:"+System.currentTimeMillis())
        return "abcdefg"
    }
}


fun main() {
    val delegate = Delegate()
    val s = delegate
    s.myDelegate().length
}


本质上这个实现和原来的by写法是基本一样的(省略了getValue的传参),自己的写法每次调用s.myDelegate(),得到返回的结果再操作不是不行,还是那句话,不够优雅。委托让我们能暂时忘了它到底是什么,让我们可以把s直接当成String来处理,剩下的让编译器去想就好了,操心那么多干嘛呢~

/   总结   /

委托模式是一种抽象的设计模式,在我看来,它是代理模式的一个超集,某些时候,委托回退化成代理模式,就如本文中的例子一样,逻辑较为简单的时候,我们也可以用代理的方式实现。

委托适用的场景是对所需对象的大部分实现细节不需要了解,仅需要改变一小部分功能的情况。

by关键字是一个面向编译器的声明,做了以下工作:

  1. 把委托对象(对应上面例子中的s)实例化为一个被委托对象(对应例子中的Delegate对象)

  2. 每次对委托对象的方法调用,都会被转换成调用被委托对象的getValue()方法,该方法返回实质调用对对象

  3. 实质调用对对象执行原来的调用(对应例子中的length方法)


Kotlin真的太甜了~

推荐阅读:
我的新书,《第一行代码 第3版》已出版!
一起来看看Android官推Kotlin-First的图片加载库
万字图文,带你学懂Handler和内存屏障

欢迎关注我的公众号
学习技术或投稿


长按上图,识别图中二维码即可关注

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存