探索 Jetpack Compose 实现原理「终于解决」

探索 Jetpack Compose 实现原理「终于解决」浅析声明式界面开发框架 Jetpack Compose 在 Android 平台上的实现原理与渲染方式。

在 Flutter 中,Dart 对如何高效回收频繁创建与销毁的对象进行了专门优化,而 Compose 在 Android 平台的实现方式本质上就是普通的 Kotlin/JVM 代码。如何设计 Compose 让其能够有可靠的性能表现,是一个挺有意思的问题。

组合在树上

在 2019 年,Leland Richardson 就在 Google Android Dev Summit 的 Understanding Compose 演讲中简要描述了 Compose 的实现原理与底层数据结构,并根据演讲内容在 Medium 上发布了两篇博客(Part 1Part 2),所以这里仅进行简单的重述。

移动空间

Compose Runtime 采用了一种特殊的数据结构,称为 Slot Table。

Slot Table 与常用于文本编辑器的另一数据结构 Gap Buffer 相似,这是一个在连续空间中存储数据的类型,底层采用数组实现。区别于数组常用方式的是,它的剩余空间,称为 Gap,可根据需要移动到 Slot Table 中的任一区域,这让它在数据插入与删除时更高效。

简单来讲,一个 Slot Table 可能长这个样子,其中 _ 表示一个未使用的数组元素,这些空间便构成了 Gap:

A B C D E _ _ _ _ _

假设要在 C 后插入新的数据,则将 Gap 移动到 C 之后:

A B C _ _ _ _ _ D E

之后便可以在 C 后直接插入新的数据:

A B C F G _ _ _ D E

Slot Table 其本质又是一个线性的数据结构,因此可以采用树存储到数组的方式,将视图树存储在 Slot Table 中,加上 Slot Table 可移动插入点的特性,让视图树在变动之后无需重新创建整个数据结构,所以 Slot Table 其实是用数组实现了树的存储。

需要注意的是,Slot Table 相比普通数组实现了任意位置插入数据的功能,这是不能到能的跨越,但实际由于元素拷贝的原因,Gap 移动仍是一个需要尽量避免的低效操作。Google 选择这一数据结构的原因在于,他们预计界面更新大部分为数据变更,即只需要更新视图树节点数据,而视图树结构并不会经常变动。

之所以 Google 不采用树或链表等数据结构,猜测可能是数组这种内存连续的数据结构在访问效率上才能达到 Compose Runtime 的要求。

比如下面这样一个登录界面的视图树,这里通过缩进来展示层级。

VerticalLinearLayout
 HorizontalLinearLayout AccountHintTextView AccountEditText HorizontalLinearLayout PasswordHintTextView PasswordEditText LoginButton 

在 Slot Table 中,树的子节点被称为 Node,非子节点被称为 Group。

底层数组本身并没有办法记录与树有关的信息,因此内部实际维护了其它的数据结构来保存一些节点信息,比如 Group 包含的 Node 个数,Node 直接所属的 Group 等。

环境

@Composable 是 Compose 系统的核心之一,被 @Composable 所注解的函数称为 可组合函数,下文也如此称呼。

这并不是一个普通的注解,添加该注解的函数会被真实地改变类型,改变方式与 suspend 类似,在编译期进行处理,只不过 Compose 并非语言特性,无法采用语言关键字的形式进行实现。

以 Android Studio 生成的 Compose App 模版为例,其中包含这样一个可组合函数:

@Composable
fun Greeting(name: String) {
    Text(text = "Hello $name!")
}

通过工具进行反编译可得到实际代码:

public static final void Greeting(String name, Composer $composer, int $changed) {
    Intrinsics.checkNotNullParameter(name, HintConstants.AUTOFILL_HINT_NAME);
    Composer $composer2 = $composer.startRestartGroup(105642380);
    ComposerKt.sourceInformation($composer2, "C(Greeting)51@1521L27:MainActivity.kt#xfcxsz");
    int $dirty = $changed;
    if (($changed & 14) == 0) {
        $dirty |= $composer2.changed(name) ? 4 : 2;
    }
    if ((($dirty & 11) ^ 2) != 0 || !$composer2.getSkipping()) {
        TextKt.m866Text6FffQQw(LiveLiterals$MainActivityKt.INSTANCE.m4017String$0$str$arg0$callText$funGreeting() + name + LiveLiterals$MainActivityKt.INSTANCE.m4018String$2$str$arg0$callText$funGreeting(), null, Color.m1136constructorimpl(ULong.m2785constructorimpl(0)), TextUnit.m2554constructorimpl(0), null, null, null, TextUnit.m2554constructorimpl(0), null, null, TextUnit.m2554constructorimpl(0), null, false, 0, null, null, $composer2, 0, 0, 65534);
    } else {
        $composer2.skipToGroupEnd();
    }
    ScopeUpdateScope endRestartGroup = $composer2.endRestartGroup();
    if (endRestartGroup != null) {
        endRestartGroup.updateScope(new MainActivityKt$Greeting$1(name, $changed));
    }
}

