Kotlin 函数式编程与 Anko 构建布局实现原理分析

Posted by 音视频开发进阶 on Friday, December 22, 2017

之前讲到了如何在 Kotlin 开发中使用 Anko 构建布局

这一篇将是分析其原理。

在分析其实现原理之前还是要补一下 Kotlin 函数式编程的相关知识:

函数式编程

之前有写过一篇文章介绍 Python 的函数式编程。

函数式编程并不是哪门语言所独有的,它是一种编程范式,就像面向对象编程一样。

在函数式编程中,函数是作为一等公民存在的。Kotlin 也可以使用函数式编程。

最直接的表现就是可以声明一个变量,它的类型就是函数。

1    val arg = fun(a:Int,b:Int) = a+b // 变量 arg 是一个函数类型
2    println(arg)    // 打印类型 (kotlin.Int, kotlin.Int) -> kotlin.Int
3    println(arg(1,2))    // 调用该函数,打印结果为 3

对应函数类型的变量,如果在后面添加了小括号()则表示调用该函数,没有则当做变量来使用。

接下来介绍 Kotlin 的几个例子:

拓展函数

拓展函数是 Kotlin 中比较灵活的东西了,可以给类添加各种各样的拓展函数。

最常见的就是给 Context 添加 Toast 的拓展函数。

1	fun Context.showToast(msg: String) {
2	    Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
3	}
4	// Activity 中调用
5   override fun onCreate(savedInstanceState: Bundle?) {
6        super.onCreate(savedInstanceState)
7        setContentView(R.layout.activity_main)
8        showToast("this is toast")
9    }

注意 子类可以调用父类的拓展函数

 1open class A
 2class B:A()
 3fun A.add(a:Int,b:Int):Int{
 4    return a + b
 5}
 6// 子类调用父类的拓展函数
 7fun main(args: Array<String>) {
 8    println(B().add(1,2))
 9}
10// 输出 3 

Lambda 表达式

Kotlin 对 Lambda 表达式添加了好多语法糖。

一个 Lambda 表达式是一个 函数字面值,即一个未声明的函数,但立即作为表达式传递。

Lambda 表达式的语法应该都很熟悉的:

1、Lambda 表达式总是被大括号括着。 2、在 -> 符号之前是参数,参数类型可以省略 3、在 -> 符号之后是函数体

若 Lambda 表达式没有指定返回类型,函数体的最后一个表达式视为返回值

如果 Lambda 的返回类型不是 Unit,那么该 Lambda 主体中的最后一个表达式会视为返回值。

就比如在 Kotlin 中使用 while 语句,while 中不能再使用赋值表达式了。

1while( (i=2) < 3) {}  // while 中不能执行 i = 2 的操作了

上述代码在 Kotlin 中是编译不过的了,这个时候就可以使用 Lambda 表达式。

1 while ({ i=4; i }() > 0){ } // 使用 lambda 表达式,最后还要添加小括号(),表示调用该函数

因为没有指定类型,所以最后的变量 i被视为了返回值,在添加小括号表示调用,就实现了将i赋值并且与 0 进行判断的比较。

最后一个参数是 Lambda 表达式可以放在圆括号之外

如果函数的最后一个参数是一个函数,并且你传递一个 Lambda 表达式作为相应的参数,那么可以在圆括号之外指定它。

比如有如下函数,它只有一个参数,当然就是最后一个参数了。

 1fun passLambda(init:() -> Int){
 2    println("lambda expression as argument")
 3    println(init())
 4}
 5
 6fun main(args: Array<String>) {
 7	// 在圆括号之内调用的方式
 8    val args = fun() :Int = 2 // 声明一个函数作为变量
 9    passLambda(fun():Int = 2)
10    passLambda(args)
11    // 使用 Lambda 表达式在圆括号之外的调用
12    passLambda { -> 3 }    // 没有参数可以直接省略
13    passLambda { 3 }       // 直接省略箭头
14}

Lambda 表达式只有一个参数,使用 it 替代

如果函数字面值只有一个参数,那么可以把它连同->一起省略掉,直接用it来代替。

 1fun singleArgument(init: (a:Int) -> Int){ // 作为参数的函数只有一个参数
 2    println("use it to replace single argument")
 3    println(init(3))
 4}
 5
 6fun main(args: Array<String>) {
 7    // 常规的调用,传递一个 lambda 表达式,需要定义好参数和函数体
 8    singleArgument { a: Int -> a+1 }
 9    // 使用 it 代替只有一个参数的情况,省略了去定义一个参数的情况,直接定义函数体
10    singleArgument { it + 1 }
11}

带接收者的函数字面值

