三流码奴的自我救赎

0%

Jetpack Compose 核心组件 - Composer

Composer是Compose框架的核心组件。顾名思义,它是用来执行Compose函数维护底层数据结构的工具

Compose所使用的数据结构不同于传统View系统使用的树,而是一种基于连续List的数据结构 - Gap Buffer

Gap Buffer

先来看看Gap Buffer的结构是什么样的,举个例子:

  1. [ ]
  2. 这是一种数据结构[ ]
  3. 这是一种[ ]数据结构
  4. 这是一种基于列表的[ ]数据结构
  5. 这是一种基于[ ]列表的数据结构
  6. 这是一种基于[ ]列表的数据结构
  7. 这是一种基于连续[ ]列表的数据结构

这就是Gap Buffer的基本操作,这种数据结构被广泛用在文本编辑中。

其中[ ]代表gap,具有如下特点:

  • 需要在那里插入或修改内容,就需要将gap移动到对应位置。
  • gap的容量不够的时候需要进行扩容。

可以发现在整体结构不发生改变你的情况下,操作复杂度基本都是O(1),一旦结构发生变动,就需要O(n)的复杂度来完成调整。

考虑到UI的结构(组件的宽高等属性也不会影响到整体结构)不会发生频繁的变动,即便显示的数据频繁变化也不会影响到页面的结构,所以在这种情况下Gap Buffer会是一个比较好的选择。

Compose函数和Composer的关系

现在有如下例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Composable
fun Wrapper(text: String) {
Content(text) {
SubContent(it)
}
}

@Composable
fun Content(
text: String,
content: @Composable (String) -> Unit
) = content(text)


@Composable
fun SubContent(text: String = "Jetpack Compose") {
Text(text)
}

这样写用是没问题的,但问题是这一段代码是如何做到像官网的文档所说的当数据发生改变的时候会自动重组呢?

Compose函数主要分为两种(为了方便表述先这么叫):

  1. 容器
  2. 节点

其中节点类型主要指的就是ComposeNode()函数,而容器类型则主要指的是包裹节点的代码块。

节点

我们直接进入Text组件,它的内部一定使用了Composer来管理Gap Buffer。

中间嵌套了很多层就不都贴出来了,也没什么意义,直接在ComposeNode中找到composer相关的代码块:

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
val currentComposer: Composer
@ReadOnlyComposable
@Composable get() { throw NotImplementedError("Implemented as an intrinsic") }

@OptIn(ComposeCompilerApi::class)
@Composable @ExplicitGroupsComposable
inline fun <T, reified E : Applier<*>> ComposeNode(
noinline factory: () -> T,
update: @DisallowComposableCalls Updater<T>.() -> Unit,
noinline skippableUpdate: @Composable SkippableUpdater<T>.() -> Unit,
content: @Composable () -> Unit
) {
if (currentComposer.applier !is E) invalidApplier()
currentComposer.startNode() // 标记Node开始
if (currentComposer.inserting) {
currentComposer.createNode(factory) // Node创建
} else {
currentComposer.useNode() // Node使用中
}
Updater<T>(currentComposer).update() // 更新
SkippableUpdater<T>(currentComposer).skippableUpdate()
currentComposer.startReplaceableGroup(0x7ab4aae9) // 标记content的起始
content() // content内容,也是一个Composable函数
currentComposer.endReplaceableGroup() // 标记content的结束
currentComposer.endNode() // 标记Node结束
}

通过上面这段代码,现在基本可以推断出:

  1. Composer确实是负责操作Gap Buffer的工具
  2. 在Gap Buffer中至少存在两类区间,一类是Node,另一类的Group,并且都通过Composer进行管理
  3. 这段代码的结构还是比较清晰的,首尾是Node的start和end标记,紧挨着content的两行是content group的start和end标记

所以现在我们对于Compose使用Gap Buffer的方式有了大概的思路:

  • startNode
    • NodeData // node数据
    • startGroup
      • GroupData // group数据
      • startNode / startGroup
        • …….. // Gap
      • endNode / endGroup
    • endGroup
  • endNode