public final class MainActivityKt$Greeting$1 extends Lambda implements Function2<Composer, Integer, Unit> {
    final int $$changed;
    final String $name;

    MainActivityKt$Greeting$1(String str, int i) {
        super(2);
        this.$name = str;
        this.$$changed = i;
    }

    @Override
    public Unit invoke(Composer composer, Integer num) {
        invoke(composer, num.intValue());
        return Unit.INSTANCE;
    }

    public final void invoke(Composer composer, int i) {
        MainActivityKt.Greeting(this.$name, composer, this.$$changed | 1);
    }
}

转换为等效且可读性强的 Kotlin 伪代码如下:

fun Greeting(name: String, parentComposer: Composer, changed: Int) {
    val composer = parentComposer.startRestartGroup(GROUP_HASH)

    val dirty = calculateState(changed)
    
    if (stateHasChanged(dirty) || composer.skipping) {
        Text("Hello $name", composer = composer, changed = ...)
    } else {
        composer.skipToGroupEnd()
    }

    composer.endRestartGroup()?.updateScope {
        Greeting(name, changed)
    }
}

可见被 @Composable 注解后,函数增添了额外的参数,其中的 Composer 类型参数作为运行环境贯穿在整个可组合函数调用链中,所以可组合函数无法在普通函数中调用,因为不包含相应的环境。因为环境传入的关系,调用位置不同的两个相同的可组合函数调用,其实现效果并不相同。

可组合函数实现的起始与结尾通过 Composer.startRestartGroup()Composer.endRestartGroup() 在 Slot Table 中创建 Group,而可组合函数内部所调用的可组合函数在两个调用之间创建新的 Group,从而在 Slot Table 内部完成视图树的构建。

Composer 根据当前是否正在修改视图树而确定这些调用的实现类型。

在视图树构建完成后,若数据更新导致部分视图需要刷新,此时非刷新部分对应可组合函数的调用就不再是进行视图树的构建,而是视图树的访问,正如代码中的 Composer.skipToGroupEnd() 调用,表示在访问过程中直接跳到当前 Group 的末端。

Composer 对 Slot Table 的操作是读写分离的,只有写操作完成后才将所有写入内容更新到 Slot Table 中。

除此之外,可组合函数还将通过传入标记参数的位运算判断内部的可组合函数执行或跳过,这可以避免访问无需更新的节点,提升执行效率。

重组

前面的文字与代码提到两点,一是可组合函数可通过传入标记参数的位运算判断内部的可组合函数执行或跳过,二是可组合函数内 Composer.endRestartGroup() 返回了一个 ScopeUpdateScope 类型对象,其 ScopeUpdateScope.updateScope() 函数被调用,传入了调用当前可组合函数的 Lambda。这些内容表明,Compose Runtime 可根据当前环境确定可组合函数的调用范围。

当视图数据发生变动时,Compose Runtime 会根据数据影响范围确定需要重新执行的可组合函数,这一步骤被称为重组,前面代码中执行 ScopeUpdateScope.updateScope() 的作用便是注册重组需要执行的可组合函数。

updateScope 这个函数名称具有迷惑性,传入的 Lambda 是一个回调,并不会立即执行,更利于理解的名称是 onScopeUpdatesetUpdateScope

为了说明 Compose 的重组机制,就需要聊一聊 Compose 管理数据的结构,State。

因为 Compose 是一个声明式(Declarative)框架,State 采用观察者模式来实现界面随数据自动更新,首先用一个例子来说明 State 的使用方式。

@Composable fun Content() {
    val state by remember { mutableStateOf(1) }
    Column {
        Button(onClick = { state++ }) {
            Text(text = "click to change state")
        }
        Text("state value: $state")
    }
}

remember() 是一个可组合函数,类似于 lazy,其作用是在可组合函数调用中记忆对象。可组合函数在调用链位置不变的情况下,调用 remember() 即可获取上次调用时记忆的内容。

