KotterKnife 库中 Kotlin 高级用法介绍

栏目: IOS · Android · 发布时间: 6年前

内容简介:KotterKnife 库中 Kotlin 高级用法介绍

KotterKnife 库中 Kotlin 高级用法介绍

Kotter Knife 是 Kotlin 版的 Butter Knife 。 用来简化初始化 View 变量的框架,具体使用很简单

class MainActivity : RxAppCompatActivity() {
  val navView: NavigationView by bindView(R.id.navView)

由于 Kotter Knife 使用太简单了,没啥可介绍的。本文打算根据 Kotter Knife 类库代码的发展来介绍一些 Kotlin 语言的特色功能。涉及到 扩展函数、高阶函数(Higher-Order Functions)、Lambdas、代理、泛型 等高级功能。帮助大家理解和灵活运用 Kotlin 的语言特性。

为了便于理解,先来看两个简单的 Kotlin 概念示例。

表达式函数

下面是一个普通的 Kotlin 函数:

fun double(x: Int): Int {
  return 2*x
}

如果函数只有一个表达式,则可以简化为:

fun double2(x: Int): Int = 2*x

这样就不需要函数体 大括号 和 return 关键字了,写代码的效率更高,代码看起来也更高效。

扩展函数

Kotlin 中的扩展函数是用来取代 Java 中大量的助手 工具 类的,比如 java.util.Collections 就包含各种集合操作函数。 使用扩展函数可以直接在对象上调用这些函数,代码看起来更加直观。比如:

fun StringBuilder.add(vararg value: String?): StringBuilder {
  for (item in value)
    append(item)
  return this
}
 
// 上面的扩展函数可以这样使用
val str = StringBuilder()
    .add("a", "b")
    .add("c","d")
    .toString()

vararg 关键字类似于 Java 中的可变长度参数 ... 声明,这样参数 value 就是数组类型的。

上面的扩展函数在 Java 中等价于下面的代码:

static StringBuilder add(StringBuilder ref, String... value) {
    for (String s : value) {
      ref.append(s);
    }
    return ref;
  }
 
  // Java 工具函数这样使用
  StringBuilder sb = add(new StringBuilder(), "a", "b");
  String str = add(sb, "c","d").toString();
 
  // 也可以写成这样
  String str = add(add(new StringBuilder(), "a", "b"), "c","d")
      .toString();  

注意 append(item) 函数的调用,在 Kotlin 扩展函数内,里面的函数体可以直接访问调用这个扩展函数对象,所以可以直接访问 StringBuilderappend() 函数。 而在 Java 助手函数中,需要把 StringBuilder 对象当做参数传递给函数,然后再调用这个参数的 append() 函数。

可以看到 扩展函数使用起来更加自然,就像这个函数本来就是所扩展类的函数一样。

Kotlin 代理

Kotlin 直接支持 Java 中的代理模式,比如

class Example {
    var p: String by Delegate()
}

上面的变量 p 的值由 Delegate 类来代理, Delegate 类 需要有 getValuesetValue 两个函数分别支持获取和设置变量的值。

class Delegate {
    operator fun getValue(thisRef: Any?, property: KProperty<*>): String {
        return "$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.'")
    }
}

这样当你通过 Example 对象来访问 p 属性的值的时候,会调用 Delegate 代理的 getValue 函数,比如

val e = Example()
println(e.p)

的结果如下:

Example@33a17727, thank you for delegating ‘p’ to me!

注意上面代理函数的第一个参数 thisRef: Any? 的值为 Example 的对象 e, property: KProperty<*> 的值为 p 属性的描述。

对于只读变量 val 只需要实现 getValue 函数即可。为了方便代理的实现, Kotlin 定义了两个接口分别应用只读代理和读写代理:

interface ReadOnlyProperty<in R, out T> {
    operator fun getValue(thisRef: R, property: KProperty<*>): T
}
 
interface ReadWriteProperty<in R, T> {
    operator fun getValue(thisRef: R, property: KProperty<*>): T
    operator fun setValue(thisRef: R, property: KProperty<*>, value: T)
}