Kotlin 提供了使用指定的 接收者对象 调用函数字面值的功能。

在函数字面值的函数体中,可以调用该接收者对象上的方法而无需任何额外的限定符,这就类似于拓展函数。

带接收者的函数字面值的表达形式如下:

1class ReceiveObject // 定义一个类作为接收者对象
2fun exec(init: ReceiveObject.() -> Int){}
3fun exec(init: ReceiveObject.(a:Int) -> String){}
4fun exec(str:String,init: ReceiveObject.(a:Int,b:Int) -> Int){}

可以看到,与之前的函数定义不同的是,在 函数的参数 前多加了一个类型 ReceiveObject ,这个类型就是指定的接受者对象。有点像是给这个类添加了拓展函数。

 1class ReceiveObject{
 2    fun show(){
 3        println("access")
 4    }
 5}
 6fun exec(init: ReceiveObject.() -> Int){
 7    val receObj = ReceiveObject()
 8    receObj.init() // 接收者对象调用函数字面值,调用方法一,类似于拓展函数的调用
 9}
10
11fun exec(init: ReceiveObject.(a:Int) -> String){
12    val receObj = ReceiveObject()
13    init(receObj,3) // 接收者对象调用函数字面值,调用方法二,在函数字面值中传入接收者对象
14}
15
16fun exec(str:String,init: ReceiveObject.(a:Int,b:Int) -> Int){
17    println(str) // 并没有让 接收者对象调用函数字面值 
18}
19
20fun main(args: Array<String>) {
21    exec {   -> this.show() ;4 } // 在函数体内调用了 接收者对象 的 show 方法
22    exec { a:Int -> println(a.toString());a.toString() } // 没有指定返回类型,返回最后一个表达式
23    exec("print"){      // 最后一个参数的 Lambda 表达式,可以放在圆括号之外
24        a: Int, b: Int -> this.show()
25        a+b
26    }
27}

当接收者类型可以从上下文推断时,Lambda 表达式可以用作带接收者的函数字面值。

这个语法糖特性对于实现 Anko 构建布局至关重要。

在函数体内部可以调用接收者对象的方法,那么假若这个方法又是带接收者类型的方法,那么就可以不断的往下调用了。

举个简单的例子:

 1class Html{
 2    fun body(init:Body.() -> Unit){}
 3    fun head(init:Head.() -> Unit){}
 4}
 5
 6class Body{
 7    fun p(init:P.() -> Unit){}
 8    fun h1(init:H1.() -> Unit){}
 9}
10
11class Head{
12    fun title(init:Title.() -> Unit){}
13}
14
15class Title
16class P
17class H1
18
19fun html(init:Html.() -> Unit){}
20
21fun main(args: Array<String>) {
22    html {
23        // body 和 head 函数都是属于函数体内的调用
24        body {
25            p {  } // p 和 h1 也是属于 Body 内部的函数调用
26            h1 {  }
27        }
28        this.head {  } // this 指接收者 Html 对象
29    }
30}

上面的结构就是调用接收者对象中的方法,而该方法又有其他的接收者对象,如此循环往下。

结合上面提到的语法糖小特性,再来理解这段代码就清晰多了,也能够更好的理解 Anko 构建布局的实现方式。

Anko 构建布局实现分析

有了上面 Kotlin 基础知识的补充,分析 Anko 的实现方式,把对应的特性和语法糖联系起来就好了。

再来看一个 Anko 实现布局的代码:

1	verticalLayout {
2		textView("hello,Anko")
3        button()
4    }

相信这时候再看源码就很简单了。

1inline fun Activity.relativeLayout(init: (@AnkoViewDslMarker _RelativeLayout).() -> Unit): android.widget.RelativeLayout {
2    return ankoView(`$$Anko$Factories$Sdk25ViewGroup`.RELATIVE_LAYOUT, theme = 0) { init() }
3}
4
5inline fun ViewManager.textView(text: CharSequence?): android.widget.TextView {
6    return ankoView(`$$Anko$Factories$Sdk25View`.TEXT_VIEW, theme = 0) {
7        setText(text)
8    }
9}

verticalLayout是 Activity 的一个拓展函数,它的接收者对象是 _RelativeLayout类,而_RelativeLayout类正好是继承自 RelativeLayout的。

RelativeLayout作为一个 ViewGroup 的子类,它也实现了ViewManager接口。

textViewbutton同样是作为拓展函数存在的,只不过它们是对 ViewManager 的拓展。

这样一来,就可以在 verticalLayout函数中去调用 textViewbutton等函数了,因为上面提到过:

子类可以调用父类的拓展函数,可以在函数体内调用接收者对象的方法而无需任何限制。