这与可组合函数的特性相关,可理解为 remember() 在树的当前位置记录数据,也意味着同一个可组合函数在不同调用位置被调用,内部的 remember() 获取内容并不相同,这是因为调用位置不同,对应树上的节点也不同。

因观察者模式的设计,当 state 写入数据时会触发重组,因此可以猜测触发重组的实现在 State 写入的实现中。

mutableStateOf() 最终会返回 ParcelableSnapshotMutableState 的对象,相关代码位于其超类 SnapshotMutableStateImpl

/** * A single value holder whose reads and writes are observed by Compose. * * Additionally, writes to it are transacted as part of the [Snapshot] system. * * @param value the wrapped value * @param policy a policy to control how changes are handled in a mutable snapshot. * * @see mutableStateOf * @see SnapshotMutationPolicy */
internal open class SnapshotMutableStateImpl<T>(
    value: T,
    override val policy: SnapshotMutationPolicy<T>
) : StateObject, SnapshotMutableState<T> {
    @Suppress("UNCHECKED_CAST")
    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) 
  
    ...
}

StateStateRecord.overwritable() 最终会调用 notifyWrite() 实现观察者的通知。

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

下一步便是确定回调,通过 Debugger 可以快速定位到 writeObserverGlobalSnapshotManager.ensureStarted() 中被注册:

/** * Platform-specific mechanism for starting a monitor of global snapshot state writes * in order to schedule the periodic dispatch of snapshot apply notifications. * This process should remain platform-specific; it is tied to the threading and update model of * a particular platform and framework target. * * Composition bootstrapping mechanisms for a particular platform/framework should call * [ensureStarted] during setup to initialize periodic global snapshot notifications. * For Android, these notifications are always sent on [AndroidUiDispatcher.Main]. Other platforms * may establish different policies for these notifications. */
internal object GlobalSnapshotManager {
    private val started = AtomicBoolean(false)

    fun ensureStarted() {
        if (started.compareAndSet(false, true)) {
            val channel = Channel<Unit>(Channel.CONFLATED)
            CoroutineScope(AndroidUiDispatcher.Main).launch {
                channel.consumeEach {
                    Snapshot.sendApplyNotifications()
                }
            }
            Snapshot.registerGlobalWriteObserver {
                channel.offer(Unit)
            }
        }
    }
}

当向 channel 推送对象,在 主线程 触发 Snapshot.sendApplyNotifications() 调用后,调用链会到达 advanceGlobalSnapshot(),这里实现了数据更新监听器的回调。

private fun <T> advanceGlobalSnapshot(block: (invalid: SnapshotIdSet) -> T): T {
    ...

    // If the previous global snapshot had any modified states then notify the registered apply
    // observers.
    val modified = previousGlobalSnapshot.modified
    if (modified != null) {
        val observers: List<(Set<Any>, Snapshot) -> Unit> = sync { applyObservers.toMutableList() }
        observers.fastForEach { observer ->
            observer(modified, previousGlobalSnapshot)
        }
    }

    ...
}

Recompose

通过 Debugger 进行调试与筛选,可以发现 observers 包含了两个回调,其中一个位于 Recomposer.recompositionRunner()

/** * The scheduler for performing recomposition and applying updates to one or more [Composition]s. */
// RedundantVisibilityModifier suppressed because metalava picks up internal function overrides
// if 'internal' is not explicitly specified - b/171342041
// NotCloseable suppressed because this is Kotlin-only common code; [Auto]Closeable not available.
@Suppress("RedundantVisibilityModifier", "NotCloseable")
@OptIn(InternalComposeApi::class)
class Recomposer(
    effectCoroutineContext: CoroutineContext
) : CompositionContext() {
    ...

    @OptIn(ExperimentalComposeApi::class)
    private suspend fun recompositionRunner( block: suspend CoroutineScope.(parentFrameClock: MonotonicFrameClock) -> Unit ) {
        withContext(broadcastFrameClock) {
            ...

            // Observe snapshot changes and propagate them to known composers only from
            // this caller's dispatcher, never working with the same composer in parallel.
            // unregisterApplyObserver is called as part of the big finally below
            val unregisterApplyObserver = Snapshot.registerApplyObserver { changed, _ ->
                synchronized(stateLock) {
                    if (_state.value >= State.Idle) {
                        snapshotInvalidations += changed
                        deriveStateLocked()
                    } else null
                }?.resume(Unit)
            }

            ...
        }
    }
  
    ...
}

触发回调将增加 snapshotInvalidations 中的元素,后续说明。