关于代理的更详细介绍,请参考 Kotlin 官方文档: https://kotlinlang.org/docs/reference/delegated-properties.html

Kotlin 代理中文介绍 http://www.jianshu.com/p/3727598015f1

Kotter Knife 进化史

上面介绍完几个基本概念,下面该看看 Kotter Knife 实现代码的变迁了,学习了解 Kotter Knife 代码的演化,可以像大神们学习下代码是如何逐步优化的。

Kotter Knife 第一个版本

由于 Kotter Knife 是可以应用到 Activity、 Dialog、 View、 Fragment 等类中的,所以实现了各种类的扩展函数,这里为了代码简单,我们只使用 Activity 的实现来说明.

下面是 GitHub 库中第一个发布版本的实现代码,相信在代码发布之前,JakeWharton 大神也优化了各种情况。

在代码关键的实现地方都详细做了解释,值得仔细研读。

<br />// Lazy 类可以看做 Java 中的延迟获取值的缓存实现,用来缓存类 T 的对象。
// Lazy 中定义了一个 EMPTY 常量对象表示 value 尚未赋值
private class Lazy<T> {
  private object EMPTY
  private var value: Any? = EMPTY
 
  // 通过 get 函数来获取缓存的对象 T,如果 value 的值为 EMPTY 对象
  // 说明还没有赋值,所以调用初始化高阶函数 initializer 来计算该对象的值
  // 注意:initializer 类型为 () -> T,说明为一个返回对象 T 的函数
  fun get(initializer: () -> T): T {
    if (value == EMPTY) {
      value = initializer.invoke()
    }
    @Suppress("UNCHECKED_CAST")
    return value as T
  }
}
 
// ViewBinding 类实现了 ReadOnlyProperty 代理接口,是一个只读代理实现类。
// 这个类的构造函数有个 id 参数为 view 的 R.id.xxx 值。
private class ViewBinding<out T : View>(val id: Int) : ReadOnlyProperty<Any, T> {
  // 保存缓存的值
  private val lazy = Lazy<T>()
 
  // 代理读取值的实现函数,该函数的实现是调用 lazy 的 get 函数来返回缓存的对象。
  override fun getValue(thisRef: Any, desc: KProperty<*>): T = lazy.get {
    // 这个花括号中的代码为 lazy get 函数的 initializer 参数,
    // 如果 lazy 的值还没有初始化,则调用 findView 函数获取具体的值,
    // 如果 findView 返回的值为 null 则抛出异常说明所指定的 id 的view 不存在
    findView<T>(thisRef, id)
        ?: throw IllegalStateException("View ID $id for '${desc.name}' not found.")
  }
}
 
// 下面这个是上面 initializer 函数中 findView<T>(thisRef, id) 函数的实现
@Suppress("UNCHECKED_CAST")
private fun <T : View> findView(thisRef: Any, id: Int): T? {
  // 在这个函数中,会先根据 thisRef 参数判断代理类 ViewBinding 的调动对象
  // 的类型,如果是 android.view.View 则调动 view 的  findViewById(id) 函数
  // 如果是 Activity 则就调用 Activity 对象的 findViewById(id) 函数,同样还有
  // Fragment 等类型判断,这里就省略了。
  return when (thisRef) {
    is View -> thisRef.findViewById(id)
    is Activity -> thisRef.findViewById(id)
    else -> throw IllegalStateException("Unable to find views on type.")
  } as T? // 注意这里的类型转换
}
 
// 上面介绍的 缓存类 Lazy、只读代理类 ViewBinding 和 缓存的初始化实现类 findView 
// 三个都是 private 类型的,所以只能在当前代码文件中被访问,下面是公开暴露的扩展
// Activity 的 bindViw 函数的实现。
fun <T : View> Activity.bindView(id: Int): ReadOnlyProperty<Any, T> = ViewBinding(id)

