使用Anko高速动态构建Android UI

2019 年 12 月更新:就在几天前,Anko 已经正式宣布停止维护,官方宣称以后将推荐使用 Android Jetpack 中的 KTX 与 Compose,Anko 这个承载了 Kotlin 初心的项目正式与大家告别了,官方的最后声明:Goodbye。

2017年Google正式确定Kotlin为Android开发一级语言后,有大量的团队和项目开始尝试使用Kotlin,而Anko作为一款和Kotlin联系极为紧密的开源库,也成为了吸引开发者尝试Kotlin的魅力之一;Anko的使用教程可以参照官方文档,这里不再赘述,本文力求从以下五个方面来介绍Anko在各方面的表现以及从个人的角度来论证其实用价值。

  • Part 1:简介

  • Part 2:优势

  • Part 3:兼容性

  • Part 4:辅助工具

  • Part 5:风险及缺点

Part 1:简介

Anko是一款JetBrains推出的,利用了Kotlin众多语法特性的Android开发函数库;而JetBrain正是Kotlin以及Intellij IDEA的开发商。虽然Google目前没有为其背书,但是也算是一款半官方的产品;Anko一共分为以下四个部分:

  • Anko Commons (Intent,Dialog,Toast等函数式便捷封装)
  • Anko Layouts (使用DSL构建速度大幅提高的UI【重点】)
  • Anko SQLite (便捷的SQLite操作)
  • Anko Coroutines (Kotlin协程辅助)

这四部分分别对应四个Maven库,此外官方还提供了数个针对Android各个support包的封装,需要另外单独集成。
在这四部分中Commons,SQLite,Coroutines三项均是对已有API的封装,使调用者可以使用函数式的方式调用一些系统API;而Layouts正是Anko的一大亮点与特色,它另辟蹊径,使用Kotlin的DSL的方式动态构建Android的UI,从而取代传统的使用XML的方式,这使得Android的UI的生成过程从此更加节省CPU的使用以及降低电池的消耗,是一种值得尝试的新方式。也是本文介绍的重点。

我们通过几个小例子来直观的感受一下Anko构建UI时的代码;

当我们在Activity中创建布局的时候:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    
    verticalLayout {
        padding = dip(30)
        editText {
            hint = "Name"
            textSize = 24f
        }
        editText {
            hint = "Password"
            textSize = 24f
        }
        button("Login") {
            textSize = 26f
        }
    }
}

可以看到,我们无需再调用setContentView()方法去加载布局文件,而是直接在Activity的onCreate()方法中进行UI代码的编写。verticalLayout是官方为了简便使用,特别封装的一个竖直方向的LinearLayout,而editText,Button,等都是我们常见的Android UI控件;而hint,textSize等属性都是它们上级控件的属性,这和我们在XML中给控件使用一些属性是一样的;而dip()函数则是官方提供的dp和px两种单位之间转换的函数,方便我们使用我们习惯的dp的方式来设置一些控件大小的值。

如果你觉的将UI的代码和Activity写在一起不够软件工程,官方也提供了如下的方式把它们分离:

class MyActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
        super.onCreate(savedInstanceState, persistentState)
        MyActivityUI().setContentView(this)
    }
}

class MyActivityUI : AnkoComponent {
    override fun createView(ui: AnkoContext) = with(ui) {
        verticalLayout {
            val name = editText()
            button("Say Hello") {
                onClick { ctx.toast("Hello, ${name.text}!") }
            }
        }
    }
}

当然,不仅仅是Activity需要UI,Fragment,以及一些Dialog中也需要,我们来看看在Fragment中如何创建UI:

class BlankFragment : Fragment() {

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? =
            UI {
                verticalLayout {
                    padding = dip(30)
                    editText {
                        hint = "Name"
                        textSize = 24f
                    }
                    editText {
                        hint = "Password"
                        textSize = 24f
                    }
                    button("Login") {
                        textSize = 26f
                    }
                }
            }.view
}

调用UI {}.view函数即可。

再来看看如何在RecyclerView的Adapter中创建item的UI以及在给Dialog中设置UI的方式:

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): OSLViewHolder {
        val view = AnkoContext.create(mContext).apply {
            verticalLayout {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                    isClickable = true
                    foreground = createTouchFeedbackBorderless(mContext)
                }
                textView {
                    id = TITLE_ID
                    textSize = 22f
                    textColor = black
                }.lparams(wrapContent, wrapContent) {
                    topMargin = dip(16)
                    bottomMargin = dip(16)
                }
                textView {
                    id = CONTENT_ID
                    textColor = black
                }.lparams(wrapContent, wrapContent) {
                    marginStart = dip(16)
                    bottomMargin = dip(16)
                }
            }
        }.view
        return OSLViewHolder(view)
    }

