1. Android架构发展历程
1.1 MVC架构
-
java文件夹下放置工程代码
-
res文件夹下放置资源文件(页面、图片等)
-
View:视图层,用于数据展示
-
Controller:控制层,用于业务逻辑
-
Model:数据层,用于处理远端传递的数据(接受数据、发送数据)
1.2 MVP架构
-
View:负责数据展示
-
Presenter:负责业务逻辑【通过回调通知视图数据变化】
-
Model:负责存储和处理远端传递的数据(接受数据、发送数据)
整洁的代码如同优美的散文。整洁的代码从不隐藏设计者的意图,充满了干净利落的抽象和直截了当的控制语句。—— Grady Booch
val k01: (String) -> (String) -> (Boolean) -> (Int) -> (String) -> Int =
{ it: String ->
{ it: String ->
{ it: Boolean ->
{ it: Int ->
{ it: String ->
99
}
}
}
}
}
1.3 MVVM架构
-
View:负责界面展示
-
ViewModel:负责业务逻辑【通过观察者模式通知数据变化】
-
Model:负责数据存储和处理相关逻辑
MVVM与MVP一样将逻辑层和视图层分离,为避免出现大量回调函数,使用双向绑定(只要你改动,我就敢变)的方式将View层和ViewModel层绑定,即通过让View层和ViewModel层的可观察对象(比如LiveData)进行绑定,实现数据显示的动态变化,是一种数据驱动页面思想的实现。相较于MVP,MVVM更为轻量化。因为MVVM不仅将View和ViewModel分离,还保证了ViewModel中的代码简洁不臃肿。但是在界面UI复杂的情况下,MVVM可能出现每个UI需要不同的数据,导致数据分散不容易维护。
1.3.1 双向绑定实现一:DataBinding
数据绑定库是一种支持库,借助该库,您可以使用声明性格式(而非程序化地)将布局中的界面组件绑定到应用中的数据源。
-
模板代码过多,还需要加注解
-
在xml布局文件中掺杂Java/Kotlin代码,出问题不容易Debug,对于开发人员非常不友好
-
需要在xml布局文件中绑定多个数据字段
-
能不用可观察变量尽量不要用
-
多个变量会同时改变的情况尽量使用一个可观察变量进行包装
-
data标签能少导入一个变量尽量少导入
-
XML布局尽量少或者不使用过多的逻辑判断
-
避免对一个数据进行多次绑定
-
严格遵守上述五条
1.3.2 双向绑定实现二:LiveData + ViewModel【Goolge标准框架流程】
在MVVM早期,开发者使用Google官方的DataBinding框架实现数据层和视图层双向绑定。但是DataBinding框架较为复杂,一处代码修改就可能导致多处联动代码的修改。为减少模板代码,随后Google推出Jetpack组件(Lifecycle, LiveData, ViewModel, Room, Paging, Navigation)并推荐使用它集中的LiveData + ViewModel,再配合Kotlin的协程和Flow,可实现更方便的流式代码编写。
-
防止内存泄露
-
使用LiveData的观察者方法时,会将传入的Activity对象向上抽象为LifecyclerOwner,它会和我们传递进去的回调函数组合成为LifecycleBoundObserver对象。
-
上述组合出来的对象最终实现Lifecycle组件中的LifecycleObserver接口,就可以 观测Activity或Fragment的 生命周期,进而在特定的时机将LiveData解除订阅并释放所持有的内存资源,防止出现内存泄露、内存溢出, 避免应用崩溃。
-
为LiveData配备一个Map来存储所有的观察者,用于处理多个观察者同时观察订阅。
-
将LifecycleBoundObserver对象传入Activity或Fragment,在不同的生命周期中通知LiveData。
-
-
数据更新回调默认只在前台触发在通知回调时,会判断LiveData的持有者(Activity或Fragment)的生命周期,默认只有在onStart()执行后和onPause()被执行前,回调函数才会被触发。换句话说, 只有页面可见的时候才会触发回调,动态改变显示的数据。当然,也只有页面可见时的数据变化有意义。
观察者模式
观察者模式:指多个对象间存在一对多的依赖关系,当目标对象(被观察者)的状态发生改变时,所有依赖于它的对象(观察者)都得到通知并被自动更新。这种模式有时又称作发布-订阅模式、模型-视图模式,它是对象行为型模式。
-
对于 被观察者来说,它只知道一个拥有共同接口的观察者对象列表,而不清楚每个具体的观察者是什么
-
对于 单个观察者来说,它不关心数据从哪里来,只关心传递过来的数据如何进行处理。更专业的说法是, 事件的发射上游和接收事件的下游互不干涉,大幅 降低相互持有依赖关系所带的强 耦合性
-
对于 多个观察者来说,它们之间不存在依赖关系,可根据系统需要对观察者进行增删操作, 提高系统的 扩展性, 符合开闭原则
-
当观察者过多时,目标对象完成所有观察者的通知的时间将会大幅增加
-
若被观察者和观察者之间存在循环依赖,则可能导致系统崩溃
-
观察者只能知道被观察者变化了,而不清楚被观察者发生变化的原因
1.4 MVI架构
-
View:负责界面的展示
-
Intent:负责封装与发送用户的操作
-
Model:负责存储视图的数据和状态
1.5 Compose单向数据流
-
State:状态,即界面中可跨越Composable函数的生命周期的数据
-
Composable:可组合函数,即界面中需要被显示的各个元素,不可跨越跨越Composable函数的生命周期
1.6 小结
2. 什么是Compose
2.1 定义
Jetpack Compose 是用于构建原生 Android 界面的新工具包。它使用更少的代码、强大的工具和直观的 Kotlin API,可以帮助您简化并加快 Android 界面开发。—— developers
2.2 特点
-
一切皆函数:每个微件都可对应为函数,函数会被编译成为View,通过组合这些函数就可实现一个页面。
-
直观:不需要手动刷新数据,只需描述页面。 当应用状态变化时,界面会自动更新。
-
更少的代码:实现相同的功能只需 原来一半的代码量,构建变得 简洁、易维护。
-
加速开发:提供大量开箱即用的Material 组件,可以使得开发者可以将注意力 聚焦于业务逻辑上,而不是在动画、主题变化等事情上。
-
功能强大:Compose与大部分代码都兼容, 最低兼容到API21,并且View和Compose可以相互调用。
-
结构扁平: 不会出现 重复测量。
2.3微件
2.4 编程思想
@Composable
fun Greeting(names: List<String>) {
names.forEach { name ->
Text(text = "Hello $name")
}
}
2.4.1 声明性范式编程
2.4.2 简单的组合函数
2.4.3 声明性范式转变
2.4.4 动态内容
2.5 重组
对于以下可组合函数,每当点击该按钮时,State会变化,触发重组。大范围的重组势必会影响性能,但是Compose编译器做了大量工作以保证重组的范围尽可能的小,避免无效开销。
@Composable fun Example() {
var text by remember { mutableStateOf("") }
Log.d(TAG, "Example")
Button(
onClick = {
text = "$text $text"
}.also {
Log.d(TAG, "Button")
}){
Log.d(TAG, "Button content lambda")
Text(text).also { Log.d(TAG, "Text")
}
}
}
// 运行结果:
// Button content lambda
// Text
2.5.1 Compose如何确定重组范围
-
非inline并且无返回值的Composable 函数/Lambda
-
inline关键字标记的函数会在代码的编译期在调用处展开(类似C语言和汇编的宏),导致无法在下次重组时找到合适的调用入口,即无法在栈中找到inline函数的入口
-
对于有返回值的函数,返回值会影响到调用方,无法进行单独重组,必须连同调用方一同进行重组
-
-
遵循重组最小化原则
2.5.2 重组是并发进行的
“并行化”重组正在开发中,当前的Composable重组仍然发生在主线程中,但是在未来的某一时刻,重组随时会变成并行执行,这要求我们现在就要以这种观点去开发。——《Jetpack Compose 从入门到实战》
2.5.3 重组是乐观的操作
确保所有可组合函数和 lambda 都幂等且没有 附带效应,以处理乐观的重组。—— developers
2.5.4 Composable函数会频繁的执行
2.6 生命周期与副作用
2.6.1 生命周期
-
onActive:添加到视图树,即Composable首次被执行
-
onUpate:重组,即Composable跟随重组不断执行,更新视图树上相对应的节点
-
onDispose:从视图树上移除,即Composable不再被执行
2.6.2 副作用
2.7 架构
Jetpack Compose 不是一个单体式项目;它由一些模块构建而成,这些模块组合在一起,构成了一个完整的堆栈。—— developers
-
灵活控制
-
自定义简单
3. 为什么用Compose
-
Compose 是一个声明性界面框架,使用更少的代码、强大的工具和直观的 Kotlin API
-
抛弃了原有安卓view的体系,完全重新实现了一套新的UI体系
-
使用可组合函数来替换view构建UI界面,只允许一次测量,避免了布局嵌套多次测量问题,从根本上解决了布局层级对布局性能的影响
3.1 Compose和View的对比
3.1.1 直观的可视化对比
-
Activity:负责管理安卓应用的用户界面
-
PhoneWindow:是Window类的具体实现,可以通过该类去绘制窗口
-
DecorView:最顶层的View,即是所有应用窗口的根 View
-
TitleView:标题导航栏
-
ContentView:Activity对应的XML布局,通过setContentView设置到DecorView中
3.1.2 继承与组合
开发者需要花费大量的精力去确保各组件之间状态的一致性,这也是造成命令式 UI 代码复杂度高的根本原因。——《Jetpack Compose 从入门到实战》
3.1.3 命令式UI和声明式UI
命令式用命令的方式告诉计算机如何去做事情( how to do),计算机通过执行命令达到结果,而声明式直接告诉计算机用户想要的结果( what to do),计算机自己去想自己该去怎么做。——《Jetpack Compose 从入门到实战》
3.1.4 Moidier和XML
-
Modifier伴生对象:Modifier链最开头的Modifier
-
Modifier.Element:代表具体的修饰符
-
CombinedModifier:连接每个Modifier,连接的两个Modifier分别存储在outer和inner中
Modifier.size(100.dp) // 设置大小
.background(Color.Red) // 设置背景颜色
.padding(10.dp) // 设置边距
3.1.5 视图树与渲染流程
3.1.5.1 对于Android View
-
测量:为测量宽高过程,如果是ViewGroup还要在onMeasure中对所有子View进行measure操作
-
布局:用于摆放View在ViewGroup中的位置,如果是ViewGroup要在onLayout方法中对所有子View进行layout操作
-
绘制:往View上绘制图像
3.1.5.2 对于Compose
-
组合:执行Composable函数体,生成LayoutNode视图树
-
布局:对视图树中每个LayoutNode完成测量并指定摆放位置
-
绘制:将所有LayoutNode实际绘制到屏幕上
3.1.5.3 Compose只允许一次测量
private fun LayoutNode.trackMeasurementByParent() {
val parent = parent
if (parent != null) {
check(
measuredByParent == LayoutNode.UsageByParent.NotUsed ||
@Suppress("DEPRECATION") canMultiMeasure
) {
"measure() may not be called multiple times on the same Measurable. Current " +
"state $measuredByParent. Parent state ${parent.layoutState}."
}
...
} else {
...
}
}
private fun LayoutNode.trackLookaheadMeasurementByParent() {
// when we measure the root it is like the virtual parent is currently laying out
val parent = parent
if (parent != null) {
check(
measuredByParentInLookahead == LayoutNode.UsageByParent.NotUsed ||
@Suppress("DEPRECATION") canMultiMeasure
) {
"measure() may not be called multiple times on the same Measurable. Current " +
"state $measuredByParentInLookahead. Parent state ${parent.layoutState}."
}
...
} else {
...
}
}
3.1.6 布局加载流程
3.1.6.1 setContentView
3.1.6.2 setContent
public fun ComponentActivity.setContent(
parent: CompositionContext? = null,
content: @Composable () -> Unit
) {
// decorView的第一个子view如果是ComposeView就直接用,否则就创建一个ComposeView加载到根布局
val existingComposeView = window.decorView
.findViewById<ViewGroup>(android.R.id.content)
.getChildAt(0) as? ComposeView
if (existingComposeView != null) with(existingComposeView) {
setParentCompositionContext(parent)
setContent(content)
} else ComposeView(this).apply {
// Set content and parent **before** setContentView
// to have ComposeView create the composition on attach
setParentCompositionContext(parent)
setContent(content)
// Set the view tree owners before setting the content view so that the inflation process
// and attach listeners will see them already present
setOwners()
setContentView(this, DefaultActivityContentLayoutParams)
}
}
3.1.7 Compose与View的兼容
Compose和AndroidView相互调用
在AndroidView中使用Compose
<androidx.compose.ui.platform.ComposeView
android:id="@+id/acv"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
findViewById<ComposeView>(R.id.acv)
.setContent {
Text(text = "ComposeView")
}
在Compose中使用AndroidView
@Composable
fun AndroidViewExample() {
Column {
Text(text = "top")
AndroidView(
factory = { context ->
TextView(context).apply{
setBackgroundColor(android.graphics.Color.GRAY)
}
},
modifier = Modifier.warpContentSize(),
update = { textView ->
textView.apply {
text = "123"
textSize = 16f
setTextColor(android.graphics.Color.BALCK)
}
}
)
}
}
延申一点
@Composable
@UiComposable
fun <T : View> AndroidView(
factory: (Context) -> T,
modifier: Modifier = Modifier,
update: (T) -> Unit = NoOpUpdate
) {
...
ComposeNode<LayoutNode, UiApplier>(
// 使用
factory = createAndroidViewNodeFactory(factory, dispatcher),
update = {
updateViewHolderParams<T>(
modifier = materializedModifier,
density = density,
lifecycleOwner = lifecycleOwner,
savedStateRegistryOwner = savedStateRegistryOwner,
layoutDirection = layoutDirection
)
set(update) { requireViewFactoryHolder<T>().updateBlock = it }
}
)
}
@Suppress("NONREADONLY_CALL_IN_READONLY_COMPOSABLE", "UnnecessaryLambdaCreation")
@Composable inline fun <T : Any, reified E : Applier<*>> ComposeNode(
noinline factory: () -> T,
update: @DisallowComposableCalls Updater<T>.() -> Unit
) {
if (currentComposer.applier !is E) invalidApplier()
currentComposer.startNode()
if (currentComposer.inserting) { // 插入新的LayoutNode
currentComposer.createNode { factory() }
} else { // 复用原来的LayoutNode
currentComposer.useNode()
}
Updater<T>(currentComposer).update()
currentComposer.endNode()
}
-
生成上下文Context并交给View
-
返回ViewFactoryHolder实例转换后的LayoutNode(ViewFactoryHolder类最终继承的就是ViewGroup)
@Composable
private fun <T : View> createAndroidViewNodeFactory(
factory: (Context) -> T,
dispatcher: NestedScrollDispatcher
): () -> LayoutNode {
val context = LocalContext.current
...
return {
ViewFactoryHolder<T>(
context = context,
factory = factory,
parentContext = parentReference,
dispatcher = dispatcher,
saveStateRegistry = stateRegistry,
saveStateKey = stateKey
).layoutNode
}
}
Compose的继承体系
3.2 小结(总体对比)
Android View
|
Jetpack Compose
|
类职责不单一,继承关系不合理
|
函数式编程思想,规避了面向对象的各种弊病
|
依赖系统版本,问题修复不及时
|
独立迭代,良好的系统兼容性
|
命令式编程,开发效率低下
|
声明式编程,DSL的开发效率更高
|
多次测量,影响性能
|
单次测量,提高性能
|