来详细看看上面这个 Activity.bindView() 扩展函数实现,虽然只有一行代码,但是非常值得研究一番。

  1. 首先这个函数是扩展至 Activity 的扩展函数,
  2. 然后该函数定义了一个继承至 View 的泛型对象 T
  3. bindView 函数参数为 Int 值,也就是 view 的 id 值 R.id.xxx
  4. bindView 函数的返回值是 ReadOnlyProperty<Any, T> 类型的,说明是一个只读属性代理
  5. bindView 函数的实现是返回一个 ViewBinding 代理类,参数为 id 值

由于上面定义的 Activity.bindView() 扩展函数是扩展的 Activity,所以使用方式如下:

class MainActivity : RxAppCompatActivity() {
  // 在 Activity 中直接使用 bindView 扩展函数,就像 bindView 函数定义
  // 在 Activity 中一样, bindView 函数返回的是一个只读代理对象,所以修饰符
  // 需要是 val 而不是 var,代理返回的参数类型为 NavigationView
  val navView: NavigationView by bindView(R.id.navView)

这样当你在 MainActivity 中第一次使用 navView 的时候,就会调用 bindView(R.id.navView) 返回的只读代理对象 ViewBinding(id)getValue() 函数来获取所代理的值。根据上面的代码可以一直执行到最终 ActivityfindViewById(id) 函数,然后通过 findViewById(id) 获取到的对象就会缓存到 Lazy 对象中。

如果对上面一段话不太清楚,请回到上面第一版本实现代码中仔细研究下,不然下面的迭代会更加迷糊。

可以看到上面使用 Kotlin 语言各种特性很简单的就实现了一个简单的替代 findViewById 函数来初始化 View 变量的功能。

但是上面的代码还不是最佳方案,还可以继续改进。

注意上面的实现中如果 findViewById 返回 null 则会抛出异常告诉对应的 id view 不存在。其实还有一个可选代理实现会返回 null 而不是抛出异常,为了简化代码上面没有列出来,下面是对应的实现代码:

private class OptionalViewBinding<T : View>(val id: Int) : ReadOnlyProperty<Any, T?> {
  private val lazy = Lazy<T?>()
 
  override fun getValue(thisRef: Any, desc: KProperty<*>): T? = lazy.get {
    findView<T>(thisRef, id) // 注意这里没有抛出异常并且函数的返回类型为 T?(可以为 null)
  }
}

Kotter Knife 代码优化之路

类型限定

在上面 Activity.bindView() 的定义中, Activity.bindView() 返回的值为 ReadOnlyProperty<Any, T> ,注意里面的 Any 说明代理属性的对象为任意类型(Any 类似于 Java 中的 Object 类),但是由于 bindView() 是 Activity 的扩展函数只能在 Activity 对象上调用,所以可以把 Any 替代为 Activity 进一步限定类型

fun <T : View> Activity.bindView(id: Int): ReadOnlyProperty<Any, T> = ViewBinding(id)
 
// 变为
 
fun <T : View> Activity.bindView(id: Int): ReadOnlyProperty<Activity, T> = ViewBinding(id)

修改泛型类型方便代码阅读

为了方便阅读,把代表 View 的泛型对象 T 修改为 V,这样可以添加新的泛型类型容易阅读代码。

fun <T : View> Activity.bindView(id: Int): ReadOnlyProperty<Activity, T> = ViewBinding(id)
 
// 变为
 
fun <V : View> Activity.bindView(id: Int): ReadOnlyProperty<Activity, V> = ViewBinding(id)

然后在 ViewBinding 代理类中添加一个代理调用对象的泛型声明 T

private class ViewBinding<V : View>(val id: Int) : ReadOnlyProperty<Any, V> {
  private val lazy = Lazy<V>()
 
  override fun getValue(thisRef: Any, desc: KProperty<*>): V = lazy.get {
    findView<V>(thisRef, id)
        ?: throw IllegalStateException("View ID $id for '${desc.name}' not found.")
  }
}
 
// 区别在于上面的 get 参数 thisRef 类型为 Any 修改为泛型 T
private class ViewBinding<T, V : View>(val id: Int) : ReadOnlyProperty<T, V> {
  private val lazy = Lazy<V>()
 