通过AnkoContext.create(context).apply{}.view 的方式就可以轻松创建。

如果你想在DSL中使用你的自定义控件,可以像下面这样写:

//CircleImageView
inline fun ViewManager.circleImageView(init: CircleImageView.() -> Unit): CircleImageView =
        ankoView({ CircleImageView(it) }, theme = 0, init = init)

看起来结构挺复杂,其实就是定义一个ViewManager的内联扩展函数,这个扩展函数的参数是一个带你想要创建的View的类型的接受者的lambda表达式,并且通过调用Anko的ankoView函数来返回我们需要的对象。当然,这不都是千篇一律的,如果你要定义一个可以在Activity中直接调用的DSL API,则需要把它定义成Activity的内联扩展函数。如果你的自定义控件是一个ViewGroup,还需要通过DSL的方式定义它的LayoutParams等,这里不再展开。

如果是一个Android的老手,可能看过示例以后就能大体掌握使用DSL构建UI的语法,但是其中的一些细节还是会增加一些学习成本;如果质疑这样的学习成本是否值得,那我们就要来分析分析这种新方式构建UI到底有什么好处,让我们来看看Part 2。

Part 2:优势

前面我们提到过,使用Anko创建的布局的优势在于节约CPU资源和节省电池电量;直观的体现就是省时和省电;当然,这些优势的描述源自官方文档,现在我们来具体分析一下它为什么省时和省电。

先来对比一下XML和DSL的执行流程,先看看我们熟知的XML:

使用Anko高速动态构建Android UI_第1张图片
XML.png

再来对比一下DSL:

使用Anko高速动态构建Android UI_第2张图片
DSL.png

这样就很直观,XML作为一种标记型语言是直接打包进APK的,每次要加载布局的时候都要系统去IO读取,并解析XML格式,之后才能在动态的代码中生成各种UI元素,而DSL编译后直接和代码一起(因为DSL本身就是Kotlin代码)被编译成.dex文件或者机器码(ART运行环境),这样在运行时我们就大量的节省了IO读取和解析这样的耗时操作,从而达到了更加快速和节省电池用量的目的。

这样我们算是从理论的角度证明了DSL的优点,但是实际上的表现到底如何?我们可以做个实验,我们就从时间这个角度来具体测验一下,DSL到底快多少。

实验之前,我找到两篇博客文章,分别是两位开发者对XML和DSL的对比测试,我们一个一个来看,首先是第一篇;

《Kotlin Android UI利器之Anko Layouts》

使用Anko高速动态构建Android UI_第3张图片
Anko测试1.png

根据他的描述,这位博主的测试是比较完善的,8款机型,30次测试,并且分别测量了Measure,Layout,Draw三大过程,可以看到差距非常明显,速度差距达到了300%,在低端机型上甚至达到了500%之多。

再来看看第二篇博客:

《使用Anko创建快400%的布局》

这本来是一篇国外博主写的文章,上面的地址是中文译文。我们也直接从文章中找到测试结果的图片:

使用Anko高速动态构建Android UI_第4张图片
Anko测试2.png

作者使用了DevMetrics作为测试工具,测试的结论仍然是性能差距达到了惊人的350% — 600%。

看完了他人的测试结论,我想自己测试一下,我使用三星S8作为测试机型,准备了一个含有6个控件(只含有LineadLayout,Button,TextView),深度为两层的布局(XML和DSL)各一份,然后,使用DevMetrics作为测试工具,发现测试结果并没有什么差异,几乎看不出来哪个是XML编写的,哪个是DSL编写的,于是我准备了一张稍微复杂一点的布局,大概含有30个控件,深度为6层的布局,测试结果发生会出现细微变化,DSL大概会比XML快12%左右。

虽然我的测试结果与两位博主的测试结果相差甚远,但我分析实验条件以后的结论是:两位博主大都采用较为老旧的低端机型,而我采用的是旗舰级别的高端机型,高端机型的优异性能在一定程度上会抹消XML和DSL的性能差距;其次,准备的测试布局不够复杂,从我个人的两次实验大概可以看出,布局变复杂以后,XML和DSL的性能会逐渐拉开差距,但是即使是第二次实验时用的布局,控件种类不够多,设置的属性也不够丰富,嵌套的方式也比较单一,但在实际情况中,一个复杂的页面的布局远比我的更复杂;如果布局中控件的总数太少,IO的时间就会很短,如果XML嵌套的层级不够复杂,XML解析的时间也会很短,这就类似如果要测试JsonObject和Gson的性能,一般都会采用结构比较复杂,总长度足够长的Json数据,否则基本很难看出差异。