AbstractComposeView.onAttachToWindow() 被调用时,Recomposer.runRecomposeAndApplyChanges() 被调用,并启用循环等待重组事件。

...
class Recomposer(
    effectCoroutineContext: CoroutineContext
) : CompositionContext() {
    ...
  
    /** * Await the invalidation of any associated [Composer]s, recompose them, and apply their * changes to their associated [Composition]s if recomposition is successful. * * While [runRecomposeAndApplyChanges] is running, [awaitIdle] will suspend until there are no * more invalid composers awaiting recomposition. * * This method will not return unless the [Recomposer] is [close]d and all effects in managed * compositions complete. * Unhandled failure exceptions from child coroutines will be thrown by this method. */
    suspend fun runRecomposeAndApplyChanges() = recompositionRunner { parentFrameClock ->
        ...
        while (shouldKeepRecomposing) {
            ...
            
            // Don't await a new frame if we don't have frame-scoped work
            if (
                synchronized(stateLock) {
                    if (!hasFrameWorkLocked) {
                        recordComposerModificationsLocked()
                        !hasFrameWorkLocked
                    } else false
                }
            ) continue

            // Align work with the next frame to coalesce changes.
            // Note: it is possible to resume from the above with no recompositions pending,
            // instead someone might be awaiting our frame clock dispatch below.
            // We use the cached frame clock from above not just so that we don't locate it
            // each time, but because we've installed the broadcastFrameClock as the scope
            // clock above for user code to locate.
            parentFrameClock.withFrameNanos { frameTime ->
                ...
                trace("Recomposer:recompose") {
                    ...
                    val modifiedValues = IdentityArraySet<Any>()
                    try {
                        toRecompose.fastForEach { composer ->
                            performRecompose(composer, modifiedValues)?.let {
                                toApply += it
                            }
                        }
                        if (toApply.isNotEmpty()) changeCount++
                    } finally {
                        toRecompose.clear()
                    }

                    ...
                }
            }
        }
    }
  
    ...
}

当重组事件产生时,recordComposerModificationLocked() 将触发,compositionInvalidations 中的内容被更新,而该对象的更新依赖于 snapshotInvalidations,最终导致 hasFrameWorkLocked 变更为 true

AndroidUiFrameClock.withFrameNanos() 将被调用,这将向 Choreographer 注册垂直同步信号回调,Recomposer.performRecompose() 最终将触发从 ScopeUpdateScope.updateScope() 注册 Lambda 的调用。

class AndroidUiFrameClock(
    val choreographer: Choreographer
) : androidx.compose.runtime.MonotonicFrameClock {
    override suspend fun <R> withFrameNanos( onFrame: (Long) -> R ): R {
        val uiDispatcher = coroutineContext[ContinuationInterceptor] as? AndroidUiDispatcher
        return suspendCancellableCoroutine { co ->
            // Important: this callback won't throw, and AndroidUiDispatcher counts on it.
            val callback = Choreographer.FrameCallback { frameTimeNanos ->
                co.resumeWith(runCatching { onFrame(frameTimeNanos) })
            }

            // If we're on an AndroidUiDispatcher then we post callback to happen *after*
            // the greedy trampoline dispatch is complete.
            // This means that onFrame will run on the current choreographer frame if one is
            // already in progress, but withFrameNanos will *not* resume until the frame
            // is complete. This prevents multiple calls to withFrameNanos immediately dispatching
            // on the same frame.

            if (uiDispatcher != null && uiDispatcher.choreographer == choreographer) {
                uiDispatcher.postFrameCallback(callback)
                co.invokeOnCancellation { uiDispatcher.removeFrameCallback(callback) }
            } else {
                choreographer.postFrameCallback(callback)
                co.invokeOnCancellation { choreographer.removeFrameCallback(callback) }
            }
        }
    }
}

Invalidate

同样,通过 Debugger 进行调试与筛选,可以定位到另一个回调是 SnapshotStateObserver.applyObserver

class SnapshotStateObserver(private val onChangedExecutor: (callback: () -> Unit) -> Unit) {
    private val applyObserver: (Set<Any>, Snapshot) -> Unit = { applied, _ ->
        var hasValues = false

        ...
        if (hasValues) {
            onChangedExecutor {
                callOnChanged()
            }
        }
    }
  
    ... 
}

SnapshotStateObserver.callOnChanged() 可定位到回调 LayoutNodeWrapper.Companion.onCommitAffectingLayer

调用链:

SnapshotStateObserver.callOnChanged() –>

SnapshotStateObserver.ApplyMap.callOnChanged() –>

SnapshotStateObserver.ApplyMap.onChanged.invoke() – implementation ->

LayoutNodeWrapper.Companion.onCommitAffectingLayer.invoke()

/** * Measurable and Placeable type that has a position. */
internal abstract class LayoutNodeWrapper(
    internal val layoutNode: LayoutNode
) : Placeable(), Measurable, LayoutCoordinates, OwnerScope, (Canvas) -> Unit {
    
    ...
    
    internal companion object {
        ...
        private val onCommitAffectingLayer: (LayoutNodeWrapper) -> Unit = { wrapper ->
            wrapper.layer?.invalidate()
        }
        ...
    }
}

最终在 RenderNodeLayer.invalidate() 中触发顶层 AndroidComposeView 重绘,实现视图更新。

/** * RenderNode implementation of OwnedLayer. */
@RequiresApi(Build.VERSION_CODES.M)
internal class RenderNodeLayer(
    val ownerView: AndroidComposeView,
    val drawBlock: (Canvas) -> Unit,
    val invalidateParentLayer: () -> Unit
) : OwnedLayer {
    ...
    override fun invalidate() {
        if (!isDirty && !isDestroyed) {
            ownerView.invalidate()
            ownerView.dirtyLayers += this
            isDirty = true
        }
    }
    ...
}

绘空

Compose 是如何绘制的?

可组合函数的执行完成了视图树的构建,但并没有进行视图树的渲染,两者的实现是分离的,系统会将重组函数运行完成后生成的视图树交由渲染模块运行。

可组合函数不一定仅在主线程运行,甚至可能在多个线程中并发运行,但这不意味着可以在可组合函数中直接进行耗时操作,因为可组合函数可能会被频繁调用,甚至一帧一次。

重组是乐观的操作,当数据在重组完成前更新,本次重组可能会被取消,因此可重组函数在设计上应该幂等且没有附带效应,相关内容可了解函数式编程。

构造

在 Google 关于 Compose 与 View 进行兼容的文档中提到了 ComposeViewAbstractComposeView,但如果查看代码会发现,这与我们前文提及的 AndroidComposeView 并没有继承关系。

先通过 官方示例 看一看如何将可组合函数转换为 View:

@Composable
fun CallToActionButton( text: String, onClick: () -> Unit, modifier: Modifier = Modifier, ) {
    Button(
        colors = ButtonDefaults.buttonColors(
            backgroundColor = MaterialTheme.colors.secondary
        ),
        onClick = onClick,
        modifier = modifier,
    ) {
        Text(text)
    }
}

class CallToActionViewButton @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
    defStyle: Int = 0
) : AbstractComposeView(context, attrs, defStyle) {

    var text by mutableStateOf<String>("")
    var onClick by mutableStateOf<() -> Unit>({})

    @Composable
    override fun Content() {
        YourAppTheme {
            CallToActionButton(text, onClick)
        }
    }
}

寻找 AbstractComposeView.Content() 的调用方,最终会定位到 ViewGroup.setContent() 扩展函数,

/** * Composes the given composable into the given view. * * The new composition can be logically "linked" to an existing one, by providing a * [parent]. This will ensure that invalidations and CompositionLocals will flow through * the two compositions as if they were not separate. * * Note that this [ViewGroup] should have an unique id for the saved instance state mechanism to * be able to save and restore the values used within the composition. See [View.setId]. * * @param parent The [Recomposer] or parent composition reference. * @param content Composable that will be the content of the view. */
internal fun ViewGroup.setContent( parent: CompositionContext, content: @Composable () -> Unit ): Composition {
    GlobalSnapshotManager.ensureStarted()
    val composeView =
        if (childCount > 0) {
            getChildAt(0) as? AndroidComposeView
        } else {
            removeAllViews(); null
        } ?: AndroidComposeView(context).also { addView(it.view, DefaultLayoutParams) }
    return doSetContent(composeView, parent, content)
}

可见,View Group 将只保留一个 AndroidComposeView 视图,同时 doSetContent() 函数将组合函数设置到 AndroidComposeView 中。

渲染

可组合函数的调用最终会构筑出包含数据与视图信息的树,各种视图类型可组合函数最终都将调用可组合函数 ReusableComposeNode(),并创建一个 LayoutNode 对象作为子节点记录到树中。

LayoutNode 的存在类似于 Flutter 中的 Element,它们是视图树结构的组成部分,并且是相对稳定的。

