From View To Jetpack Compose

原生 View

在 Android APP 中,所有的用户界面元素都是由 View 和 ViewGroup 的对象构成的。View 是绘制在屏幕上的用户能与之交互的一个对象。而 ViewGroup 则是一个用于存放其他 View(和 ViewGroup)对象的布局容器。 Android 为我们提供了一个 View 和 ViewGroup 子类的集合,集合中提供了一些常用的输入控件 (比如按钮和文本域) 和各种各样的布局模式。

原生 View 显示到我们的手机上面主要通过 Measure、layout、Draw 过程。

Window、Activity、DecorView、ViewRoot

Activity

Activity 并不负责视图控制,它只是控制生命周期和处理事件。真正控制视图的是 Window。一个 Activity 包含了一个 Window,Window 才是真正代表一个窗口。Activity 就像一个控制器,统筹视图的添加与显示,以及通过其他回调方法,来与 Window、以及 View 进行交互。

Window

Window 是视图的承载器,内部持有一个 DecorView,而这个 DecorView 才是 view 的根布局。Window 是一个抽象类,实际在 Activity 中持有的是其子类 PhoneWindow。PhoneWindow 中有个内部类 DecorView,通过创建 DecorView 来加载 Activity 中设置的布局 R.layout.activity_main。Window 通过 WindowManager 将 DecorView 加载其中,并将 DecorView 交给 ViewRoot,进行视图绘制以及其他交互。

DecorView

DecorView 是 FrameLayout 的子类,它可以被认为是 Android 视图树的根节点视图。DecorView 作为顶级 View,一般情况下它内部包含一个竖直方向的 LinearLayout,**在这个 LinearLayout 里面有上下三个部分,上面是个 ViewStub,延迟加载的视图(应该是设置 ActionBar,根据 Theme 设置),中间的是标题栏 (根据 Theme 设置,有的布局没有),下面的是内容栏 **

ViewRoot

ViewRoot 主要处理 View 的绘制以及事件分发等交互。

ViewRoot 对应 **ViewRootImpl 类,它是连接 WindowManagerService 和 DecorView 的纽带 **,View 的三大流程(测量(measure),布局(layout),绘制(draw))均通过 ViewRoot 来完成。

img

setContentView

在 View 系统中,我们所编写的 xml 文件通过 setContentView 方法装载到视图。

1
2
3
4
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}

可以看到我们的 xml 文件是给到了 Window 去装载视图,即通过 Activity 持有的 PhoneWindow 中的 DecorView 装载

1
2
3
4
5
6
7
8
9
10
11
12
public void setContentView(int layoutResID) {
if (mContentParent == null) {
installDecor();
} else {
mContentParent.removeAllViews();
}
mLayoutInflater.inflate(layoutResID, mContentParent);
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
}

这个 Window 通过 installDecor 创建 DecorView,并且把我们的 xml 文件装载到 mContentParent,这个 mContentParent 就是 DecorView 中的 Content 那部分的 FrameLayout

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private void installDecor() {
if (mDecor == null) {

// 创建 DecorView
mDecor = generateDecor();
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
}
if (mContentParent == null) {
// 为 DecorView 设置布局格式 并且返回 mContentParent
mContentParent = generateLayout(mDecor);
...
}
}
}

以上还只是加载了布局文件,DecorView 真正显示还要到 ActivityonResume 过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
final void handleResumeActivity(IBinder token, boolean clearHide,
boolean isForward, boolean reallyResume) {

ActivityClientRecord r = performResumeActivity(token, clearHide);

if (r != null) {
final Activity a = r.activity;
if (r.window == null && !a.mFinished && willBeVisible) {
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
//decor 不可见
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;

l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;

if (a.mVisibleFromClient) {
a.mWindowAdded = true;
wm.addView(decor, l);
}

if (!r.activity.mFinished && willBeVisible
&& r.activity.mDecor != null && !r.hideForNow) {
// 真正可见
if (r.activity.mVisibleFromClient) {
r.activity.makeVisible();
}
}
}
}
}

可以看到这里在调用 makeVisible 方法之后才真正可见,在 makeVisible 方法中兜兜转转到最后 WindowManager 中的 ViewRoot 中去 measurelayoutdraw

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
...
// Schedule the first layout -before- adding to the window
// manager, to make sure we do the relayout before receiving
// any other events from the system.
requestLayout();
...
try {
...
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(),
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mInputChannel);
}
}