其他的一些 View 的控件基本都大同小异了,对于我们的自定义 View 也要把它当做 ViewManager 的拓展函数来实现。

理解了 Anko 像层级一样的实现方式,接下来就是看它如何把界面添加到 Activity 上去。

verticalLayout拓展函数最终返回的是一个 ankoView

 1// verticalLayout 函数实现
 2inline fun Activity.verticalLayout(theme: Int = 0, init: _LinearLayout.() -> Unit): LinearLayout {
 3    return ankoView(`$$Anko$Factories$CustomViews`.VERTICAL_LAYOUT_FACTORY, theme, init)
 4}
 5
 6// ankoView 函数实现
 7inline fun <T : View> Activity.ankoView(factory: (ctx: Context) -> T, theme: Int, init: T.() -> Unit): T {
 8    val ctx = AnkoInternals.wrapContextIfNeeded(this, theme)
 9    val view = factory(ctx)
10    view.init()
11    AnkoInternals.addView(this, view)
12    return view
13}

在 ankoView 函数中,第一个参数factory是一个函数,并且指定了返回类型,在这里就是 verticalLayout。它是由 Anko.Factories.CustomViews 这样一个工厂类来提供的。

第三个参数则是带有接收者对象的函数,由 factory函数生成 View 之后再调用 init 函数。当 view 调用 init 函数之后,我们那些 textView 、button 等拓展函数才会被真正调用。

而最后则是通过 AnkoInternals 这个单例类来完成将界面添加到 Activity 中。

它的 addView 方法如下:

 1	fun <T : View> addView(activity: Activity, view: T) {
 2	        createAnkoContext(activity, { AnkoInternals.addView(this, view) }, true)
 3	}
 4	// 第二个参数是带接收者类型的函数
 5    inline fun <T> T.createAnkoContext(
 6            ctx: Context,
 7            init: AnkoContext<T>.() -> Unit,
 8            setContentView: Boolean = false
 9    ): AnkoContext<T> {
10        val dsl = AnkoContextImpl(ctx, this, setContentView)
11        dsl.init()
12        return dsl
13    }

实际调用的是 createAnkoContext 方法,传入的第二个参数init还是一个带接收者类型的函数,只不过这个函数没有参数,而且也不要求返回什么。

最终是由AnkoContextImpl调用了init函数,AnkoContextImplAnkoContext是一个子类。

init函数是实际内容是 AnkoInternals.addView(this, view)

这里参数 this比较重要,因为init函数是一个带接收者类型的函数,所以这里的this指的就是接收者类型AnkoContext

1    fun <T : View> addView(manager: ViewManager, view: T) {
2        return when (manager) {
3            is ViewGroup -> manager.addView(view)
4            is AnkoContext<*> -> manager.addView(view, null)
5            else -> throw AnkoException("$manager is the wrong parent")
6        }
7    }

init被调用时,实际调用的就是上面的代码,在when语句中,因为 manager 参数正好是 AnkoContext类型,最终进入到第二个 case 中去。

由于manager参数代表了接收者类型,而我们使用的接收者类型是 AnkoContextImpl,所以最后的添加布局工作代码在 AnkoContextImpl 类中。

 1    override fun addView(view: View?, params: ViewGroup.LayoutParams?) {
 2        if (view == null) return
 3        if (myView != null) {
 4            alreadyHasView()
 5        }
 6        this.myView = view
 7        if (setContentView) {
 8            doAddView(ctx, view)
 9        }
10    }
11    
12    private fun doAddView(context: Context, view: View) {
13        when (context) {
14            is Activity -> context.setContentView(view)
15            is ContextWrapper -> doAddView(context.baseContext, view)
16            else -> throw IllegalStateException("Context is not an Activity, can't set content view")
17        }
18    }
19    
20    open protected fun alreadyHasView(): Unit = throw IllegalStateException("View is already set: $myView")

上述代码就比较简单理解, 先判断之前是否添加过,没有则添加,有则报错。

到这里,就分析完了 Anko 类似层级一样的调用方式以及将布局界面添加到 Activity 中去。

至于 Anko 是如何将一个 View 添加到一个 ViewGroup 中去的,也是大同小异了。

总结

学习一门语言最好的方式之一就是看他人的优秀代码了。

Kotlin 的语法糖不理解到位了,还是会有点绕的,但是理解了之后,还是觉得挺有意思的,或许能够开拓自己写代码的一种思路,要是能在工程实践中灵活运用就更好了。

原创文章,转载请注明来源:    Kotlin 函数式编程与 Anko 构建布局实现原理分析


相关文章

留言