目录
运行时app打开某个页面,必须做的事情有:
1.先把xml文件加载到内存
2.解析xml标签,读取布局
3.渲染绘制各层级View到屏幕
而使用代码直接动态绘制页面布局的话,就不需要这1、2两个耗时步骤。
实际测试对比,简单单层布局的页面就是20ms->2ms左右的巨大提升。如果是复杂或层级更深的页面,提升更大。
有利就有弊,不足的是:
1.代码动态布局,代码可读性较差
2.页面布局有变动,维护难度较大
3.代码量较大,同一类里编写,代码行数容易突破750行
4.界面无法预览,为了提升运行效率,而牺牲了开发效率
高阶函数是一种特殊的函数:它的参数或者返回值是另一个函数。比如平时开发用的传参回调,可以省去回调接口、实现的代码编写:
private fun exampleFun(emit: (bean: ImEventBean) -> Unit) { ... ... val result = GsonUtils.fromJson(json, ImEventBean::class.java) emit(result) }
exampleFun { EventBusUtils.post(it) }
或者如:常用的let函数
public inline funT.let(block: (T) -> R): R { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } return block(this) }
简单理解,就是编译时,把函数体复制粘贴到函数调用处,减少一次方法栈的开辟(局部变量表、操作栈等附带开支)
主要运用场景,用于配合高阶函数/Lambda的使用。其中与Lambda的配合,主要是为了使其允许局部返回,如:
return@let
Kotlin 中的 Lambda函数,可以访问接收者的非私有成员。如:
info.apply{ updateName(info.nickName)//获取非私有成员 info.age = mInput.text.toString()//设置非私有成员 }
DSL(domain specific language),全称“特定领域语言”,DSL专注特定领域的操作/表达,具有结构性。例如HTML、SQL也是一种具有结构性DSL语言
基于Kotlin 自带接收者的lambda(&高阶函数)的特性,结合DSL可读化的结构性质,可以对动态布局代码进行可读性的优化,实践举例如下:
ConstraintLayout { layout_width = match_parent layout_height = match_parent ImageView { layout_id = "ivBack" layout_width = 28 layout_height = 28 margin_start = 16 margin_top = 18 src = R.drawable.ic_back start_toStartOf = parent_id top_toTopOf = parent_id onClick = { onBackClick() } } TextView { layout_width = wrap_content layout_height = wrap_content text = "commit" textSize = 16f textStyle = bold align_vertical_to = "ivBack" center_horizontal = true } }
此处仅举例,具体以demo&lib_dsl为准,DemoGit
inline fun ViewGroup.TextView( style: Int? = null, autoAdd: Boolean = true, init: AppCompatTextView.() -> Unit ): TextView { val textView = if (style != null) AppCompatTextView(ContextThemeWrapper(context, style)) else AppCompatTextView(context) return textView.apply(init).also { if (autoAdd) addView(it) } }
然后就可以这么用
inline var View.layout_width: Number get() { return 0 } set(value) { val w = if (value.dp > 0) value.dp else value.toInt() val h = layoutParams?.height ?: 0 updateLayoutParams{ width = w height = h } }
val wrap_content = ViewGroup.LayoutParams.WRAP_CONTENT val match_parent = ViewGroup.LayoutParams.MATCH_PARENT
然后就可以在kt代码中这么用
因为DSL的结构化构建布局,我们可以直接声明成员变量,构建view时直接赋值,也可以后续需要时,再通过父view.findViewById()获取
lateinit var btnMenu: View ... mRootView = context.ConstraintLayout { ... // 直接赋值 btnMenu = TextView { //声明id 后续再findViewById() layout_id = "tvMenu" layout_width = wrap_content layout_height = wrap_content textSize = 16F text = "菜单" } ... }
将动态布局代码独立为一个类,并在其中使用DSL编写布局。然后再在需要的地方进行初始化、使用
/** 布局dsl 等价于 viewBinding文件 */ private val contentViewDsl by lazy { SecondActivityDslLayout().inflate(this) }
val beforeMills = System.currentTimeMillis() // 懒加载,setView setContentView(contentViewDsl.getRoot()) val totalMills = System.currentTimeMillis() - beforeMills Log.d(this.javaClass.simpleName, "================= 页面构建耗时:${totalMills}ms") // 类似ViewBinding使用控件 contentViewDsl.tvTitle.text = "耗时:${totalMills}ms"
最终运行效果与xml一致,可以运行代码查看。
与xml相比首次渲染布局dsl耗时更久。
我们通过Android Studio工具,查看DSL布局.kt文件 编译后的情况,发现仅Demo简单的页面,编译后的Java代码已经是几千行。其中一部分原因是扩展函数使用了inline关键字,另一部分原因是kotlin自身的特性。
以上Demo是不涉及 点击或者变化监听 的纯UI布局代码,如果是正常业务的页面,那么生成的Java类文件将会是非常庞大的,后期依然是要被优化的。
1.使用DSL代码动态布局,解决了普通代码可读性差的问题,可读性与xml 基本持平
2.页面布局有变动,维护难度降低,利于扩展
3.编写代码量较小,界面绘制效率大大优于xml
4.界面无法预览是硬伤,为了提升运行效率,而牺牲了开发效率,建议后期不需兼顾低版本用户时,切换到Jetpack Compose
5.前期DSL扩展库编写需要耗费较多时间,实际使用的话推广难度较大
6.DSL布局.kt 代码转化为Java代码,代码量随着页面复杂度增加而增加
总而言之,等允许放弃5.0以下的用户的时候,直接上Jetpack Compose吧。