ViewRootImpl 调用到 requestLayout 来完成 View 的绘制操作

1
2
3
4
5
6
7
8
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}

View 绘制,先判断当前线程

1
2
3
4
5
6
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}

最后兜兜转转到了 performTraversals 方法

1
2
3
4
5
6
7
8
9
10
private void performTraversals() {
......
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
performLayout(lp, desiredWindowWidth, desiredWindowHeight);
......
performDraw();
}
......
}

img

Measure

Android View 的测量过程中使用到了 MeasureSpec(测量规格)。

img

其中,Mode 模式共分为三类:

UNSPECIFIED :不对 View 进行任何限制,要多大给多大,一般用于系统内部

EXACTLY:对应 LayoutParams 中的 match_parent 和具体数值这两种模式。检测到 View 所需要的精确大小,这时候 View 的最终大小就是 SpecSize 所指定的值,

AT_MOST :对应 LayoutParams 中的 wrap_content。View 的大小不能大于父容器的大小。

MeasureSpec

对于 View 的 MeasureSpec,其确定是通过父布局的 MeasureSpec 和自身的布局参数 LayoutParams

img

以上看来,View 的 MeasureSpec 要通过父容器 ViewGroup 的 getChildMeasureSpec 方法获取。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60

public static int getChildMeasureSpec(int spec, int padding, int childDimension) {

int specMode = MeasureSpec.getMode(spec);

int specSize = MeasureSpec.getSize(spec);

int size = Math.max(0, specSize - padding);

int resultSize = 0;
int resultMode = 0;

switch (specMode) {
case MeasureSpec.EXACTLY:
if (childDimension>= 0) {

resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;

} else if (childDimension == LayoutParams.MATCH_PARENT) {

resultSize = size;
resultMode = MeasureSpec.EXACTLY;

} else if (childDimension == LayoutParams.WRAP_CONTENT) {

resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;


case MeasureSpec.AT_MOST:
if (childDimension>= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;l
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;

case MeasureSpec.UNSPECIFIED:
if (childDimension>= 0) {
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}

首先我们来看 ViewGroup 的 measure 过程,对于 ViewGroup 没有 onMeasure 方法,只有 measureChildren 方法

1
2
3
4
5
6
7
8
9
10
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
protected void measureChild(View child, int parentWidthMeasureSpec,
int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();

final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);

child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}

对于 View 的 measure 过程主要在于 onMeasure 方法,通过 setMeasuredDimension 方法设置测量值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
}

public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}

以上 getDefaultSize 方法可以看到当 mode 为 AT_MOST 或者 EXACTLY 时,其实都是 specSize,但是结合上面的表我们可以知道,这时的 specSize 其实都是 parentSize,所以当我们自定义 View 的时候我们要重写 onMeasure 方法。

那么 ViewGroup 如何 Measure 自身呢?对于不同的 ViewGroup,Measure 方式都有所不同,具体我们等会自定义 View 的时候可以细看。

Layout

测量完 View 大小之后,需要将 View 布局在 Window 中,View 的布局主要通过上下左右四个点来确定。

其中布局也是自上而下,不同的是 ViewGroup 先在 layout() 中确定自己的布局,然后在 onLayout() 方法中再调用子 View 的 layout() 方法,让子 View 布局。在 Measure 过程中,ViewGroup 一般是先测量子 View 的大小,然后再确定自身的大小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void layout(int l, int t, int r, int b) {

int oldL = mLeft;
int oldT = mTop;
int oldB = mBottom;
int oldR = mRight;

// setOpticalFrame setFrame 确定自身布局
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);

if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {

// onLayout 确定子 View 布局
onLayout(changed, l, t, r, b);
...

}

onLayout 是一个可重写的方法,如果当前 View 就是一个单一的 View,那么没有子 View,就不需要实现该方法。

如果当前 View 是一个 ViewGroup,就需要实现 onLayout 方法,在自定义 ViewGroup 的时候就需要自行实现 onLayout 方法布局子 View

Draw

View 的绘制过程遵循如下几步:

  1. 绘制背景 background.draw(canvas)

  2. 绘制自己(onDraw)

  3. 绘制 Children(dispatchDraw)

  4. 绘制装饰(onDrawScrollBars)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public void draw(Canvas canvas) {
...
int saveCount;
if (!dirtyOpaque) {
drawBackground(canvas);
}

final int viewFlags = mViewFlags;
if (!verticalEdges && !horizontalEdges) {

if (!dirtyOpaque)

// 自定义 View 需要真正重写的方法
onDraw(canvas);


dispatchDraw(canvas);

........

onDrawScrollBars(canvas);

..........
return;
}
...
}

无论是 ViewGroup 还是单一的 View,都需要实现这套流程,不同的是,在 ViewGroup 中,实现了 dispatchDraw() 方法,而在单一子 View 中不需要实现该方法。自定义 View 一般要重写 onDraw() 方法,在其中绘制不同的样式。

自定义 View

有了前面的知识,我们可以着手实现一个自己 ViewGroup 和 View。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
public class MyViewGroup extends ViewGroup {
// horizontal LinearLayout

@SuppressLint("DrawAllocation")
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);

final int childrenCount = getChildCount();
final int paddingLeft = getPaddingLeft();
final int paddingRight = getPaddingRight();
final int paddingTop = getPaddingTop();
final int paddingBottom = getPaddingBottom();

int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int marginLeft = 0;
int marginTop = 0;
int marginRight = 0;
int marginBottom = 0;

int allChildrenWidth = 0;
int maxChildrenHeight = 0;

for (int i = 0; i < childrenCount; i++) {

View childView = getChildAt(i);
ViewGroup.LayoutParams params = childView.getLayoutParams();
ViewGroup.MarginLayoutParams marginParams;

if (params instanceof ViewGroup.MarginLayoutParams) {
marginParams = (ViewGroup.MarginLayoutParams) params;
} else {
marginParams = new ViewGroup.MarginLayoutParams(params);
}

measureChild(childView, widthMeasureSpec, heightMeasureSpec);
maxChildrenHeight = Math.max(maxChildrenHeight, childView.getMeasuredHeight());
allChildrenWidth += childView.getMeasuredWidth();
marginLeft += marginParams.leftMargin;
marginRight += marginParams.rightMargin;
marginBottom = Math.max(marginBottom, marginParams.bottomMargin);
marginTop = Math.max(marginTop, marginParams.topMargin);

}
if (childrenCount == 0) {
setMeasuredDimension(100, 100);
} else if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(allChildrenWidth + paddingLeft + paddingRight + marginRight + marginLeft,
maxChildrenHeight + paddingTop + paddingBottom + marginTop + marginBottom);
} else if (widthMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(allChildrenWidth + paddingLeft + paddingRight + marginRight + marginLeft, heightSize);
} else if (heightMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSize, maxChildrenHeight + paddingTop + paddingBottom + marginTop + marginBottom);
}
}