Compose 在 Android 上的实现最终依赖于 AndroidComposeView,且这是一个 ViewGroup,那么按原生视图渲染的角度,看一下 AndroidComposeViewonDraw()dispatchDraw() 的实现,即可看到 Compose 渲染的原理。

@SuppressLint("ViewConstructor", "VisibleForTests")
@OptIn(ExperimentalComposeUiApi::class)
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
internal class AndroidComposeView(context: Context) :
    ViewGroup(context), Owner, ViewRootForTest, PositionCalculator {
    
    ...
    
    override fun onDraw(canvas: android.graphics.Canvas) {
    }
      
    ...
    
    override fun dispatchDraw(canvas: android.graphics.Canvas) {
        ...
        measureAndLayout()

        // we don't have to observe here because the root has a layer modifier
        // that will observe all children. The AndroidComposeView has only the
        // root, so it doesn't have to invalidate itself based on model changes.
        canvasHolder.drawInto(canvas) { root.draw(this) }

        ...
    }
    
    ...
}

CanvasHolder.drawInto()android.graphics.Canvas 转化为 androidx.compose.ui.graphics.Canvas 实现传递至顶层 LayoutNode 对象 rootLayoutNode.draw() 函数中,实现视图树的渲染。

由于各种视图类型可组合函数的设计不同,这里仅以绘制 Bitmap 的可组合函数 Image() 作为例子,其实现如下。

/** * A composable that lays out and draws a given [ImageBitmap]. This will attempt to * size the composable according to the [ImageBitmap]'s given width and height. However, an * optional [Modifier] parameter can be provided to adjust sizing or draw additional content (ex. * background). Any unspecified dimension will leverage the [ImageBitmap]'s size as a minimum * constraint. * * The following sample shows basic usage of an Image composable to position and draw an * [ImageBitmap] on screen * @sample androidx.compose.foundation.samples.ImageSample * * For use cases that require drawing a rectangular subset of the [ImageBitmap] consumers can use * overload that consumes a [Painter] parameter shown in this sample * @sample androidx.compose.foundation.samples.BitmapPainterSubsectionSample * * @param bitmap The [ImageBitmap] to draw * @param contentDescription text used by accessibility services to describe what this image * represents. This should always be provided unless this image is used for decorative purposes, * and does not represent a meaningful action that a user can take. This text should be * localized, such as by using [androidx.compose.ui.res.stringResource] or similar * @param modifier Modifier used to adjust the layout algorithm or draw decoration content (ex. * background) * @param alignment Optional alignment parameter used to place the [ImageBitmap] in the given * bounds defined by the width and height * @param contentScale Optional scale parameter used to determine the aspect ratio scaling to be used * if the bounds are a different size from the intrinsic size of the [ImageBitmap] * @param alpha Optional opacity to be applied to the [ImageBitmap] when it is rendered onscreen * @param colorFilter Optional ColorFilter to apply for the [ImageBitmap] when it is rendered * onscreen */
@Composable
fun Image( bitmap: ImageBitmap, contentDescription: String?, modifier: Modifier = Modifier, alignment: Alignment = Alignment.Center, contentScale: ContentScale = ContentScale.Fit, alpha: Float = DefaultAlpha, colorFilter: ColorFilter? = null ) {
    val bitmapPainter = remember(bitmap) { BitmapPainter(bitmap) }
    Image(
        painter = bitmapPainter,
        contentDescription = contentDescription,
        modifier = modifier,
        alignment = alignment,
        contentScale = contentScale,
        alpha = alpha,
        colorFilter = colorFilter
    )
}