总而言之,DSL和XML的确存在性能差距,但是具体差多少,这个依赖于实际的手机机型,以及实际的布局复杂度;如果要在项目中引入Anko,可以先从复杂布局的优化入手,这样对于用户来说体验上会更加明显。

Part 3:兼容性

在上面的论述中,我们可以看到Anko的DSL的API支持绝大部分Android UI控件,但是我们在用XML做开发的时候,我们还会用到其它的标签,比如ViewStub,Merge,Include等等,根据我的测试,Anko支持ViewStub和Include标签,不支持Merge标签;也就是说,DSL可以支持我们将XML定义的布局Include进来,这就极大的增加了兼容性,如果我们要在DSL中include在其它地方定义的UI也很容易,我们直接把其它地方构建的UI定义在一个函数内,在DSL内直接调用函数即可。

近些年来,Google官方推出了很多和XML布局有直接关联的库或框架,便于我们实现许多功能;例如ConstraintLayout,一款帮助我们降低UI层级从而优化布局的工具;我们在Anko的github主页上也找到了ConstraintLayout的支持包,需要单独集成:

// ConstraintLayout
implementation "org.jetbrains.anko:anko-constraint-layout:$anko_version"

可见ConstraintLayout官方已经做了支持。

我们另外关注的框架的就是DataBinding,官方提供的用于便捷实现MVVM架构模式的组件,它的数据绑定基于XML,但是我没在Anko的主页找到JetBrain对DataBinding的支持,可见目前JetBrain官方还没有支持的计划。
目前在网上流传较多的是开发者们个人为Anko开发的DataBinding,也有些文章通过举例来讲解如何编写Anko的DataBinding,如果要编写一个这样的框架也不是难事,在DataBinding中,我们需要在XML中定义data,而根据Anko的API设计哲学,我们可以将原先定义data的方式也包装成DSL的形式,这样做甚至比DataBinding更有优势:比如,我们可以让data的定义在编译期就知道其行为是否正确,而如果在XML中定义,不到运行时我们是没法检验data的定义是否正确的。

Part 4:辅助工具

XML有一个优点在于,由于它是静态的,所以我们即使不运行程序,Android Studio也能给我们提供方便的预览功能——可以在运行前提前看到布局的样子,从而进行动态的调整。而Android Studio肯定不支持DSL的预览,因此我们就需要寻找一些辅助工具。Anko的官方文档中提到,他们推出了一款可以运行在Android Studio和intellij IDEA的插件——Anko Support,我们可以方便的在Android Studio中点击Prefercences -> Plugins -> Install JetBrains Plugins然后列表中的第三个就是Anko Support,安装它就行了。

据官方文档介绍,Anko Support只能预览写在AnkoComponent类中的布局......这其实很无语,上面介绍了那么多种花式写法,全都因为不能预览而只能用AnkoComponent这种写法来写了......

既然如此,我们就看看预览AnkoComponent的效果,我写了一个非常简单的布局:




    

    

    

    

    

然后我发现,预览根本啥都没有......

我build了一下工程,预览终于出来了:

使用Anko高速动态构建Android UI_第5张图片
预览.png

需要先build才能预览......这太反人类了,这和编译运行到手机直接看效果相比简直没区别.....我在Anko的github的issues里找到了有人问相同的问题,提问的时间是2018年6月15日,在6月30日的时候有人回复说当前版本有bug,正在等待更新,本文的写作日期是2018年7月17日,使用的Anko版本是0.10.5,使用的Anko Support版本是0.10.5-2,然而,还未有相关更新。

此外,Anko Support提供了一个XML TO DSL的工具,可以方便的实现代码转化操作。我们来试试把刚才测试预览的XML布局转化:

转化得到:

class SomeActivity : Activity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        linearLayout {
            orientation = LinearLayout.VERTICAL
        