@SuppressLint("DrawAllocation")
@Override
protected void onLayout(boolean b, int i, int i1, int i2, int i3) {

// layout children View

final int paddingLeft = getPaddingLeft();
final int paddingTop = getPaddingTop();
final int childrenCount = getChildCount();
int childLeft = paddingLeft;
for (int j = 0; j < childrenCount; j++) {
View childView = getChildAt(j);
ViewGroup.LayoutParams params = childView.getLayoutParams();
ViewGroup.MarginLayoutParams marginParams;
if (params instanceof ViewGroup.MarginLayoutParams) {
marginParams = (ViewGroup.MarginLayoutParams) params;
} else {
marginParams = new ViewGroup.MarginLayoutParams(params);
}
if (childView.getVisibility() != GONE) {
childView.layout(childLeft + marginParams.leftMargin,
marginParams.topMargin + paddingTop,
childLeft + childView.getMeasuredWidth(),
childView.getMeasuredHeight() + paddingTop + marginParams.topMargin);
childLeft += (childView.getMeasuredWidth() + marginParams.rightMargin);
}
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class MyView extends View {
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);

int width = getWidth() - paddingLeft - paddingRight;
int height = getHeight() - paddingBottom - paddingTop;

@SuppressLint("DrawAllocation") Bitmap bitmap = BitmapFactory.decodeResource(mContext.getResources(), R.drawable.archlinux_logo_icon);
@SuppressLint("DrawAllocation") Rect src = new Rect(paddingLeft, paddingTop, paddingLeft + width, paddingTop + height);
canvas.drawBitmap(bitmap, src, src, mPaint);
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// handle wrap_content
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
if (widthMode == MeasureSpec.AT_MOST && heightMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(200, 200);
} else if (widthMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(200, heightSize);
} else if (heightMode == MeasureSpec.AT_MOST) {
setMeasuredDimension(widthSize, 200);
}
}
}