容器

在上面的例子中,我们写的三个Compose函数都是容器

就先拿Wrapper()来看:

1
2
3
4
5
6
@Composable
fun Wrapper(text: String) {
Content(text) {
SubContent(it)
}
}

可以发现这里面并没有对于Group的标记,那么Compose是如何对自定义容器进行标记的呢?

我们找到Composer接口的定义:

1
2
3
4
5
6
7
8
9
/**
* Composer is the interface that is targeted by the Compose Kotlin compiler plugin and used by
* code generation helpers. It is highly recommended that direct calls these be avoided as the
* runtime assumes that the calls are generated by the compiler and contain only a minimum amount
* of state validation.
*/
interface Composer {
...
}

可以看到在Composer的定义中写到,Composer是为Compose Kotlin compiler plugin提供的,并且会被代码生成器使用,也就是说对于容器来说,Group的起始和结束位置的标记调用几乎可以肯定实在编译的时候生成的。

所以我们需要找到生成后的输出类,来搞明白编译器到底做了什么,以及容器的空间如何标记。

探究生成类

不幸的是在.build目录瞎并不能直接找到生成类,但还是可以在生成的classes.dex中找到一些蛛丝马迹。

找到build -> intermediates -> dex -> debug -> mergeProjectDexDebug,找到其中对应的classes.dex文件,是可以看到编译好的类的。但是Android Studio中只能显示字节码,并不能直接反编译成Java,所以还需要一些其他的操作。

将这个classes.dex文件copy出来,利用dex2jar工具可以得到一个jar包,然后将其导入Android工程中,并在gradle中添加外部依赖,就可以查看class文件,并且可以直接使用Android Studio反编译为Java代码。

还是以上面的例子来说,得到的Java代码如下:

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
public final class ComposeTestKt {
/**
* Wrapper
* 在这里就发现我们一直在找的东西了,经过编译之后,Wrapper函数多了两个参数
* 其中String类型的var0就是我们定义的text参数,Composer作为var1传入Wrapper函数,那么var2是什么?
* 目前还不清楚,只能根据下面的代码去推断var2是什么东西。
**/
public static final void Wrapper(final String var0, Composer var1, final int var2) {
Intrinsics.checkNotNullParameter(var0, "text");
// 一直var1是一个composer实例,这一行就是我们在找的Group起始标记,可以发现这里有一个int类型的常量作为参数
// 可以推断这个常量应该是一个随机生成的id,用于标记Group的起始位置
var1 = var1.startRestartGroup(2121373260, "C(Wrapper)7@174L44:ComposeTest.kt#kzsjk3");


// 这里可以看出,Compose使用前面我们不知道是什么东西的var2和Composer.changed()方法
// 得出了var5,而紧接着就通过var5和Composer.getSkipping()方法推断出是否要执行Composer.skipToGroupEnd(),
// 而var2参与的运算基本都是逻辑运算,到这里就基本可以断定var2是一个按位记录data有没有改变的记录值。

int var5;
if ((var2 & 14) == 0) {
byte var3;
if (var1.changed(var0)) {
var3 = 4;
} else {
var3 = 2;
}

var5 = var3 | var2;
} else {
var5 = var2;
}

// 如果child需要的data改变则需要再次调用child的Compose函数,通知其data已改变并触发recompose,否则直接调过。
// 如果需要进行recompose,data会被传给后续需要的child,同时将var2传递给child,
// child可以根据var2来判断自身是否需要更新data
// 在这里我们就可以理解清楚了,Compose就是通过这种方式来最小化recompose操作的
if ((2 ^ var5 & 11) == 0 && var1.getSkipping()) { // 注意这里的skipping不是说直接跳过整个更新过程,而是代表是否可以不执行Composable函数创建child
// 不需要创建child,在skipToGroupEnd中触发recompose,更新数据
var1.skipToGroupEnd();
} else {
// 创建child
Content(var0, com.example.jetpackcomposetast.ui.test.ComposableSingletons.ComposeTestKt.INSTANCE.getLambda-1$app_debug(), var1, var5 & 14);
}

// 在执行Composer.endRestartGroup()的时候会返回一个ScopeUpdateScope的实例,随后对该实例中的一个lambda进行了更新
ScopeUpdateScope var4 = var1.endRestartGroup();
if (var4 != null) {
var4.updateScope((Function2)(new Function2() {
// $FF: synthetic method
// $FF: bridge method
public Object invoke(Object var1, Object var2x) {
this.invoke((Composer)var1, ((Number)var2x).intValue());
return Unit.INSTANCE;
}

public final void invoke(Composer var1, int var2x) {
ComposeTestKt.Wrapper(var0, var1, var2 | 1);
}
}));
}

}

public static final void Content(final String var0, final Function3 var1, Composer var2, final int var3) {
Intrinsics.checkNotNullParameter(var0, "text");
Intrinsics.checkNotNullParameter(var1, "content");
var2 = var2.startRestartGroup(-662051966, "C(Content)P(1)16@311L13:ComposeTest.kt#kzsjk3");
// ... 略
}

public static final void SubContent(final String var0, Composer var1, final int var2) {
Intrinsics.checkNotNullParameter(var0, "text");
var1 = var1.startRestartGroup(431678665, "C(SubContent)21@374L10:ComposeTest.kt#kzsjk3");
// ... 略
}

到目前为止,基本的流程算是已经明了:

  1. 编译器在编译阶段为Compose函数添加两个参数:一个Composer实例用于维护Gap Buffer,还有一个按位存储data改变状态的记录值,用于比较数据,避免不必要的recompose
  2. 更新ScopeUpdateScope,以便在触发recompose的时候得到正确的结果

现在还剩一个问题,在上面的代码中看不出来最后具体由哪里调用。

可以发现这个lambda看起来非常像是触发recompose的时候调用的代码块,所以尝试着搜索”Recompose”关键字,果不其然可以找到实现类 RecomposeScopeImpl,同时实现了ScopeUpdateScope和RecomposeScope两个接口:

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
/**
* A RecomposeScope is created for a region of the composition that can be recomposed independently
* of the rest of the composition. The composer will position the slot table to the location
* stored in [anchor] and call [block] when recomposition is requested. It is created by
* [Composer.startRestartGroup] and is used to track how to restart the group.
*/
@OptIn(ComposeCompilerApi::class)
internal class RecomposeScopeImpl(
var composition: CompositionImpl?
) : ScopeUpdateScope, RecomposeScope {

// ... 略

/**
* Restart the scope's composition. It is an error if [block] was not updated. The code
* generated by the compiler ensures that when the recompose scope is used then [block] will
* be set but it might occur if the compiler is out-of-date (or ahead of the runtime) or
* incorrect direct calls to [Composer.startRestartGroup] and [Composer.endRestartGroup].
*/
fun compose(composer: Composer) {
block?.invoke(composer, 1) ?: error("Invalid restart scope")
}

/**
* Update [block]. The scope is returned by [Composer.endRestartGroup] when [used] is true
* and implements [ScopeUpdateScope].
*/
override fun updateScope(block: (Composer, Int) -> Unit) { this.block = block }

// ... 略
}

可以看到在这里updateScoper()方法中对this.block进行了更新,而触发recompose时通过compose()方法调用this.block

其中updateScope()方法正如同之前猜测的,就是ScopeUpdateScope中的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* Internal compose compiler plugin API that is used to update the function the composer will
* call to recompose a recomposition scope. This should not be used or called directly.
*/
@ComposeCompilerApi
interface ScopeUpdateScope {
/**
* Called by generated code to update the recomposition scope with the function to call
* recompose the scope. This is called by code generated by the compose compiler plugin and
* should not be called directly.
*/
fun updateScope(block: (Composer, Int) -> Unit)
}

而recompose的方法RecomposeScopeImpl.compose()的调用位置可以在Composer.skipToGroupEnd()中找到。

官方资料

可以在公众号Android开发者中找到:深入详解 Jetpack Compose | 实现原理