            textView("hello") {
                textSize = 14f
            }.lparams(width = wrapContent, height = wrapContent) {
                gravity = Gravity.CENTER_HORIZONTAL
            }
            textView("I'm") {
                textColor = R.color.yellow
                textSize = 16f
            }.lparams(width = wrapContent, height = wrapContent) {
                gravity = Gravity.CENTER_HORIZONTAL
            }
            textView("Anko") {
                textColor = R.color.blue
                textSize = 18f
            }.lparams(width = wrapContent, height = wrapContent) {
                gravity = Gravity.CENTER_HORIZONTAL
            }
            textView("DSL") {
                textColor = R.color.green
                textSize = 20f
            }.lparams(width = wrapContent, height = wrapContent) {
                gravity = Gravity.CENTER_HORIZONTAL
            }
            button("yes") {
                backgroundResource = R.color.blue
            }.lparams(width = matchParent, height = wrapContent)
        }
    }
    
}

它直接转换成了一个Activity,实际上我们并不想要Activity,而是想要一个AnkoComponent。但实际上我们并不指望这种自动化工具来帮我们写代码,就像Android Studio提供的自动将Java转化为Kotlin的功能,转化出来的代码肯定可以正确工作,但是并不符合我们的编程习惯。我们细看转换出来的DSL布局,大体上没有问题,但是还是有bug,它把颜色ID直接设置给了textColor,但实际上在代码中需要将颜色ID解析成颜色的具体值,或是将ID赋值给textColorResource属性,所以这个转换生成的代码的行为也并不完全符合预期,所以这个XML TO DSL的功能只能作为一种参考,想通过直接转换来得到结果,还是不行的。

总结一下Part 4,预览功能目前仍然处于不完善的状态,也许后续会有改善,但对于很多依赖预览功能来写布局的开发者来说,这项缺点也许足以让他们放弃Anko。代码转化功能大体能达到预期效果,但是还是存在bug,而且这并非是杀手级功能,对于已经掌握DSL的开发者来说,基本可有可无,对于初学者来说,如果事先不知道什么样的代码是bug,很可能会陷入困扰。

Part 5:风险以及缺点

上面说了很多Anko的优点,但是任何一种技术都有缺点和风险,要引入一项新技术,我们就必须权衡优势以及缺点,才能得到最终结论。我认为Anko DSL目前还存在以下问题:

  • 代码可读性要略差于XML

  • 预览功能有很大限制

  • 会增加包大小

  • 不能绝对保证它的行为完全符合预期

  • 没有Google的背书

可读性要略差于XML

XML的可读性是非常高的,它使用的标签化有很高的辨识度。而如果使用DSL构建有多层嵌套的布局时,你可能常常看不清楚,代码块结尾的“}”是属于谁的,加上DSL的LayoutParams独特的设置方式————在代码块结尾调用lParams().{}函数,会更加降低代码层级辨识度。

预览功能有很大限制

这一点在Part 4中已经阐述了,Anko Support插件也只能预览AnkoComponent的子类,而且当前版本的Bug导致它只能先build再预览。

会增加包大小

这一点算是对我们所有App开发者都有较大影响的一项缺点,我们来测试一下引入Anko后APK文件增加的尺寸。我创建了一个全新的Android Project,里面只有一个自动生成的Activity。在我们不引入Anko的时候,我们的项目依赖如下:

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:28.0.0-alpha3'
    implementation 'com.android.support:support-v4:28.0.0-alpha3'
    implementation 'com.android.support:design:28.0.0-alpha3'
    implementation 'com.android.support.constraint:constraint-layout:1.1.2'
    implementation "com.android.support:recyclerview-v7:28.0.0-alpha3"
    implementation "com.android.support:cardview-v7:28.0.0-alpha3"
    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"

    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}

基本只有一些Android官方提供的支持包,我们build一下,来看看debug版APK的包大小:

使用Anko高速动态构建Android UI_第6张图片
没有Anko.png

可以看到大小是2.7MB。