  override fun getValue(thisRef: T, desc: KProperty<*>): V = lazy.get {
    findView<V>(thisRef, id)
        ?: throw IllegalStateException("View ID $id for '${desc.name}' not found.")
  }
}

修改 findView 函数返回值类型

然后把 findView 函数返回值的类型由泛型 V? 修改为 View? , 并把返回值的类型转换给移动到 ViewBinding 代理类的 get 函数中。

private fun findView<V : View>(thisRef: Any, id: Int): V? {
  @suppress("UNCHECKED_CAST")
  return when (thisRef) {
    is View -> thisRef.findViewById(id)
    is Activity -> thisRef.findViewById(id)
    is Dialog -> thisRef.findViewById(id)
    is Fragment -> thisRef.getView().findViewById(id)
    is ViewHolder -> thisRef.itemView.findViewById(id)
    else -> throw IllegalStateException("Unable to find views on type.")
  } as V?
}
 
private class ViewBinding<T, V : View>(val id: Int) : ReadOnlyProperty<T, V> {
  private val lazy = Lazy<V>()
 
  override fun getValue(thisRef: T, desc: KProperty<*>): V = lazy.get {
    findView<V>(thisRef, id)
        ?: throw IllegalStateException("View ID $id for '${desc.name}' not found.")
  }
}

上面的代码修改为下面的:

// 由于 Android 里面的 findViewById 函数返回值都是 View,
// 所以这个函数的返回值类型由泛型 V? 修改为 View?
private fun findView(thisRef: Any, id: Int): View? {
  return when (thisRef) {
    is View -> thisRef.findViewById(id)
    is Activity -> thisRef.findViewById(id)
    is Dialog -> thisRef.findViewById(id)
    is Fragment -> thisRef.getView().findViewById(id)
    is ViewHolder -> thisRef.itemView.findViewById(id)
    else -> throw IllegalStateException("Unable to find views on type.")
  }
}
 
private class ViewBinding<T, V : View>(val id: Int) : ReadOnlyProperty<T, V> {
  private val lazy = Lazy<V>()
 
  @suppress("UNCHECKED_CAST")
  override fun getValue(thisRef: T, desc: KProperty<*>): V = lazy.get {
 
  // 在这里把 View 转换为 需要的类型 V?
 
    findView(thisRef, id) as V? 
        ?: throw IllegalStateException("View ID $id for '${desc.name}' not found.")
  }
}

经过上面几步优化后,现在 findView 函数如下

private fun findView(thisRef: Any, id: Int): View? {
  return when (thisRef) {
    is View -> thisRef.findViewById(id)
    is Activity -> thisRef.findViewById(id)
    is Dialog -> thisRef.findViewById(id)
    is Fragment -> thisRef.getView().findViewById(id)
    is ViewHolder -> thisRef.itemView.findViewById(id)
    else -> throw IllegalStateException("Unable to find views on type.")
  }
}
 
private class ViewBinding<T, V : View>(val id: Int) : ReadOnlyProperty<T, V> {
  private val lazy = Lazy<V>()
 
  @suppress("UNCHECKED_CAST")
  override fun getValue(thisRef: T, desc: KProperty<*>): V = lazy.get {
    findView(thisRef, id) as V?
        ?: throw IllegalStateException("View ID $id for '${desc.name}' not found.")
  }
}

findView 也变为扩展函数

然后可以把 findView 这个函数也变为扩展函数,这样就可以简化里面的 findViewById(id) 的调用了,具体修改后的代码如下:

// Any.findView 也变为扩展函数了,这样 thisRef: Any 这个参数就不需要了
private fun Any.findView(id: Int): View? {
  // 由于是扩展函数,就可以直接访问 this 关键字了
  return when (this) {
    // 调用扩展对象上的对应的 findViewById 函数
    is View -> findViewById(id)
    is Activity -> findViewById(id)
    is Dialog -> findViewById(id)
    is Fragment -> getView().findViewById(id)
    is ViewHolder -> itemView.findViewById(id)
    else -> throw IllegalStateException("Unable to find views on type.")
  }
}
 
// 由于前面的改进, ViewBinding 的调用对象为泛型 T 
private class ViewBinding<T, V : View>(val id: Int) : ReadOnlyProperty<T, V> {
  private val lazy = Lazy<V>()
 