总结如下:

  • 从 View 的测量、布局和绘制原理来看,要实现自定义 View,根据自定义 View 的种类不同,可能分别要自定义实现不同的方法。但是这些方法不外乎:onMeasure() 方法,onLayout() 方法,onDraw() 方法。

  • **onMeasure() 方法 **:单一 View,一般重写此方法,针对 wrap_content 情况,规定 View 默认的大小值,避免于 match_parent 情况一致。ViewGroup,若不重写,就会执行和单子 View 中相同逻辑,不会测量子 View。一般会重写 onMeasure() 方法,循环测量子 View。

  • onLayout() 方法: 单一 View,不需要实现该方法。ViewGroup 必须实现,该方法是个抽象方法,实现该方法,来对子 View 进行布局。

  • onDraw() 方法: 无论单一 View,或者 ViewGroup 都可以实现该方法。

Jetpack Compose

Jetpack Compose 是用于构建原生 Android 界面的新工具包。它可简化并加快 Android 上的界面开发,使用更少的代码、强大的工具和直观的 Kotlin API,快速打造生动而精彩的应用。

更少的代码、直观、加速开发、功能强大

声明式 UI

在传统 View 中,我们通常需要使用 findViewById 方法拿到控件之后,再通过 setText、setImageBitmap 等方法更新界面的数据,这样手动操纵视图的方法会提高出错的可能性。如果一条数据在多个位置呈现,很容易忘记更新显示它的某个视图。此外,当两项更新以出人意料的方式发生冲突时,也很容易造成异常状态。

而 Jetpack Compose 作为声明式 UI,其技术的工作原理是在概念上从头开始重新生成整个屏幕,然后执行必要的更改,我们只需要声明每个控件需要的状态,在我们更新状态的时候,相应的控件会发生 重组,此方法可避免手动更新有状态视图层次结构的复杂性。

简单使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Composeable
fun Content(){
var state by remember {mutableStateOf(0) }
StateContent(state){
state = state +1
}
}

@Composable
fun StateContent(state:Int, add: () -> Unit) {
Column {
Text(text = state.toString())
Button(onClick = { add() }) {
Text("Add")
}
}
}

配合 ViewModel 和 Flow 食用

上面的例子还是不够 MVVM,我们的 State 与事件应该保存在 ViewModel 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class MainViewModel : ViewModel() {
private val _state = MutableStateFlow(0)
val state = _state

fun addNum() {
_state.value = _state.value + 1
}
}

@Composable
fun Content(mainViewModel: MainViewModel) {
val state by mainViewModel.state.collectAsState(initial = 0)

StateContent(state = state) {
mainViewModel.addNum()
}
}

@Composable
fun StateContent(state: Int, addNum: () -> Unit) {
Column {
Text(text = state.toString())
Button(onClick = { addNum() }) {
Text("Add")
}
}
}

以上的做法是 Google 官方推崇的最佳架构,即 数据单向流通,唯一可信源,状态向下传递,事件向上传递

Compose 界面中数据从高级对象向下流向其子级的图示。说明界面元素如何通过触发由应用逻辑处理的事件来响应交互的图示。

重组原理以及性能优化

Composable 的本质

在 Compose 中,普通的函数通过 Composable 注解就变成了构建 UI 的可组合函数,具体其是通过 Compose Compiler Plugin 实现的。

1
2
3
4
@Composable
fun Greeting(msg: String) {
Text(text = "Hello $msg!")
}

通过 Compose Compiler Plugin 将 @Composable Greeting(msg:String) 编译成了 Greeting(final String msg, Composer $composer, final int $changed)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public static final void Greeting(final String msg, Composer $composer,
final int $changed) {

$composer = $composer.startRestartGroup(-1948405856);

int $dirty = $changed;
if (($changed & 14) == 0) {
$dirty = $changed | ($composer.changed(msg) ? 4 : 2);
}

if (($dirty & 11) == 2 && $composer.getSkipping()) {
$composer.skipToGroupEnd();
} else {
TextKt.Text-fLXpl1I(msg, $composer, 0, 0, 65534);
}

ScopeUpdateScope var10000 = $composer.endRestartGroup();

if (var10000 != null) {
var10000.updateScope((Function2)(new Function2() {
public final void invoke(@Nullable Composer $composer, int $force) {
MainActivityKt.Greeting(msg, $composer, $changed | 1);
}
}));
}
}