再来看看加上Anko后的项目依赖:

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:28.0.0-alpha3'
    implementation 'com.android.support:support-v4:28.0.0-alpha3'
    implementation 'com.android.support:design:28.0.0-alpha3'
    implementation 'com.android.support.constraint:constraint-layout:1.1.2'
    implementation "com.android.support:recyclerview-v7:28.0.0-alpha3"
    implementation "com.android.support:cardview-v7:28.0.0-alpha3"
    implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"

    // Anko Commons
    implementation "org.jetbrains.anko:anko-commons:$anko_version"

    // Anko Layouts
    implementation "org.jetbrains.anko:anko-sdk25:$anko_version"

    // Appcompat-v7 (only Anko Commons)
    implementation "org.jetbrains.anko:anko-appcompat-v7-commons:$anko_version"

    // Appcompat-v7 (Anko Layouts)
    implementation "org.jetbrains.anko:anko-appcompat-v7:$anko_version"

    // CardView-v7
    implementation "org.jetbrains.anko:anko-cardview-v7:$anko_version"

    // Design
    implementation "org.jetbrains.anko:anko-design:$anko_version"
    implementation "org.jetbrains.anko:anko-design-coroutines:$anko_version"

    // GridLayout-v7
    implementation "org.jetbrains.anko:anko-gridlayout-v7:$anko_version"

    // RecyclerView-v7
    implementation "org.jetbrains.anko:anko-recyclerview-v7:$anko_version"
    implementation "org.jetbrains.anko:anko-recyclerview-v7-coroutines:$anko_version"

    // Support-v4 (only Anko Commons)
    implementation "org.jetbrains.anko:anko-support-v4-commons:$anko_version"

    // Support-v4 (Anko Layouts)
    implementation "org.jetbrains.anko:anko-support-v4:$anko_version"

    // ConstraintLayout
    implementation "org.jetbrains.anko:anko-constraint-layout:$anko_version"

    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}

这样的项目依赖包含Anko的Commons和Layouts这两部分以及其它针对Support包UI的支持库,由于SQLite和Coroutines并不是每个项目都会用得到,所以这里没有包含,我们再来看看包的大小:

使用Anko高速动态构建Android UI_第7张图片
有Anko.png

3.2MB,相比之前也就增加了0.5MB,我觉得可以接受,特别是对于大项目来说,几乎不算什么;就算是对于用户来说刷微博看一张大图都不止0.5MB;所以能否接受这样的包大小增加,见仁见智。

不能绝对保证它的行为完全符合预期

我不否认,Anko中存在bug;简述一个我自己之前遇到的例子,在5.0以上的系统中,只要在style中设置statusBarColor为透明,然后在Activity中使用DrawerLayout就可以实现沉浸式的侧滑菜单,但是行为完全一样的代码使用Anko DSL来编写,状态栏的颜色就变成了诡异的白色,且DrawerLayout的侧滑菜单无法正常延伸到状态栏。这个问题我在Google中搜索后,发现StackOverFlow上有人遇到了同样的问题,但是所有答案给出的解法均无法正常解决。这个问题的详细描述可以参照下面这个链接:

https://stackoverflow.com/questions/39296436/statusbar-is-not-transparent-but-white

当然这样的Bug我也只遇到过这一个,虽然Anko存在这样的问题,但不代表类似的问题比比皆是。

没有Google的背书

Google在2017年确定Kotlin为Android的一级语言后并没有顺带大力推行Anko。而在2017年底Google推出了官方的Kotlin辅助函数库————Android KTX,KTX和Anko在很多功能上都有重叠,而KTX中唯一没有的就是Anko的DSL动态构建UI,可见Google要么是还在观望态度,要么就是也不打算将DSL构建UI的方式纳入主流体系,加上年初开始,特别是2018年的I/O大会,Google开始强推跨平台框架Flutter,可见Google官方的重心以后都会放在这里,DSL的转正可谓是遥遥无期。

但这一切不代表以后都没有任何转机;参考之前的例子,最在在Android上使用的HTTP底层封装的API一款是Apache提供的HttpClient,一款是JDK中自带的HttpURLConnection,HttpClient自带严重BUG,直接在Android 6.0以后被彻底移除,而HttpURLConnection的API太过复杂,开发者都觉得很麻烦,这时,Android之神Jake Wartton的团队开发的开源库OkHttp受到了广泛认可,并最终被Google加入了官方API中,而Jake Wartton本人也被Google招安,所以如果以后Anko DSL发展的更为成熟,也许也会受到Google官方认可。

结论

结合上面的所有讨论,个人认为,如果你正要开始一个新项目,或是准备彻底重写一个当前的项目,都可以试着使用Anko。引入Anko不代表你一定要彻底放弃XML布局,你可以尝试使用Anko重写一些UI特别复杂的布局以在一定程度上降低布局的生成时间,或是其它开销本来就不大的布局里仍使用XML编写的方式;或是你也可以使用Anko来构建绝大部分的UI,只有当你遇到Part 5中介绍的bug时再退守XML的方式;总而言之,如何使用都取决于开发者本人。虽然Anko到目前为止仍未成为主流,但是我仍然认为这是一项值得一试的亮点技术。

你可能感兴趣的:(使用Anko高速动态构建Android UI)