引言
前段時間寫了一篇Kotlin語法入門的文章,還沒有看過的盆友請戳(這里),有的可能看完之后已經(jīng)開始嘗試用kotlin來寫代碼了。不過上篇體現(xiàn)的僅僅是針對于Kotlin相較于Java在用法上的擴(kuò)展性以及寫法上的簡潔性,那么Android中還有另一個重要的組成部分,布局文件呢?接下來我們就繼續(xù)看一下Anko(基于Kotlin的擴(kuò)展庫)對于Android傳統(tǒng)布局文件XML做的改進(jìn)及優(yōu)化,以及工作原理。
定義
Anko是Kotlin為Android推出的第三方庫,旨在提升Android界面的開發(fā)效率,使代碼更簡潔易懂并更容易閱讀。Anko總共分為以下四個部分:
- Anko Commons: 輕量級類庫包括intent,dialog,logging等幫助類
- Anko Layouts:快速的空安全的方式來寫動態(tài)的布局
- Anko SQLite:關(guān)于Android SQLite查詢語句DSL和容器解析器
- Anko Coroutines:Coroutines提供了一種長時間阻塞線程的解決方案,并且代之以開銷更小和更可控的操作(suspension of a coroutine)
我們可以看到,Anko不僅僅可以用來寫布局,更加可以做一些基礎(chǔ)支持工具,比如操作數(shù)據(jù)庫,用Intent進(jìn)行數(shù)據(jù)傳遞等等,本文著重探討的是Anko Layouts這一部分。
優(yōu)勢
- Anko可以讓我們在源碼中寫UI布局,嚴(yán)格的編譯檢查可以保證類型安全,不會出現(xiàn)類型轉(zhuǎn)換異常
- 沒有多余的CPU開銷來解析XML文件
- 我們可以把Anko DSL約束放在函數(shù)中,提高代碼復(fù)用率,比原有xml的include更強(qiáng)大
用法
如下,是應(yīng)用中的關(guān)于我們界面布局文件:
由于布局非常簡單,就不多解釋了,那么如果將上述布局用Anko來寫如下所示:
verticalLayout {
verticalLayout {
backgroundResource = R.mipmap.setting_about_us_bg
setGravity(Gravity.CENTER_HORIZONTAL)
imageView {
backgroundResource = R.mipmap.setting_about_us_logo_ic
}.lparams(width = wrapContent, height = wrapContent){
topMargin = dip(114)
}
mTvVersion = textView{
textSize = 14f
textColor = R.color.yx_text_desc
}.lparams(width = wrapContent, height = wrapContent){
topMargin = dip(9)
bottomMargin = dip(186)
}
verticalLayout {
setGravity(Gravity.CENTER_HORIZONTAL)
textView{
text = ResourcesUtil.getString(R.string.about_check_update)
textSize = 14f
backgroundResource = R.drawable.selector_about_us_btn_bg
textColor = R.color.yx_text_desc
gravity = Gravity.CENTER
}.lparams(width = dip(127), height = dip(36)){
bottomMargin = dip(20)
}
textView{
text = ResourcesUtil.getString(R.string.private_rights)
textSize = 14f
backgroundResource = R.drawable.selector_about_us_btn_bg
textColor = R.color.yx_text_desc
gravity = Gravity.CENTER
}.lparams(width = dip(127), height = dip(36)){
}
view().lparams(width = wrapContent, height = 0 , weight = 1.0f)
mTvCorpRight = textView{
text = ResourcesUtil.getString(R.string.corpright_format)
textColor = R.color.yx_text_desc
}.lparams(width = wrapContent, height = wrapContent){
bottomMargin = dip(20)
}
}
}
}
- verticalLayout就是orientation設(shè)置為Vertical的LinearLayout
- 布局總共分為兩部分,一部分關(guān)于控件自身的屬性,比如
textView
的text
屬性。一部分是關(guān)于控件的LayoutParam,寫在lparams參數(shù)中,例如margin
等等,括號內(nèi)定義控件的寬高值 - 整體寫法上與XML布局很相似,也是從上往下依次定義各控件
支持?jǐn)U展
Anko支持?jǐn)U展方法,例如我們可以做如下擴(kuò)展
fun Context.toast(message: CharSequence, duration: Int = Toast.LENGTH_SHORT) {
Toast.makeText(this, message, duration).show()
}
然后我們就可以在Anko中直接用該toast方法
verticalLayout {
val name = editText()
button("Say Hello") {
onClick { toast("Hello!") }
}
}
當(dāng)然,如果括號中任何方法也沒有的話可以省略括號。
verticalLayout {
button("Ok")
button(R.string.cancel)
}
支持Runtime Layouts
如果你有在特定邏輯下才會出現(xiàn)的布局,那么使用Anko來實(shí)現(xiàn)就很方便了,而如果用原有的方式,就必須在Java代碼里編寫布局,而相較于Anko來實(shí)現(xiàn)會顯得冗余而且難以維護(hù),尤其遇到復(fù)雜的布局實(shí)現(xiàn),純粹使用Java代碼去寫會非常頭疼。
例如要實(shí)現(xiàn)一個只有在橫屏情況下,且橫屏最小寬度要大于700px,才會展示一個特定的寬度的RecyclerView,寬度為屏幕寬度的50%。
用Anko DSL來實(shí)現(xiàn),只需10行代碼。
configuration(orientation = Orientation.LANDSCAPE, smallestWidth = 700) {
recyclerView {
init()
}.lparams(width = widthProcent(50), height = matchParent)
frameLayout().lparams(width = matchParent, height = matchParent)
}
fun <T : View> T.widthProcent(procent: Int): Int =
getAppUseableScreenSize().x.toFloat().times(procent.toFloat() / 100).toInt()
有興趣的童鞋可以用Java代碼來實(shí)現(xiàn)這個布局,并且與以上代碼進(jìn)行對比。
適配不同SDK版本更方便
如上述代碼那樣,用Anko來寫布局和XML沒有什么兩樣。但由于Android碎片化問題比較嚴(yán)重,不同版本的SDK占有率相差不大,為了針對不同SDK版本的手機(jī)有更優(yōu)的體驗,我們就需要對不同的SDK版本進(jìn)行最新API的適配。
用Anko來編寫布局使得我們可以進(jìn)行兼容性檢查,根據(jù)SDK的版本來使用哪種API,而不是在布局文件中來寫兩個XML文件。例如當(dāng)SDK版本大于5.0才會設(shè)置elevation屬性:
appBarLayout {
toolBar = toolbar {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) elevation = 4f
}.lparams(width = matchParent, height = actionBarSize())
}.lparams(width = matchParent)
Anko DSL Preview插件支持
那么我們?nèi)绾尾拍芟窬帉慩ML布局可以隨時查看編寫效果呢?Anko推出了Android Studio的擴(kuò)展插件,裝上之后也就和我們平時用XML編寫布局別無二致了。
性能
當(dāng)然上述的寫法上雖然Anko寫起來更加簡潔明了,但作為開發(fā)人員,我們更關(guān)注于效率和性能,那么他們之間到底有什么差別呢?
XML布局需要從資源文件中獲取,然后需要用XmlPullParser解析所有的元素并一個一個的創(chuàng)建它們。這個過程很繁重,而且XML有很多冗余的tag,加載這些冗余的信息也加大了開銷。我們來做個實(shí)驗,筆者挑了嚴(yán)選項目一個簡單的頁面進(jìn)行改造,發(fā)現(xiàn)即便這個比較簡單的View用Anko與XML的時間開銷的差別達(dá)到了好幾倍。
XML
Anko
以上是同一個界面用XML實(shí)現(xiàn)和Anko實(shí)現(xiàn)的截取的三個結(jié)果,為了實(shí)驗的準(zhǔn)確性,總共用了8款機(jī)型(Meizu MX2, VIVO X5M, HUAWEI Mate 8, HUAWEI Nexus 6P, XIAOMI 2S, Galaxy Note Edge, T1, MeiZu M1),分別進(jìn)行了30次測量,并對結(jié)果進(jìn)行整理統(tǒng)計:
XML | Anko | |
---|---|---|
Measure | 0.312ms | 0.136ms |
Layout | 0.28ms | 0.130ms |
Draw | 39.4ms | 27.5ms |
我們僅僅在這個簡單的頁面中就體現(xiàn)出近300%的速度差距,足以見得Anko在性能上的優(yōu)勢相較于XML更節(jié)省渲染時間。而且對于低端機(jī)型MX2,小米2S,差距尤為顯著,有近500%的速度差距。
原理
那么為什么XML和Anko可以效率差距那么明顯呢?我們首先來看一下Anko Layouts部分的源碼,了解它的工作原理。這里以verticalLayout為例:
我們找到CustomService.kt
這個類,發(fā)現(xiàn)有如下擴(kuò)展方法
inline fun Activity.verticalLayout(theme: Int = 0): LinearLayout = verticalLayout(theme) {}
inline fun Activity.verticalLayout(theme: Int = 0, init: (@AnkoViewDslMarker _LinearLayout).() -> Unit): LinearLayout {
return ankoView(`$$Anko$Factories$CustomViews`.VERTICAL_LAYOUT_FACTORY, theme, init)
}
正是由于這個擴(kuò)展方法,才允許我們在Activity中使用verticalLayout,VERTICAL_LAYOUT_FACTORY即是定義orientation為vertical的工廠類factory。
繼續(xù)看AnkoView這個擴(kuò)展方法
inline fun <T : View> Activity.ankoView(factory: (ctx: Context) -> T, theme: Int, init: T.() -> Unit): T {
val ctx = AnkoInternals.wrapContextIfNeeded(this, theme)
val view = factory(ctx)
view.init()
AnkoInternals.addView(this, view)
return view
}
這個方法很簡單,主要做了如下三件事情:
- 工廠類將子view提取出來
- 初始化提取出來的子view
- 將view添加至root view上,這里是LinearLayout
AnkoInternals
是Anko核心類,提供了許多核心方法,其中就有涉及布局的addView方法,稍后會介紹。首先看
wrapContextIfNeeded
這個方法
fun wrapContextIfNeeded(ctx: Context, theme: Int): Context {
return if (theme != 0 && (ctx !is AnkoContextThemeWrapper || ctx.theme != theme)) {
// 如果該context不是ContextThemeWrapper或它的子類且theme不為0,將對其進(jìn)行包裝,使其成為AnkoContextThemeWrapper繼承自ContextThemeWrapper。
AnkoContextThemeWrapper(ctx, theme)
} else {
ctx
}
}
接下來劃重點(diǎn)了,著重看一下AnkoInternals.addView(this, view)
:
fun <T : View> addView(manager: ViewManager, view: T) {
return when (manager) {
is ViewGroup -> manager.addView(view)
is AnkoContext<*> -> manager.addView(view, null)
else -> throw AnkoException("$manager is the wrong parent")
}
}
這里is
其實(shí)就是if (manager instanceof ViewGroup)
,所以這里是調(diào)用了LinearLayout的addView,從ViewGroup源碼可知,即將view添加到最后一個子View的后面。
將子View添加到ViewGroup之后又是怎么設(shè)置到activity的contentView的呢?我們繼續(xù)往下看,在Activity的addView擴(kuò)展方法中調(diào)用了createAnkoContext(activity, { AnkoInternals.addView(this, view) }, true)
,以下所示
inline fun <T> T.createAnkoContext(
ctx: Context,
init: AnkoContext<T>.() -> Unit,
setContentView: Boolean = false
): AnkoContext<T> {
val dsl = AnkoContextImpl(ctx, this, setContentView)
dsl.init()
return dsl
}
繼續(xù)看實(shí)現(xiàn)類AnkoContextImpl,
open class AnkoContextImpl<T>(
override val ctx: Context,
override val owner: T,
private val setContentView: Boolean
) : AnkoContext<T> {
private var myView: View? = null
override val view: View
get() = myView ?: throw IllegalStateException("View was not set previously")
override fun addView(view: View?, params: ViewGroup.LayoutParams?) {
if (view == null) return
if (myView != null) {
alreadyHasView()
}
this.myView = view
if (setContentView) {
doAddView(ctx, view)
}
}
private fun doAddView(context: Context, view: View) {
when (context) {
is Activity -> context.setContentView(view)
is ContextWrapper -> doAddView(context.baseContext, view)
else -> throw IllegalStateException("Context is not an Activity, can't set content view")
}
}
open protected fun alreadyHasView(): Unit = throw IllegalStateException("View is already set: $myView")
}
主要做了以下幾個事情:
- 判斷view是不是為空,為空則直接返回
- 判斷view是不是已經(jīng)設(shè)置過,如果已設(shè)置會拋出異常
- 判斷setContentView是否為true,為true,則會調(diào)用Activity的
setContentView(view)
方法。
所以到這里我們就把Anko DSL的工作流程基本上講完了,那么可以看到,Anko在解析時間上節(jié)省了XML解析的開銷,接下來我們來對比一下Android加載XML布局的方式。
我們知道,Android可以通過LayoutInflater.inflate方法來加載布局文件到內(nèi)存中,由于本文著重介紹的是Anko DSL,這里簡單列出關(guān)鍵的rInflate代碼
void rInflate(XmlPullParser parser, View parent, Context context,
AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
...
while (((type = parser.next()) != XmlPullParser.END_TAG ||
parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
if (type != XmlPullParser.START_TAG) {
continue;
}
final String name = parser.getName();
if (TAG_REQUEST_FOCUS.equals(name)) {
parseRequestFocus(parser, parent);
} else if (TAG_TAG.equals(name)) {
parseViewTag(parser, parent, attrs);
} else if (TAG_INCLUDE.equals(name)) {
if (parser.getDepth() == 0) {
throw new InflateException("<include /> cannot be the root element");
}
parseInclude(parser, context, parent, attrs);
} else if (TAG_MERGE.equals(name)) {
throw new InflateException("<merge /> must be the root element");
} else {
final View view = createViewFromTag(parent, name, context, attrs);
final ViewGroup viewGroup = (ViewGroup) parent;
final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
rInflateChildren(parser, view, attrs, true);
viewGroup.addView(view, params);
}
}
...
}
通過分析源碼,不難發(fā)現(xiàn),主要是使用XmlPullParser通過循環(huán)解析xml文件并將信息解析到內(nèi)存View對象,布局文件中定義的一個個組件都被順序的解析到了內(nèi)存中并被父子View的形式組織起來。
總結(jié)
結(jié)合上面的分析,我們不難總結(jié)出Anko Layouts相較于XML的優(yōu)勢主要在于:
- DSL減少了XML解析的時間及內(nèi)存開銷,加快了渲染效率。
- DSL更簡潔易讀,減少了XML冗余的tag信息。
- DSL擴(kuò)展性更強(qiáng),支持?jǐn)U展方法。
- DSL復(fù)用性更好,相比include方式更靈活。
- 在動態(tài)布局方面更有優(yōu)勢,避免了復(fù)雜的判斷邏輯。
當(dāng)然缺點(diǎn)也有如下幾點(diǎn):
- 有一定的學(xué)習(xí)成本
- Anko DSL Preview插件對于AS 2.2以上支持還有點(diǎn)問題。
當(dāng)然這些缺點(diǎn)都不算什么,既然有Google的支持,未來趨勢Kotlin所占的份額肯定是越來越多,Anko也在不斷完善中,以上文章如有寫錯的地方歡迎拍磚,文明交流。