/** * Creates a composable that lays out and draws a given [Painter]. This will attempt to size * the composable according to the [Painter]'s intrinsic size. However, an optional [Modifier] * parameter can be provided to adjust sizing or draw additional content (ex. background) * * **NOTE** a Painter might not have an intrinsic size, so if no LayoutModifier is provided * as part of the Modifier chain this might size the [Image] composable to a width and height * of zero and will not draw any content. This can happen for Painter implementations that * always attempt to fill the bounds like [ColorPainter] * * @sample androidx.compose.foundation.samples.BitmapPainterSample * * @param painter to draw * @param contentDescription text used by accessibility services to describe what this image * represents. This should always be provided unless this image is used for decorative purposes, * and does not represent a meaningful action that a user can take. This text should be * localized, such as by using [androidx.compose.ui.res.stringResource] or similar * @param modifier Modifier used to adjust the layout algorithm or draw decoration content (ex. * background) * @param alignment Optional alignment parameter used to place the [Painter] in the given * bounds defined by the width and height. * @param contentScale Optional scale parameter used to determine the aspect ratio scaling to be used * if the bounds are a different size from the intrinsic size of the [Painter] * @param alpha Optional opacity to be applied to the [Painter] when it is rendered onscreen * the default renders the [Painter] completely opaque * @param colorFilter Optional colorFilter to apply for the [Painter] when it is rendered onscreen */
@Composable
fun Image( painter: Painter, contentDescription: String?, modifier: Modifier = Modifier, alignment: Alignment = Alignment.Center, contentScale: ContentScale = ContentScale.Fit, alpha: Float = DefaultAlpha, colorFilter: ColorFilter? = null ) {
    val semantics = if (contentDescription != null) {
        Modifier.semantics {
            this.contentDescription = contentDescription
            this.role = Role.Image
        }
    } else {
        Modifier
    }

    // Explicitly use a simple Layout implementation here as Spacer squashes any non fixed
    // constraint with zero
    Layout(
        {},
        modifier.then(semantics).clipToBounds().paint(
            painter,
            alignment = alignment,
            contentScale = contentScale,
            alpha = alpha,
            colorFilter = colorFilter
        )
    ) { _, constraints ->
        layout(constraints.minWidth, constraints.minHeight) {}
    }
}

这里构建了一个包含 BitmapPainterModifier 传入 Layout() 中,而这一 Modifier 对象最终会被设置到对应的 LayoutNode 对象中。

而由前文提及的,当 LayoutNode.draw() 被调用时,其 outLayoutNodeWrapperLayoutNodeWrapper.draw() 会被调用。

/** * An element in the layout hierarchy, built with compose UI. */
internal class LayoutNode : Measurable, Remeasurement, OwnerScope, LayoutInfo, ComposeUiNode {
    ...
    
    internal fun draw(canvas: Canvas) = outerLayoutNodeWrapper.draw(canvas)
    
    ...
}

/** * Measurable and Placeable type that has a position. */
internal abstract class LayoutNodeWrapper(
    internal val layoutNode: LayoutNode
) : Placeable(), Measurable, LayoutCoordinates, OwnerScope, (Canvas) -> Unit {
		...
    
    /** * Draws the content of the LayoutNode */
    fun draw(canvas: Canvas) {
        val layer = layer
        if (layer != null) {
            layer.drawLayer(canvas)
        } else {
            val x = position.x.toFloat()
            val y = position.y.toFloat()
            canvas.translate(x, y)
            performDraw(canvas)
            canvas.translate(-x, -y)
        }
    }
    
    ...
}

经过多层委托之后,LayoutNodeWrapper.draw() 将调用 InnerPlaceholder.performDraw() 实现对子视图的渲染分发。

internal class InnerPlaceable(
    layoutNode: LayoutNode
) : LayoutNodeWrapper(layoutNode), Density by layoutNode.measureScope {
    ...
  
    override fun performDraw(canvas: Canvas) {
        val owner = layoutNode.requireOwner()
        layoutNode.zSortedChildren.forEach { child ->
            if (child.isPlaced) {
                child.draw(canvas)
            }
        }
        if (owner.showLayoutBounds) {
            drawBorder(canvas, innerBoundsPaint)
        }
    }
  
    ...
}

最终到达渲染 Bitmap 的 Image 视图节点时,LayoutNodeWrapper 的实现是 ModifiedDrawNode

internal class ModifiedDrawNode(
    wrapped: LayoutNodeWrapper,
    drawModifier: DrawModifier
) : DelegatingLayoutNodeWrapper<DrawModifier>(wrapped, drawModifier), OwnerScope {
    ...
  
    // This is not thread safe
    override fun performDraw(canvas: Canvas) {
        ...
        val drawScope = layoutNode.mDrawScope
        drawScope.draw(canvas, size, wrapped) {
            with(drawScope) {
                with(modifier) {
                    draw()
                }
            }
        }
    }
  
    ...
}

这里调用的是 PainterModifierDrawScope.draw() 实现。

这是采用 Kotlin 扩展函数实现的一种非常奇特的写法,扩展函数可以作为接口函数,由接口实现类实现,调用时则必须通过 with()apply()run() 等设定 this 范围的函数构建环境。

但是这种写法在多层 this 嵌套时,可读性上还需进行探讨,正如上方对 DrawScope.draw() 的调用。如果无法理解上方的代码包含了什么值得吐槽的东西,可以看看下面的例子 🤔。