  @suppress("UNCHECKED_CAST")
  override fun getValue(thisRef: T, desc: KProperty<*>): V = lazy.get {
 
    // 这里在代理调用对象 T 上调用上面的扩展函数 Any.findView
    thisRef.findView(id) as V?
        ?: throw IllegalStateException("View ID $id for '${desc.name}' not found.")
  }
}

Any.findView(id: Int) 函数作为 ViewBinding 代理的属性

由于 Kotlin 中方法(fun 定义的函数)也是可以作为变量保存到类中的,所以这一步在 ViewBinding 代理类中定义了一个类型为 T.(Int) -> View? 的方法属性,这个类型和 fun Any.findView(id: Int): View? 扩展函数类型一致,这样 Activity.bindView(id: Int) 函数的实现就可以直接把 Any.findView 当做方法引用作为参数来创建 ViewBinding 对象了。 改变如下:

private class ViewBinding<T, V : View>(val id: Int, val findViewFun: T.(Int) -> View?)
  : ReadOnlyProperty<T, V> {
  // 上面 ViewBinding 构造函数中多了一个参数 findViewFun,这是一个方法属性
  // 在下面的 getValue 函数中直接调用了这个方法
 
  private val lazy = Lazy<V>()
 
  @Suppress("UNCHECKED_CAST")
  override fun getValue(thisRef: T, desc: KProperty<*>): V = lazy.get {
 
    // 使用 ViewBinding 的属性 id 作为参数调用属性 findViewFun
 
    thisRef.findViewFun(id) as V?
        ?: throw IllegalStateException("View ID $id for '${desc.name}' not found.")
  }
}
 
// 由于 ViewBinding 添加了一个方法属性参数,所以这里可以直接把
// `Any.findView(id: Int)` 这个函数使用方法引用的方式作为 ViewBinding 构造函数的参数
fun <V : View> Activity.bindView(id: Int)
    : ReadOnlyProperty<Activity, V> = ViewBinding(id, Any::findView)

改进 Any.findView(id: Int) 函数

下面是目前的 findView 函数实现:

private fun Any.findView(id: Int): View? {
  return when (this) {
    // 调用扩展对象上的对应的 findViewById 函数
    is View -> findViewById(id)
    is Activity -> findViewById(id)
    is Dialog -> findViewById(id)
    is Fragment -> getView().findViewById(id)
    is ViewHolder -> itemView.findViewById(id)
    else -> throw IllegalStateException("Unable to find views on type.")
  }
}

Any.findView(id: Int) 扩展函数的功能就是根据所调用的对象类型来调用其合适的方法来查找 id 所对应的 View。 对于 View Activity Dialog 这三个类来说,由于 Android 已经定义了一个查找对应 View 的 findViewById 函数了,所以这里再定义一个扩展函数在里面调用这个 findViewById 显得有些多余,所以这上个对象上调用 findView 来获取 View 的方法可以直接调用 findViewById 函数。 这样就可以做如下修改:

fun <V : View> Activity.bindView(id: Int)
    : ReadOnlyProperty<Activity, V> = ViewBinding(id, Any::findView)
 