以上表明每个 Composable 函数其实都对应了一个 ScopeUpdateScope,通过监听来实现重组。

其中 Compose 中 State 的读、写行为是通过 Snapshot(状态快照系统)监听。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@Stable
interface MutableState<T> : State<T> {
override var value: T
}

inline operator fun <T> MutableState<T>.setValue(thisObj: Any?, property: KProperty<*>, value: T) {
this.value = value
}

internal open class SnapshotMutableStateImpl<T>(
value: T,
override val policy: SnapshotMutationPolicy<T>
) : StateObject, SnapshotMutableState<T> {

override var value: T
get() = next.readable(this).value
set(value) = next.withCurrent {
if (!policy.equivalent(it.value, value)) {
next.overwritable(this, it) { this.value = value }
}
}

private var next: StateStateRecord<T> = StateStateRecord(value)
}

internal inline fun <T : StateRecord, R> T.overwritable(
state: StateObject,
candidate: T,
block: T.() -> R
): R {
var snapshot: Snapshot = snapshotInitializer
return sync {
snapshot = Snapshot.current
this.overwritableRecord(state, snapshot, candidate).block() // 更新 next
}.also {
notifyWrite(snapshot, state) // 写入通知
}
}

本质上,它其实就是通过自定义 Getter、Setter 来实现的。当我们定义的 state 变量变化的时候,Compose 可以通过自定义的 Setter 捕获到这一行为,从而调用 ScopeUpdateScope 当中的监听,触发重组。

然后我们看到上面的 notifyWrite 方法

1
2
3
4
@PublishedApi
internal fun notifyWrite(snapshot: Snapshot, state: StateObject) {
snapshot.writeObserver?.invoke(state)
}

这里可以看到快照系统拥有 writeObserver ,事实上他还有 readObserver

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
private inline fun <T> composing(
composition: ControlledComposition,
modifiedValues: IdentityArraySet<Any>?,
block: () -> T
): T {
val snapshot = Snapshot.takeMutableSnapshot(
readObserverOf(composition), writeObserverOf(composition, modifiedValues)
)
try {
return snapshot.enter(block)
} finally {
applyAndCheck(snapshot)
}
}

override fun recordReadOf(value: Any) {
if (!areChildrenComposing) {
composer.currentRecomposeScope?.let {
it.used = true
observations.add(value, it)
...
}
}
}

override fun recordWriteOf(value: Any) = synchronized(lock) {
invalidateScopeOfLocked(value)

derivedStates.forEachScopeOf(value) {
invalidateScopeOfLocked(it)
}
}

private fun invalidateScopeOfLocked(value: Any) {
observations.forEachScopeOf(value) { scope ->
if (scope.invalidateForResult(value) == InvalidationResult.IMMINENT) {
observationsProcessed.add(value, scope)
}
}
}

readObserverOf 来记录哪些 scope 使用了此 state ,而 writeObserverOf 而写入时会找出对应使用此 statescope 使其 invalidate ,然后对于这些 scope 执行重组

性能优化

有了以上知识,我们就可以理解官方文档性能优化中的尽可能延后读取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Composable
fun SnackDetail() {
// ...

Box(Modifier.fillMaxSize()) { // Recomposition Scope Start
val scroll = rememberScrollState(0)
Title(snack, scroll.value)
// ...
} // Recomposition Scope End
}

@Composable
private fun Title(snack: Snack, scroll: Int) {
// ...
val offset = with(LocalDensity.current) { scroll.toDp() }

Column(
modifier = Modifier
.offset(y = offset)
) {
// ...
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Composable
fun SnackDetail() {
// ...

Box(Modifier.fillMaxSize()) { // Recomposition Scope Start
val scroll = rememberScrollState(0)
// ...
Title(snack) { scroll.value }
// ...
} // Recomposition Scope End
}

@Composable
private fun Title(snack: Snack, scrollProvider: () -> Int) {

val offset = with(LocalDensity.current) { scrollProvider().toDp() }
Column(
modifier = Modifier
.offset(y = offset)
) {
// ...
}
}

状态读取发生在哪个 Scope,状态更新的时候,哪个 Scope 就发生重组