class Api {
    fun String.show() {
        println(this)
    }
}

fun main() {
    "Hello world!".apply {
        Api().apply {
            show()
        }
    }
}

接着调用 BitmapPainterDrawScope.onDraw() 实现。

/** * [Painter] implementation used to draw an [ImageBitmap] into the provided canvas * This implementation can handle applying alpha and [ColorFilter] to it's drawn result * * @param image The [ImageBitmap] to draw * @param srcOffset Optional offset relative to [image] used to draw a subsection of the * [ImageBitmap]. By default this uses the origin of [image] * @param srcSize Optional dimensions representing size of the subsection of [image] to draw * Both the offset and size must have the following requirements: * * 1) Left and top bounds must be greater than or equal to zero * 2) Source size must be greater than zero * 3) Source size must be less than or equal to the dimensions of [image] */
class BitmapPainter(
    private val image: ImageBitmap,
    private val srcOffset: IntOffset = IntOffset.Zero,
    private val srcSize: IntSize = IntSize(image.width, image.height)
) : Painter() {
    ...

    override fun DrawScope.onDraw() {
        drawImage(
            image,
            srcOffset,
            srcSize,
            dstSize = IntSize(
                this@onDraw.size.width.roundToInt(),
                this@onDraw.size.height.roundToInt()
            ),
            alpha = alpha,
            colorFilter = colorFilter
        )
    }

    ...
}

DrawScope.onDraw() 中将调用 androidx.compose.ui.graphics.Canvas 进行绘制,而这最终将委托给其内部持有的 android.graphics.Canvas 对象绘制,最终实现 Compose 的渲染。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
转载请注明出处: https://daima100.com/13051.html

(0)

相关推荐

  • Python安装tar.gz教程

    Python安装tar.gz教程对于一些在Linux系统下进行Python开发的人来说,安装Python tar.gz是一个非常常见的任务。Python tar.gz是源代码压缩包,通常有一些优点,比如自由度更高、安装更灵活、可以自己手动编译。在这篇文章中,我将提供一些简单且易于跟随的步骤,以便让读者可以轻松地完成Python tar.gz的安装。

    2024-06-28
    58
  • mysql笔记-

    mysql笔记-用单表接近一千万数据(四列:int,两个varchar,日期时间)测,发现几个效率几乎一致 count(0)=count(1)=count(数字)=count(*)>count(primary

    2023-02-15
    144
  • 用Vue3造个轮子吧:拖拽缩放组件,支持吸附对齐,实时参考线「终于解决」

    用Vue3造个轮子吧:拖拽缩放组件,支持吸附对齐,实时参考线「终于解决」可配合内置的DraggableContainer组件方便地实现参考线以及自动吸附功能。 该组件所支持的参数和事件加起来有几十种,可进行各种配置,具体可查看Github的详细文档。 下面我来介绍下使用方式。 执行use(Vue3DraggableResizable)后将会全局注册…

    2023-08-06
    122
  • MySQL锁表解决方案(亲测)「终于解决」

    MySQL锁表解决方案(亲测)「终于解决」1,MySQL5.6版本之前的解决方案: 查询表锁定的SQL语句: SELECT r.trx_id waiting_trx_id, r.trx_mysql_thread_id waiting_thr…

    2023-03-30
    684
  • MySQL触发器了解一下「终于解决」

    MySQL触发器了解一下「终于解决」简介 触发器是与表有关的数据库对象,当表发生INSERT/UPDATE/DELETE操作时,对应操作的触发器会被触发,将在这些操作之前或之后执行触发器中定义的SQL语句集合。 触发器的使用 创建触发器

    2023-04-12
    149
  • lols4天赋(lol新手天赋一览表)

    lols4天赋(lol新手天赋一览表)

    2023-09-12
    127
  • DataHub——实时数据治理平台

    DataHub——实时数据治理平台DataHub 首先,阿里云也有一款名为DataHub的产品,是一个流式处理平台,本文所述DataHub与其无关。 数据治理是大佬们最近谈的一个火热的话题。不管国家层面,还是企业层面现在对这个问题是越

    2023-02-25
    145
  • 4.DBus学习

    4.DBus学习1、什么是DBus DBus(数据总线)项目为了统一数据采集需求而生, 专注于数据的收集及实时数据流计算,通过简单灵活的配置,以无侵入的方式对源端数据进行采集,采用高可用的流式计算框架,对公司各个I…

    2023-03-31
    283

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注