    // 修改为 
 
fun <V : View> Activity.bindView(id: Int)
    : ReadOnlyProperty<Activity, V> = ViewBinding(id, Activity::findViewById)

这样对于 Activity 来说, Any.findView 已经没用了,直接使用 Activity 的 findViewById 函数即可。 但是对于 Fragment 以及其他 Android 没有直接定义 findViewById 的控件来说还是需要 Any.findView 这个扩展函数。所以 Any.findView 现在修改为如下内容:

private fun Any.findView(id: Int): View? {
  return when (this) {
    is Fragment -> getView().findViewById(id)
    is RecyclerView.ViewHolder -> itemView.findViewById(id)
    else -> throw IllegalStateException("Unable to find views on type.")
  }
}
 
// 在 Fragment 中使用的时候还需要使用这个扩展的 findView
 
fun <V : View> Fragment.bindView(id: Int)
    : ReadOnlyProperty<Fragment, V> = ViewBinding(id, Any::findView)

把 Any.findView 扩展函数作为具体类的扩展函数

由于 Any.findView 扩展函数扩展的是 Any 对象,这样在里面使用的时候需要判断对象类型,对于有限的几个扩展可以直接把扩展函数定义到具体的类型上,这样代码看起来更直观,这样就可以把 Any.findView 拆分为具体的 实现:

private fun Fragment.findView(id: Int): View? = getView().findViewById(id)
private fun RecyclerView.ViewHolder.findView(id: Int): View? = itemView.findViewById(id)
 
// 为了和 Android 定义的 `findViewById` 函数统一,则可以把 findView 名字也修改为 findViewById
 
private fun Fragment.findViewById(id: Int): View? = getView().findViewById(id)
private fun RecyclerView.ViewHolder.findViewById(id: Int): View? = itemView.findViewById(id)

上面改名以后,相当于在扩展的类上也定义了一个 findViewById 函数,这样属性绑定的时候就可以统一了:

// 之前的调用方式
fun <V : View> Activity.bindView(id: Int): ReadOnlyProperty<Activity, V> 
  = ViewBinding(id, Activity::findViewById) // Activity 调用的
  //是 Android 定义的 findViewById 函数
fun <V : View> Fragment.bindView(id: Int): ReadOnlyProperty<Fragment, V> 
  = ViewBinding(id, Any::findView) // Fragment 调用的是扩展函数  findView   
 
// 扩展函数拆分并改名后的调用  
fun <V : View> Activity.bindView(id: Int): ReadOnlyProperty<Activity, V> 
  = ViewBinding(id, Activity::findViewById) // Activity 调用的
  //是 Android 定义的 findViewById 函数
fun <V : View> Fragment.bindView(id: Int): ReadOnlyProperty<Fragment, V> 
  = ViewBinding(id, Fragment::findViewById) // Fragment 调用的是
  //扩展函数  findViewById   

美化代理类 ViewBinding 的 getValue 函数

现在 ViewBinding 的类的实现如下:

private class ViewBinding<T, V : View>(val id: Int, val findViewFun: T.(Int) -> View?)
  : ReadOnlyProperty<T, V> {
  private val lazy = Lazy<V>()
 
  @Suppress("UNCHECKED_CAST")
  override fun getValue(thisRef: T, desc: KProperty<*>): V = lazy.get {
    thisRef.findViewFun(id) as V?
        ?: throw IllegalStateException("View ID $id for '${desc.name}' not found.")
  }
}

上面 getValue 函数代码看起来有点乱,可以把后面的抛出异常的表达式放到函数中去:

private class ViewBinding<T, V : View>(val id: Int, val findViewFun: T.(Int) -> View?)
  : ReadOnlyProperty<T, V> {
  private val lazy = Lazy<V>()
 
  @Suppress("UNCHECKED_CAST")
  override fun getValue(thisRef: T, desc: KProperty<*>): V = 
      lazy.get { thisRef.findViewFun(id) as V? ?: viewNotFound(id, desc) }
}
 
private fun viewNotFound(id: Int, desc: KProperty<*>): Nothing =
    throw IllegalStateException("View ID $id for '${desc.name}' not found.")

上面定义了新的函数 viewNotFound 并把 getValue 函数代码格式化一下,看起来是不是更简洁明白。

把 findViewById 的方法引用调用给移动单独函数中

现在使用 ViewBinding 代理类的方式如下:

fun <V : View> Activity.bindView(id: Int)
    : ReadOnlyProperty<Activity, V> = ViewBinding(id, Activity::findViewById)
 
fun <V : View> Fragment.bindView(id: Int)
    : ReadOnlyProperty<Fragment, V> = ViewBinding(id, Fragment::findViewById)

为了方便继续重构代码呢,可以把创建 ViewBinding 对象的表达式放到单独的函数中:

// 在 required 函数中创建 ViewBinding 类
private fun <T, V : View> required(id: Int, finder : T.(Int) -> View?): ReadOnlyProperty<T, V>
    = ViewBinding(id, finder)
 
// 下面不直接创建 ViewBinding 类对象,
// 修改为使用上面的 required 函数
fun <V : View> Activity.bindView(id: Int)
    : ReadOnlyProperty<Activity, V> = required(id, Activity::findViewById)
 
fun <V : View> Fragment.bindView(id: Int)
    : ReadOnlyProperty<Fragment, V> = required(id, Fragment::findViewById)

上面的修改是为下面进一步重构做铺垫。

把 Lazy 和 ViewBinding 两个类合并到一个类中

下面是当前 LazyViewBinding 以及 required 方法的代码:

// Lazy 用来延时调用初始化方法获取数据并缓存起来
private class Lazy<T> {
  private object EMPTY
 
