绘空 - 探索 Jetpack Compose 实现原理

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

组合在树上

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

移动空间

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

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

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

A B C D E _ _ _ _ _
复制代码

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

A B C _ _ _ _ _ D E
复制代码

以后即可以在 C 后直接插入新的数据:markdown

A B C F G _ _ _ D E
复制代码

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

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

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

好比下面这样一个登陆界面的视图树,这里经过缩进来展现层级。

VerticalLinearLayout
	HorizontalLinearLayout
		AccountHintTextView
		AccountEditText
	HorizontalLinearLayout
		PasswordHintTextView
		PasswordEditText
	LoginButton
复制代码

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

底层数组自己并无办法记录与树有关的信息,所以内部实际维护了其它的数据结构来保存一些节点信息,好比 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 的渲染。

相关文章
相关标签/搜索