引言
前段時間寫了一篇Kotlin語法入門的文章,還沒有看過的盆友請戳(這裏),有的可能看完之後已經開始嘗試用kotlin來寫代碼了。不過上篇體現的僅僅是針對於Kotlin相較於Java在用法上的擴展性以及寫法上的簡潔性,那麼Android中還有另一個重要的組成部分,佈局文件呢?接下來我們就繼續看一下Anko(基於Kotlin的擴展庫)對於Android傳統佈局文件XML做的改進及優化,以及工作原理。
定義
Anko是Kotlin爲Android推出的第三方庫,旨在提升Android界面的開發效率,使代碼更簡潔易懂並更容易閱讀。Anko總共分爲以下四個部分:
- Anko Commons: 輕量級類庫包括intent,dialog,logging等幫助類
- Anko Layouts:快速的空安全的方式來寫動態的佈局
- Anko SQLite:關於Android SQLite查詢語句DSL和容器解析器
- Anko Coroutines:Coroutines提供了一種長時間阻塞線程的解決方案,並且代之以開銷更小和更可控的操作(suspension of a coroutine)
我們可以看到,Anko不僅僅可以用來寫佈局,更加可以做一些基礎支持工具,比如操作數據庫,用Intent進行數據傳遞等等,本文着重探討的是Anko Layouts這一部分。
優勢
- Anko可以讓我們在源碼中寫UI佈局,嚴格的編譯檢查可以保證類型安全,不會出現類型轉換異常
- 沒有多餘的CPU開銷來解析XML文件
- 我們可以把Anko DSL約束放在函數中,提高代碼複用率,比原有xml的include更強大
用法
如下,是應用中的關於我們界面佈局文件:
由於佈局非常簡單,就不多解釋了,那麼如果將上述佈局用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設置爲Vertical的LinearLayout
- 佈局總共分爲兩部分,一部分關於控件自身的屬性,比如
textView
的text
屬性。一部分是關於控件的LayoutParam,寫在lparams參數中,例如margin
等等,括號內定義控件的寬高值 - 整體寫法上與XML佈局很相似,也是從上往下依次定義各控件
支持擴展
Anko支持擴展方法,例如我們可以做如下擴展
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!") }
}
}
當然,如果括號中任何方法也沒有的話可以省略括號。
verticalLayout {
button("Ok")
button(R.string.cancel)
}
支持Runtime Layouts
如果你有在特定邏輯下才會出現的佈局,那麼使用Anko來實現就很方便了,而如果用原有的方式,就必須在Java代碼裏編寫佈局,而相較於Anko來實現會顯得冗餘而且難以維護,尤其遇到複雜的佈局實現,純粹使用Java代碼去寫會非常頭疼。
例如要實現一個只有在橫屏情況下,且橫屏最小寬度要大於700px,纔會展示一個特定的寬度的RecyclerView,寬度爲屏幕寬度的50%。
用Anko DSL來實現,只需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代碼來實現這個佈局,並且與以上代碼進行對比。
適配不同SDK版本更方便
如上述代碼那樣,用Anko來寫佈局和XML沒有什麼兩樣。但由於Android碎片化問題比較嚴重,不同版本的SDK佔有率相差不大,爲了針對不同SDK版本的手機有更優的體驗,我們就需要對不同的SDK版本進行最新API的適配。
用Anko來編寫佈局使得我們可以進行兼容性檢查,根據SDK的版本來使用哪種API,而不是在佈局文件中來寫兩個XML文件。例如當SDK版本大於5.0纔會設置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插件支持
那麼我們如何才能像編寫XML佈局可以隨時查看編寫效果呢?Anko推出了Android Studio的擴展插件,裝上之後也就和我們平時用XML編寫佈局別無二致了。
性能
當然上述的寫法上雖然Anko寫起來更加簡潔明瞭,但作爲開發人員,我們更關注於效率和性能,那麼他們之間到底有什麼差別呢?
XML佈局需要從資源文件中獲取,然後需要用XmlPullParser解析所有的元素並一個一個的創建它們。這個過程很繁重,而且XML有很多冗餘的tag,加載這些冗餘的信息也加大了開銷。我們來做個實驗,筆者挑了嚴選項目一個簡單的頁面進行改造,發現即便這個比較簡單的View用Anko與XML的時間開銷的差別達到了好幾倍。
XML
Anko
以上是同一個界面用XML實現和Anko實現的截取的三個結果,爲了實驗的準確性,總共用了8款機型(Meizu MX2, VIVO X5M, HUAWEI Mate 8, HUAWEI Nexus 6P, XIAOMI 2S, Galaxy Note Edge, T1, MeiZu M1),分別進行了30次測量,並對結果進行整理統計:
XML | Anko |
---|---|
Measure | 0.312ms |
Layout | 0.28ms |
Draw | 39.4ms |
我們僅僅在這個簡單的頁面中就體現出近300%的速度差距,足以見得Anko在性能上的優勢相較於XML更節省渲染時間。而且對於低端機型MX2,小米2S,差距尤爲顯著,有近500%的速度差距。
原理
那麼爲什麼XML和Anko可以效率差距那麼明顯呢?我們首先來看一下Anko Layouts部分的源碼,瞭解它的工作原理。這裏以verticalLayout爲例:
我們找到CustomService.kt
這個類,發現有如下擴展方法
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)
}
正是由於這個擴展方法,才允許我們在Activity中使用verticalLayout,VERTICAL_LAYOUT_FACTORY即是定義orientation爲vertical的工廠類factory。
繼續看AnkoView這個擴展方法
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,將對其進行包裝,使其成爲AnkoContextThemeWrapper繼承自ContextThemeWrapper。
AnkoContextThemeWrapper(ctx, theme)
} else {
ctx
}
}
接下來劃重點了,着重看一下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
其實就是if (manager instanceof ViewGroup)
,所以這裏是調用了LinearLayout的addView,從ViewGroup源碼可知,即將view添加到最後一個子View的後面。
將子View添加到ViewGroup之後又是怎麼設置到activity的contentView的呢?我們繼續往下看,在Activity的addView擴展方法中調用了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
}
繼續看實現類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是不是已經設置過,如果已設置會拋出異常
- 判斷setContentView是否爲true,爲true,則會調用Activity的
setContentView(view)
方法。
所以到這裏我們就把Anko DSL的工作流程基本上講完了,那麼可以看到,Anko在解析時間上節省了XML解析的開銷,接下來我們來對比一下Android加載XML佈局的方式。
我們知道,Android可以通過LayoutInflater.inflate方法來加載佈局文件到內存中,由於本文着重介紹的是Anko DSL,這裏簡單列出關鍵的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);
}
}
...
}
通過分析源碼,不難發現,主要是使用XmlPullParser通過循環解析xml文件並將信息解析到內存View對象,佈局文件中定義的一個個組件都被順序的解析到了內存中並被父子View的形式組織起來。
總結
結合上面的分析,我們不難總結出Anko Layouts相較於XML的優勢主要在於:
- DSL減少了XML解析的時間及內存開銷,加快了渲染效率。
- DSL更簡潔易讀,減少了XML冗餘的tag信息。
- DSL擴展性更強,支持擴展方法。
- DSL複用性更好,相比include方式更靈活。
- 在動態佈局方面更有優勢,避免了複雜的判斷邏輯。
當然缺點也有如下幾點:
- 有一定的學習成本
- Anko DSL Preview插件對於AS 2.2以上支持還有點問題。
當然這些缺點都不算什麼,既然有Google的支持,未來趨勢Kotlin所佔的份額肯定是越來越多,Anko也在不斷完善中,以上文章如有寫錯的地方歡迎拍磚,文明交流。