  private var value: Any? = EMPTY
 
  fun get(initializer: () -> T): T {
    if (value == EMPTY) {
      value = initializer.invoke()
    }
    @Suppress("UNCHECKED_CAST")
    return value as T
  }
}
 
// ViewBinding 把通过 getValue 实现把获取 View 的功能代理给 Lazy 类
private class ViewBinding<T, V : View>(val id: Int, val findViewFun: T.(Int) -> View?)
  : ReadOnlyProperty<T, V> {
  private val lazy = Lazy<V>()
 
  @Suppress("UNCHECKED_CAST")
  override fun getValue(thisRef: T, desc: KProperty<*>): V =
      lazy.get { thisRef.findViewFun(id) as V? ?: viewNotFound(id, desc) }
}
 
// require 方法使用 ViewBinding 代理类的对象
private fun <T, V : View> required(id: Int, finder : T.(Int) -> View?): ReadOnlyProperty<T, V>
    = ViewBinding(id, finder)

LazyViewBinding 两个类合并以后的代码如下:

// initializer 初始化方法作为 Lazy 的属性,并且 Lazy 是一个只读代理实现
private class Lazy<T, V>(private val initializer : (T, KProperty<*>) -> V) : ReadOnlyProperty<T, V> {
  private object EMPTY
  private var value: Any? = EMPTY
 
  // getValue 中使用 initializer 方法
  override fun getValue(thisRef: T, desc: KProperty<*>): V {
    if (value == EMPTY) {
      value = initializer(thisRef, desc)
    }
    @Suppress("UNCHECKED_CAST")
    return value as V
  }
}
 
private fun <T, V : View> required(id: Int, finder : T.(Int) -> View?): ReadOnlyProperty<T, V>
    = Lazy { t, desc -> t.finder(id) as V? ?: viewNotFound(id, desc) }

这样就消灭了一个类。

由于 kotlin 支持类型推导,所以上面 required 函数的返回值类型也可以省略

private fun <T, V : View> required(id: Int, finder : T.(Int) -> View?): ReadOnlyProperty<T, V>
    = Lazy { t, desc -> t.finder(id) as V? ?: viewNotFound(id, desc) }
 
    // 变为
 
private fun <T, V : View> required(id: Int, finder : T.(Int) -> View?)
    = Lazy { t:T, desc -> t.finder(id) as V? ?: viewNotFound(id, desc) }
    // 由于省略了返回值,所以 Lazy Lambda 表达式中的变量 t 的类型需要做一下限定(t:T)

上面的这一步一步的重构,可以查看 Github 提交记录: https://github.com/JakeWharton/kotterknife/pull/14 ,通过提交记录中的 diff 文件看起来更加简单。

方法引用 Activity::findViewById

由于 Java 6 中不支持方法引用,所以每处的 Activity::findViewById 编译为 Java 字节码的时候会生成一个匿名内部类,这样在 KotterKnife 中一个有 24 处使用 方法引用 的地方,就需要 24 个匿名内部类,为了减少生成的匿名内部类的数量,可以把 Activity::findViewById 的调用放到单独的函数中去,然后其他的地方使用提炼出来的函数。具体问题描述请参考这个链接: https://github.com/JakeWharton/kotterknife/issues/16

fun <V : View> Activity.bindView(id: Int)
    : ReadOnlyProperty<Fragment, V> = required(id, Activity::findViewById)
 
    // 修改为
 
private val Activity.viewFinder: Activity.(Int) -> View?
  get() = Activity::findViewById
 
fun <V : View> Activity.bindView(id: Int)
    : ReadOnlyProperty<Activity, V> = required(id, viewFinder)    

消除反射调用

在 Kotlin 使用方法引用 Activity::findViewById 的时候,还会使用反射来查找方法,比如下面的函数

private val Activity.viewFinder: Activity.(Int) -> View?
  get() = Activity::findViewById

编译为 Java 的字节码为

GETSTATIC butterknife/ButterKnifeKt$viewFinder$1.INSTANCE$ : Lbutterknife/ButterKnifeKt$viewFinder$1;
INVOKESTATIC kotlin/jvm/internal/Reflection.function (Lkotlin/jvm/internal/FunctionReference;)Lkotlin/reflect/KFunction;
CHECKCAST kotlin/jvm/functions/Function2
ARETURN

把上面的方法引用修改为高级函数则可以消除反射:

private val Activity.viewFinder: Activity.(Int) -> View?
  get() = { findViewById(it) }

这样对应的 java 字节码为:

GETSTATIC butterknife/ButterKnifeKt$viewFinder$1.INSTANCE$ : Lbutterknife/ButterKnifeKt$viewFinder$1;
CHECKCAST kotlin/jvm/functions/Function2
ARETURN

这些重构实现了下面四个目标:

  1. 消除运行时的反射调用
  2. 减少运行时堆内存消耗,把 2 个类实例和一个代理变为1 个
  3. 减少需要定义的类
  4. Make it more functional

下面是经过一系列重构后的代码

fun <V : View> Activity.bindView(id: Int)
    : ReadOnlyProperty<Activity, V> = required(id, viewFinder)
 
private val Activity.viewFinder: Activity.(Int) -> View?
  get() = { findViewById(it) }
 
@Suppress("UNCHECKED_CAST")
private fun <T, V : View> required(id: Int, finder: T.(Int) -> View?)
    = Lazy { t: T, desc -> t.finder(id) as V? ?: viewNotFound(id, desc) }
 
private fun viewNotFound(id: Int, desc: KProperty<*>): Nothing =
    throw IllegalStateException("View ID $id for '${desc.name}' not found.")
 
private class Lazy<T, V>(private val initializer: (T, KProperty<*>) -> V) : ReadOnlyProperty<T, V> {
  private object EMPTY
 
  private var value: Any? = EMPTY
 
  override fun getValue(thisRef: T, property: KProperty<*>): V {
    if (value == EMPTY) {
      value = initializer(thisRef, property)
    }
    @Suppress("UNCHECKED_CAST")
    return value as V
  }
}

总结

经过上面对 Kotter Knife 这个小巧的库演进的分析,可以看到 Kotlin 语言提供的一些 Java 语言所不具有的特性,以及这些特性的强大之处,很明显使用 Kotlin 语言可以提高开发效率。当然 Kotlin 语言也有一些 Java 语言所没有的坑, 比如上面方法引用导致的反射调用、还有上面没有提到的 inline 关键字的使用来提高特殊情况下的代码执行效率。

如果你还不确定是否需要在 Android 开发中使用 Kotlin 语言,则可以参考一下 17 位谷歌 Android 开发专家是如何看待 Kotlin 的 这篇文章。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

APP蓝图

APP蓝图

吕皓月 / 清华大学出版社 / 2015-1-1 / 69.00

移动互联网原型设计,简单来说,就是使用建模软件制作基于手机或者平板电脑的App,HTML 5网站的高保真原型。在7.0 之前的版本中,使用Axure RP进行移动互联网的建模也是可以的。比如,对于桌面的网站模型,制作一个1024像素宽度的页面就可以了;现在针对移动设备,制作320像素宽度的页面就好了。但是在新版本的Axure RP 7.0 中,加入了大量对于移动互联网的支持,如手指滑动,拖动,横屏......一起来看看 《APP蓝图》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

html转js在线工具
html转js在线工具

html转js在线工具

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具