揭秘 Compose 原理,理解 Composable 的本质

深入浅出 Jetpack Compose:从底层原理到渲染机制 Jetpack Compose 彻底改变了 Android 开发模式,从传统命令式转向声明式 UI。其核心公式 UI=f(State) 实现状态驱动更新,通过编译器插件将 @Composable 函数转换为 UI 描述。渲染过程分为组合、布局和绘制三个阶段,采用 Slot Table 机制和单次测量优化性能。Snapshot 系统确保状态变更安全可靠。相比传统 View 体系,Compose 消除了状态不一致问题,提升了代码复用性和性能上限,并实现真正的跨平台能力。这场开发范式的革命,让开发者只需定义数据流 阅读详情

你好,我是朱涛。

今年的Google I/O大会上,Android官方针对Jetpack Compose给出了一系列的性能优化建议,文档和视频都已经放出来了。总的来说,官方的内容都非常棒,看完以后我也有些意犹未尽。推荐你去看看。

不过,如果你对Compose的底层原理不是特别熟悉的话,那么,看完Android官方的文档、视频以后,你心中可能还是会充满疑问,似懂非懂。毕竟,官方对「Compose性能优化」主题的定义是「Intermediate 中阶」。对于一门刚推出不到一年的新技术而言,「中阶」已经是比较高的要求了。

当然,你也别担心,我写这篇文章的目的,就是为了让更多的Compose开发者能够看懂官方的Compose性能优化建议。

换句话说,我们只需要补「亿点点」Compose的「底层原理」即可。[捂脸]

Composable 的本质

我们都知道,Jetpack Compose最神奇的地方就是:可以用 Kotlin 写UI界面(无需XML)。而且,借助Kotlin的高阶函数特性,Compose UI界面的写法也非常的直观。

// 代码段1

@Composable
fun Greeting() { // 1
    Column { // 2
        Text(text = "Hello")
        Text(text = "Jetpack Compose!")
    }
}

上面这段代码,即使你没有任何Compose基础,应该也可以轻松理解。Column相当于Android当中纵向的线性布局LinearLayout,在这个布局当中,我们放了两个Text控件。

最终的UI界面展示,如下图所示。

例子虽然简单,但是上面的代码中,还是有两个细节需要我们注意,我已经用注释标记出来了:

注释1,Greeting()它是一个Kotlin的函数,如果抛开它的@Composable注解不谈的话,那么,它的函数类型应该是() -> Unit。但是,由于@Composable是一个非常特殊的注解,Compose的编译器插件会把它当作影响函数类型的因子之一。所以,Greeting()它的函数类型应该是@Composable () -> Unit。(顺便提一句,另外两个常见的函数类型影响因子是:suspend、函数类型的接收者。)

注释2:Column {},请留意它的{},我们之所以可以这样写代码,这其实是Kotlin提供的高阶函数简写。它完整的写法应该是这样的:

// 代码段2

Column(content =  {
    log(2)
    Text(text = "Hello")
    log(3)
    Text(text = "Jetpack Compose!")
})

由此可见,Compose的语法,其实就是通过Kotlin的高阶函数实现的。Column()、Text()看起来像是在调用UI控件的构造函数,但它实际上只是一个普通的顶层函数,所以说,这只是一种DSL的“障眼法”而已。

备注:如果你想研究如何用Kotlin编写DSL,可以去看看我公众号的历史文章。

那么,到这里,我们其实可以做出一个阶段性的总结了:Composable的本质,是函数。这个结论看似简单,但它却可以为后面的原理研究打下基础。

接下来,我们来聊聊Composable的特质。

Composable 的特质

前面我们已经说过了,Composable本质上就是函数。那么,它的特质,其实跟普通的函数也是非常接近的。这个话看起来像是废话,让我来举个例子吧。

基于前面的代码,我们增加一些log:

// 代码段3

@Composable
fun Greeting() {
    log(1)
    Column {
        log(2)
        Text(text = "Hello")
        log(3)
        Text(text = "Jetpack Compose!")
    }
    log(4)
}

private fun log(any: Any) {
    Log.d("MainActivity", any.toString())
}

请问,上面代码的输出结果是怎样的呢?如果你看过我的协程教程,那么心里肯定会有点“虚”,对吧?不过,上面这段代码的输出结果是非常符合直觉的。

// 输出结果
// 注意:当前Compose版本为1.2.0-beta
// 在未来的版本当中,Compose底层是可能做出优化,并且改变这种行为模式的。

com.boycoder.testcompose D/MainActivity: 1
com.boycoder.testcompose D/MainActivity: 2
com.boycoder.testcompose D/MainActivity: 3
com.boycoder.testcompose D/MainActivity: 4

你看,Composable不仅从源码的角度上看是个普通的函数,它在运行时的行为模式,跟普通的函数也是类似的。我们写出来的Composable函数,它们互相嵌套,最终会形成一个树状结构,准确来说是一个N叉树。而Composable函数的执行顺序,其实就是对一个N叉树的DFS遍历。

这样一来,我们写出来的Compose UI就几乎是:“所见即所得”。

也许,你会觉得,上面这个例子,也不算什么,毕竟,XML也可以做到类似的事情。那么,让我们来看另外一个例子吧。

// 代码段4

@Composable
fun Greeting() {
    log("start")
    Column {
        repeat(4) {
            log("repeat $it")
            Text(text = "Hello $it")
        }
    }
    log("end")
}

// 输出结果:
com.boycoder.testcompose D/MainActivity: start
com.boycoder.testcompose D/MainActivity: repeat 0
com.boycoder.testcompose D/MainActivity: repeat 1
com.boycoder.testcompose D/MainActivity: repeat 2
com.boycoder.testcompose D/MainActivity: repeat 3
com.boycoder.testcompose D/MainActivity: end

我们使用repeat{}重复调用了4次Text(),我们就成功在屏幕上创建了4个Text控件,最关键的是,它们还可以在Column{}当中正常纵向排列。这样的代码模式,在从前的XML时代是不可想象的。

话说回来,正是因为Composable的本质就是函数,它才会具备普通函数的一些特质,从而,也让我们可以像写普通代码一样,用逻辑语句来描述UI布局。

好了,现在我们已经知道了Composable的本质是函数,可是,我们手机屏幕上的那些UI控件是怎么出现的呢?接下来,我们需要再学「一点点」Compose编译器插件的知识。PS:这回,我保证真的是「一点点」。

Compose 编译器插件

虽然Compose Compiler Plugin看起来像是一个非常高大上的东西,但从宏观概念上来看的话,它所做的事情还是很简单的。

如果你看过我的博客《图解协程原理》的话,你一定会知道,协程的suspend关键字,它可以改变函数的类型,Compose的注解@Composable也是类似的。总的来说,它们之间的对应关系是这样的:

具体来说,我们在Kotlin当中写的Composable函数、挂起函数,在经过编译器转换以后,都会被额外注入参数。对于挂起函数来说,它的参数列表会多出一个Continuation类型的参数;对于Composable函数,它的参数列表会多出一个Composer类型的参数。

为什么普通函数无法调用「挂起函数」和「Composable函数」,底层的原因就是:普通函数根本无法传入Continuation、Composer作为调用的参数。

注意:需要特殊说明的是,在许多场景下,Composable函数经过Compose Compiler Plugin转换后,其实还可能增加其他的参数。更加复杂的情况,我们留到后续的文章里再分析。

另外,由于Compose并不是属于Kotlin的范畴,为了实现Composable函数的转换,Compose团队是通过「Kotlin编译器插件」的形式来实现的。我们写出的Kotlin代码首先会被转换成IR,而Compose Compiler Plugin则是在这个阶段直接改变了它的结构,从而改变了最终输出的Java字节码以及Dex。这个过程,也就是我在文章开头放那张动图所描述的行为。

动图我就不重复贴了,下面是一张静态的流程图。

不过,Compose Compiler 不仅仅只是改变「函数签名」那么简单,如果你将Composable函数反编译成Java代码,你就会发现它的函数体也会发生改变。

让我们来看一个具体的例子,去发掘Compose的「重组」(Recompose)的实现原理。

Recompose 的原理

// 代码段5

class MainActivity : ComponentActivity() {
    // 省略
    
    @Composable
    fun Greeting(msg: String) {
        Text(text = "Hello $msg!")
    }
}

上面的代码很简单,Greeting()的逻辑十分简单,不过当它被反编译成Java后,它实际的逻辑会变复杂许多。

// 代码段6

public static final void Greeting(final String msg, Composer $composer,
 final int $changed) { // 多出来的changed我们以后分析吧
 
  //                        1,开始
  //                          ↓
  $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);
  }

  //                                  2,结束
  //                                     ↓
  ScopeUpdateScope var10000 = $composer.endRestartGroup();
  
  if (var10000 != null) {
     var10000.updateScope((Function2)(new Function2() {
        public final void invoke(@Nullable Composer $composer, int $force) {
           //              3,递归调用自己
           //                ↓
           MainActivityKt.Greeting(msg, $composer, $changed | 1);
        }
     }));
  }
}

毫无疑问,Greeting()反编译后,之所以会变得这么复杂,背后的原因全都是因为Compose Compiler Plugin。上面这段代码里值得深挖的细节太多了,为了不偏离主题,我们暂时只关注其中的3个注释,我们一个个看。

  • 注释1,composer.startRestartGroup,这是Compose编译器插件为Composable函数插入的一个辅助代码。它的作用是在内存当中创建一个可重复的Group,它往往代表了一个Composable函数开始执行了;同时,它还会创建一个对应的ScopeUpdateScope,而这个ScopeUpdateScope则会在注释2处用到。
  • 注释2,composer.endRestartGroup(),它往往代表了一个Composable函数执行的结束。而这个Group,从一定程度上,也描述了UI的结构与层级。另外,它也会返回一个ScopeUpdateScope,而它则是触发「Recompose」的关键。具体的逻辑我们看注释3。
  • 注释3,我们往ScopeUpdateScope.updateScope()注册了一个监听,当我们的Greeting()函数需要重组的时候,就会触发这个监听,从而递归调用自身。这时候你会发现,前面提到的RestartGroup也暗含了「重组」的意味。

由此可见,Compose当中看起来特别高大上的「Recomposition」,其实就是:“重新调用一次函数”而已。

那么,Greeting()到底是在什么样的情况下才会触发「重组」呢?我们来看一个更加完整的例子。

// 代码段7

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MainScreen()
        }
    }
}

@Composable
fun MainScreen() {
    log("MainScreen start")
    val state = remember { mutableStateOf("Init") }
    // 1
    LaunchedEffect(key1 = Unit) {
        delay(1000L)
        state.value = "Modified"
    }
    Greeting(state.value)
    log("MainScreen end")
}

private fun log(any: Any) {
    Log.d("MainActivity", any.toString())
}

@Composable
fun Greeting(msg: String) {
    log("Greeting start $msg")
    Text(text = "Hello $msg!")
    log("Greeting end $msg")
}

/* 输出结果 
MainActivity: MainScreen start
MainActivity: Greeting start Init
MainActivity: Greeting end Init
MainActivity: MainScreen end
等待 1秒
MainActivity: MainScreen start        // 重组
MainActivity: Greeting start Modified // 重组
MainActivity: Greeting end Modified   // 重组
MainActivity: MainScreen end          // 重组
*/

上面的代码逻辑仍然十分的简单,setContent {}调用了MainScreen();MainScreen()调用了Greeting()。唯一需要注意的,就是注释1处的LaunchedEffect{},它的作用是启动一个协程,延迟1秒,并对state进行赋值。

从代码的日志输出,我们可以看到,前面4个日志输出,是Compose初次执行触发的;后面4个日志输出,则是由state改变导致的「重组」。看起来,Compose通过某种机制,捕捉到了state状态的变化,然后通知了MainScreen()进行了重组。

如果你足够细心的话,你会发现,state实际上只在Greeting()用到了,而state的改变,却导致MainScreen()、Greeting()都发生了「重组」,MainScreen()的「重组」看起来是多余。这里其实就藏着Compose性能优化的一个关键点。

注意:类似上面的情况,Compose Compiler 其实做了足够多的优化,MainScreen()的「重组」看似是多余的,但它实际上对性能的影响并不大,我们举这个例子只是为了讲明白「重组」的原理,引出优化的思路。Compose Compiler 具体的优化思路,我们留到以后再来分析。

让我们改动一下上面的代码:

// 代码段8

class MainActivity : ComponentActivity() {
    // 不变
}

@Composable
fun MainScreen() {
    log("MainScreen start")
    val state = remember { mutableStateOf("Init") }
    LaunchedEffect(key1 = Unit) {
        delay(1000L)
        state.value = "Modified"
    }
    Greeting { state.value } // 1,变化在这里
    log("MainScreen end")
}

private fun log(any: Any) {
    Log.d("MainActivity", any.toString())
}

@Composable   // 2,变化在这里 ↓
fun Greeting(msgProvider: () -> String) {
    log("Greeting start ${msgProvider()}") // 3,变化
    Text(text = "Hello ${msgProvider()}!") // 3,变化
    log("Greeting end ${msgProvider()}")   // 3,变化
}

/*
MainActivity: MainScreen start
MainActivity: Greeting start Init
MainActivity: Greeting end Init
MainActivity: MainScreen end
等待 1秒
MainActivity: Greeting start Modified  // 重组
MainActivity: Greeting end Modified    // 重组
*/

代码的变化我用注释标记出来了,主要的变化在:注释2,我们把原先String类型的参数改为了函数类型:() -> String。注释1、3处改动,都是跟随注释2的。

请留意代码的日志输出,这次,「重组」的范围发生了变化,MainScreen()没有发生重组!这是为什么呢?这里涉及到两个知识点:一个是Kotlin函数式编程当中的「Laziness」;另一个是Compose重组的「作用域」。我们一个个来看。

Laziness

Laziness 在函数式编程当中是个相当大的话题,要把这个概念将透的话,得写好几篇文章才行,这里我简单解释下,以后有机会我们再深入讨论。

理解 Laziness 最直观的办法,就是写一段这样对比的代码:

// 代码段9

fun main() {
    val value = 1 + 2
    val lambda: () -> Int = { 1 + 2 }
    println(value)
    println(lambda)
    println(lambda())
}

其实,如果你对Kotlin高阶函数、Lambda理解透彻的话,你马上就能理解代码段8当中的Laziness是什么意思了。如果你对这Kotlin的这些基本概念还不熟悉,可以去看看我公众号的历史文章。

上面这段代码的输出结果如下:

3
Function0<java.lang.Integer>
3

这样的输出结果也很好理解。1 + 2是一个表达式,当我们把它用{}包裹起来以后,它就一定程度上实现了Laziness,我们访问lambda的时候并不会触发实际的计算行为。只有调用lambda()的时候,才会触发实际的计算行为。

Laziness讲清楚了,我们来看看Compose的重组「作用域」。

重组「作用域」

其实,在前面的代码段6处,我们就已经接触过它了,也就是ScopeUpdateScope。通过前面的分析,我们每个Composable函数,其实都会对应一个ScopeUpdateScope,Compiler底层就是通过注入监听,来实现「重组」的。

实际上,Compose底层还提供一个:状态快照系统(SnapShot)。Compose的快照系统底层的原理还是比较复杂的,以后有机会我们再深入探讨,更多信息你可以看看这个链接。

总的来说,SnapShot 可以监听Compose当中State的读、写行为。

// 代码段10

@Stable
interface MutableState<T> : State<T> {
    override var value: T
}

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 }
            }
        }
}

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

那么,代码段7、代码段8,它们之间的差异到底在哪里呢?关键其实就在于ScopeUpdateScope的不同。

这其中的关联,其实用一句话就可以总结:状态读取发生在哪个Scope,状态更新的时候,哪个Scope就发生重组。

如果你看不懂这句话也没关系,我画了一个图,描述了代码段7、代码段8之间的差异:

对于代码段7,当state的读取发生在MainScreen()的ScopeUpdateScope,那么,当state发生改变的时候,就会触发MainScreen()的Scope进行「重组」。

代码段8也是同理:

现在,回过头来看这句话,相信你就能看懂了:状态读取发生在哪个Scope,状态更新的时候,哪个Scope就发生重组。

好,做完前面这些铺垫以后,我们就可以轻松看懂Android官方给出的其中三条性能优化建议了。

  1. Defer reads as long as possible.
  2. Use derivedStateOf to limit recompositions
  3. Avoid backwards writes

以上这3条建议,本质上都是为了尽可能避免「重组」,或者缩小「重组范围」。由于篇幅限制,我们就挑第一条来详细解释吧~

尽可能延迟State的读行为

其实,对于我们代码段7、代码段8这样的改变,Compose的性能提升不明显,因为Compiler底层做了足够多的优化,多一个层级的函数调用,并不会有明显差异。Android官方更加建议我们将某些状态的读写延迟到Layout、Draw阶段。

这就跟Compose整个执行、渲染流程相关了。总的来说,对于一个Compose页面来说,它会经历以下4个步骤:

  • 第一步,Composition,这其实就代表了我们的Composable函数执行的过程。
  • 第二步,Layout,这跟我们View体系的Layout类似,但总体的分发流程是存在一些差异的。
  • 第三步,Draw,也就是绘制,Compose的UI元素最终会绘制在Android的Canvas上。由此可见,Jetpack Compose虽然是全新的UI框架,但它的底层并没有脱离Android的范畴。
  • 第四步,Recomposition,重组,并且重复1、2、3步骤。

总体的过程如下图所示:

Android官方推荐我们尽可能推迟状态读取的原因,其实还是希望我们可以在某些场景下直接跳过Recomposition的阶段、甚至Layout的阶段,只影响到Draw。

而实现这一目标的手段,其实就是我们前面提到的「Laziness」思想。让我们以官方提供的代码为例:

首先,我要说明的是,Android官方文档当中的注释其实是存在一个小瑕疵的。它对新手友好,但容易对我们深入底层的人产生困扰。上面代码中描述的Recomposition Scope并不准确,它真正的Recomposition Scope,应该是整个SnackDetail(),而不是Box()。对此,我已经在Twitter与相关的Google工程师反馈了,对方也回复了我,这是“故意为之”的,因为这更容易理解。具体细节,你可以去这条Twitter看看。

好,我们回归正题,具体分析一下这个案例:

// 代码段11

@Composable
fun SnackDetail() {
    // Recomposition Scope
    // ...

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

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

    Column(
        modifier = Modifier
            .offset(y = offset) // 2,状态使用
    ) {
        // ...
    }
}

上面的代码有两个注释,注释1,代表了状态的读取;注释2,代表了状态的使用。这种“状态读取与使用位置不一致”的现象,其实就为Compose提供了性能优化的空间。

那么,具体我们该如何优化呢?其实很简单,借助我们之前Laziness的思想,让:“状态读取与使用位置一致”。

// 代码段12

@Composable
fun SnackDetail() {
    // Recomposition Scope 
    // ...

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

@Composable
private fun Title(snack: Snack, scrollProvider: () -> Int) {
    // ...
    val offset = with(LocalDensity.current) { scrollProvider().toDp() }
    Column(
        modifier = Modifier
            .offset(y = offset) // 2,状态读取+使用
    ) {
    // ...
    }
}

请留意注释1这里的变化,由于我们将scroll.value变成了Lambda,所以,它并不会在composition期间产生状态读取行为,这样,当scroll.value发生变化的时候,就不会触发「重组」,这就是「Laziness」的意义。

代码段11、代码段12之间的差异是巨大的:

前者会在页面滑动的期间频繁触发:「重组」+「Layout」+「Draw」,后者则完全绕过了「重组」,只有「Layout」+「Draw」,由此可见,它的性能提升也是非常显著的。

结尾

OK,到这里,我们这篇文章就该结束了。我们来简单总结一下:

  • 第一,Composable函数的本质,其实就是函数。多个Composable函数互相嵌套以后,就自然形成了一个UI树。Composable函数执行的过程,其实就是一个DFS遍历过程。
  • 第二,@Composable修饰的函数,最终会被Compose编译器插件修改,不仅它的函数签名会发生变化,它函数体的逻辑也会有天翻地覆的改变。函数签名的变化,导致普通函数无法直接调用Composable函数;函数体的变化,是为了更好的描述Compose的UI结构,以及实现「重组」。
  • 第三,重组,本质上就是当Compose状态改变的时候,Runtime对Composable函数的重复调用。这涉及到Compose的快照系统,还有ScopeUpdateScope。
  • 第四,由于ScopeUpdateScope取决于我们对State的读取位置,因此,这就决定了我们可以使用Kotlin函数式编程当中的Laziness思想,对Compose进行「性能优化」。也就是让:状态读取与使用位置一致,尽可能缩小「重组作用域」,尽可能避免「重组」发生。
  • 第五,今年的Google I/O大会上,Android官方团队提出了:5条性能优化的最佳实践,其中3条建议的本质,都是在践行:状态读取与使用位置一致的原则。
  • 第六,我们详细分析了其中的一条建议「尽可能延迟State的读行为」。由于Compose的执行流程分为:「Composition」、「Layout」、「Draw」,通过Laziness,我们可以让Compose跳过「重组」的阶段,大大提升Compose的性能。

结束语

其实,Compose的原理还是相当复杂的。它除了UI层跟Android有较强的关联以外,其他的部分Compiler、Runtime、Snapshot都是可以独立于Android以外而存在的。这也是为什么JetBrains可以基于Jetpack Compose构建出Compose-jb的原因。

这篇文章是「沉思录」系列除了开篇以外的第一篇文章,后续除了Compose以后,还会有更多Kotlin、Android,以及其他底层的原创技术文章,敬请关注。

感谢你的阅读,我们下期再见!

在这里插入图片描述

Android Composable 与 View 的联系和区别 ‌优先选择 Compose‌:新项目、动态 UI(如动画、主题切换)或需快速迭代的场景‌‌沿用传统 View‌:维护旧项目或依赖原生控件(如 WebView、地图 SDK)的场景‌。 阅读详情

相关推荐

深度解析 Jetpack Compose中的@Composable和DisposableEffect:优雅处理资源生命周期

随着Jetpack Compose在Android应用开发中崭露头角,@Composable注解和DisposableEffect函数成为引领现代UI开发的关键工具。本文将深入研究DisposableEffect,这一在Compose中处理资源生命周期的利器。我们将探索其独特的语法结构、生动的示例代码,以及在实际应用中的多种场景,为开发者打开Jetpack Compose的新篇章。

专注于深入研究多种编程语言,以实战为导向,逐步拓展开发技能,提升工程化编码和思维能力,展现无敌技术实力。 1732

Compose 基础 - 可组合函数(重组、稳定性) @Composable

原因:调用 forEach() 遍历时,生成4个 Item(子组合项)并依次设置 Element(列表元素),LaunchEffect() 也就输出了4次,但是由于 Item 和 Element 没有一一对应,当列表头部新插入一个 Element 时,会把新 Element 设置到第一个 Item 上,其余 Element 依次设置到后面的 Item 上,最后一个 Element 设置到新生成的 Item 上,因此前四个 Item 发生了重组而最后一个是新增的就不存在重组了。

HugMua的博客 3218

深入解析Sigma Delta调制器 —— 从电流采样到数字信号处理的完整链路

本文深入解析了Sigma Delta调制器的工作原理与应用。通过剖析过采样、噪声整形与负反馈核心机制,并结合AMC1306等实例,详细阐述了从电流采样到数字信号处理的完整链路。该技术凭借其高精度和高抗干扰能力,在ADC、电流测量及音频处理等领域发挥着关键作用。

ttt77的博客 643

Android jectpack compose最全解析【建议收藏】

动态展示UI:在执行过程之中不断的变化UI,就需要不断的对其进行验证,并且确保其依赖关系。还要保证生命周期内的依赖满足情况。紧耦合:一处代码影响多处,并且大概率情况是隐式,看起来毫无关联,实际有影响命令式UI:在编写UI代码时,随时都要考虑如何转换到对应的状态单继承:是否可以通过其他方式实现突破单继承的限制?代码膨胀:随着业务的不断扩展,如何控制代码膨胀?

m0_56255097的博客 2835

4×V100部署Qwen3.8-Flash-Next:vLLM/GGUF实战

大模型本地部署的核心,是把参数量、量化精度、显存容量和推理框架放在同一张账本里算清楚。MoE 模型总参数量大但激活参数少,解码阶段更受显存带宽与 KV Cache 限制;通过 INT4/AWQ 或 GGUF 等 4-bit 量化可显著压缩权重占用,再结合 vLLM 张量并行、llama.cpp 分层切分提升多卡利用率。其技术价值在于让 V100 这类老架构 GPU 也能承载百亿级参数模型的私有化推理,并服务于 RAG 知识库、工作流 API、批量生成等场景。这次围绕 4×V100 32G 部署 Qwen3.

weixin_29061425的博客 348

arXiv订阅进阶玩法:除了邮件,还能用RSS和Python脚本打造你的专属论文追踪器

本文介绍了arXiv订阅的进阶玩法,教你如何通过RSS和Python脚本打造个性化的论文追踪器。突破传统邮件订阅的局限,实现实时更新、精准过滤和自动化管理,提升科研效率。特别适合需要高效追踪最新研究进展的学者和开发者。

weixin_26757925的博客 627

Jetpack Compose深度解析:声明式UI架构揭秘与实战指南

Jetpack Compose作为Android官方推出的现代声明式UI框架,彻底改变了Android应用开发范式。它通过基于Kotlin的声明式API,显著提升了UI开发效率并降低了维护成本。本文将从架构原理、性能优化到生产实践,全面剖析Jetpack Compose的核心机制,为中级开发者和技术决策者提供深度技术参考。 ## 问题:传统Android UI开发的局限性与挑战 ### 传统V

gitblog_00938的博客 680

SuccessFactors入门篇

SuccessFactors入门篇—— Employee Central(一)最近通过一个月左右的学习,拿到了SF EC(SuccessFactors Employee Central )模块SAP认证,所以打算把自己学习SF的整个过程以及相关资料和大家分享一下,相互切磋,共同进步。Employee Central简单了解Employee Central简单了解新的改变功能快捷键合理的创建标题,有...

qq_42288853的博客 1万+

Jetpack Compose Navigation 3.0 完全指南

Nav3 将返回栈从黑盒变为开发者可自由操作的 List,导航退化为 add/remove,真正实现单一可信源与状态驱动 UI。原生支持大屏自适应布局,大幅降低多设备适配成本。

ITMonkeyKing的博客 530

基于ensp大学网络规划与设计(完整文档+ensp拓扑图)

大家好,我是小华学长,一名计算机领域的博主。经过多年的学习和实践,我积累了丰富的计算机知识和经验,在这里我想与大家分享我的学习心得和技巧,帮助你成为更好的程序员。 作为一名计算机博主,我一直专注于编程、算法、软件开发等领域,在这些方面积累了大量的经验。我相信分享是一种双赢的方式,通过分享,我可以帮助他人提升技术水平,同时也能够得到学习交流的机会。 在我的文章中,你将会看到我对于各种编程语言、开发工具以及常见问题的解析和分析。我会结合自己的实际项目经验,为你提供实用的解决方案和优化技巧。我相信这些经验不仅能够

qq1325513482的博客 5万+

《UDS协议从入门到精通》系列——图解0x85:控制DTC的设置

如何让ECU停止或恢复DTC状态位更新?有哪些注意点?图解+示例教你快速上手UDS 0x85服务!!

车载系统攻城狮 3863

Android Compose 指南:Column与LazyColumn的抉择

在 Android 开发中,Column 和 LazyColumn 虽然都能实现子组件的垂直排列,但它们在工作方式、性能表现和滚动机制等方面存在着显著的区别。Column 简单直接,适用于数据量较小且相对固定的场景,能快速加载和渲染界面;而 LazyColumn 采用懒加载机制,在处理大数据集时表现出色,能够有效避免内存占用过高和界面卡顿的问题,为用户提供流畅的体验。在实际开发中,我们要根据具体的数据量和业务需求,合理地选择使用 Column 或 LazyColumn。

qq_32890771的博客 570

c#【保姆级教程】基于System.Text.Json 支持多态类型序列化! 手把手教你实现一个通用的 PolymorphicJsonConverter!

我们需要自己实现一个继承自的转换器,在序列化时记录具体类型,在反序列化时根据类型标识还原成正确的子类对象。public abstract class WorkStepBase { /* 抽象方法等 */ }public class Person : WorkStepBase { /* 实现 */ }public class Product : WorkStepBase { /* 实现 */ }特性说明✅ 自动发现子类无需手动维护类型列表,自动扫描程序集✅ 类名映射。

一个编程的同学 425

Android 面试题全攻略:基于 android-interview-questions 的 Cheat Sheet 系统化备战指南

> 本文以开源仓库 [android-interview-questions](https://link.gitcode.com/i/aea0b8d8519762fe81565b35fbdf8cd5) 的 README 为核心骨架,系统梳理了其中覆盖的 14 大 Android 面试知识域、数百条高频面试问题与内联答案,并结合仓库源码进行佐证与延伸。读完本文,你将获得一份可直接对照自测的完整面试题

gitblog_02572的博客 1096

华为OD机试双机位C卷-恢复数字序列 (Py/Java/C/C++/Js/Go)

华为OD机试双机位C卷 恢复数字序列,提供C语言、Java、C++、Python、Go、JavaScript源码实现,并提供完整思路讲解。华为OD机试 - 恢复数字序列。

qq_45776114的博客 1203

IDEA的使用指南

IDEA基于java的使用手册,可以最快速的上手,在使用中不断的丰富自己的使用技巧.

Espresso核心机制揭秘:同进程注入、自动同步与IdlingResource实战

UI自动化测试是保障Android应用质量的关键手段,其稳定性直接影响回归效率。与黑盒框架不同,基于同进程注入的测试框架能直接访问应用内部状态,借助UI线程自动同步机制,在消息队列空闲时精确执行操作,避免固定等待带来的随机失败。围绕异步资源管理,IdlingResource扩展了框架对网络请求、图片加载等任务的感知边界,让等待条件清晰可控。在工程实践中,按场景选择Espresso、UIAutomator或Compose Test,并配合动画关闭、数据隔离、用例独立等策略,可大幅提升测试套件的可靠性。本文从框

cigang4063的博客 373

Data-Structures-and-Algorithm-Analysis-in-Java.pdf

作者: Weiss, Mark A. 出版社: Addison Wesley Data Structures and Algorithm Analysis in Java is an “advanced algorithms” book that fits between traditional CS2 and Algorithms Analysis courses. In the old ACM Curriculum Guidelines, this course was known as CS7. This text is for readers who want to learn good programming and algorithm analysis skills simultaneously so that they can develop such programs with the maximum amo...

Saleor电商平台架构深度解析:高性能可组合API的核心技术揭秘

Saleor作为一个基于GraphQL的无头电商平台,采用现代化的微服务架构设计,为开发者提供了构建高性能、可扩展电商系统的完整解决方案。其核心架构围绕GraphQL API构建,支持多租户、多语言、多货币等企业级需求,同时保持出色的性能表现。本文将从技术架构、核心模块、性能优化、扩展机制和生产部署五个维度进行深度剖析。 ## 架构设计解析:分层解耦与数据流控制 Saleor采用典型的分层架构

gitblog_00033的博客 422

Camera2Demo

在Camera2 的基础上进行封装,使得Camera2一堆初始化逻辑与Activity分离,降低耦合度,完成预览拍照并保存的操作

Animeko(Ani)一站式弹幕追番平台技术解析:Kotlin Multiplatform 架构、聚合数据源与 BitTorrent 引擎

Animeko 是一个集"找番、追番、看番"于一体的一站式弹幕追番平台,支持云收藏同步(Bangumi)、离线缓存、BitTorrent 下载与弹幕云过滤,其客户端主体采用 100% Kotlin/Compose Multiplatform 实现。本文以仓库 [README.md](https://link.gitcode.com/i/eccc139a081e1b2018c2a61219a37b0

gitblog_00191的博客 527

Jetpack Compose 深入探索系列一:Composable 函数

一般来说,只有当运行时对需要运行的代码有一定的确定性时,才可能进行运行时优化,因此它可以从中假设特定的条件和行为。它可以将重组安排到不同的线程中,以利用多核心提升性能,或者它可以根据自己的需要或优先级以任意顺序运行可组合函数 (例如:没有显示在屏幕上的组合可以被分配一个较低的优先级)为何不能很好地组合在一起,因为你不能从同步函数中调用异步函数,除非你使同步也成为异步的,或者提供一种等待机制,允许调用异步函数并等待它们的结果。Compose会选择重新启动树中的哪些节点,以保持其内存中的表示状态始终是最新的。

川峰的博客 3259

Compose原理-Compose的设计原理

Compose原理解析系列之一Compose的设计原理

分享+记录 1528

Android前沿技术学习——Compose布局原理+配置布局+四大块技术全方面解析

布局系统的 Jetpack Compose 实现有两个主要目标:一是实现高性能,二是让开发者能够轻松编写自定义布局。在 Compose 中,通过避免多次测量布局子级可实现高性能。如果需要进行多次测量,Compose 具有一个特殊系统,即固有特性测量。注意:使用 Android View 系统时,在嵌套某些 View(如 )时,可能会出现一些性能问题。由于 Compose 可以避免多次测量,因此可以根据需要进行深层次嵌套,而不会影响性能。布局主要包括:布局基础知识、Material组件和布局、自定义布局、Co

m0_71524094的博客 1万+

Android全新UI框架之常用ComposeUI组件

在Compose中,每个组件都是一个带有@Composable注解的函数,被称为Composable。Compose已经预置了很多基于MD设计规范的Composable组件。在布局方面,Compose提供了Column、Row、Box三种布局组件(感觉跟flutter差不多),类似于传统视图开发中的LinearLayout(Vertical)、LinearLayout(Horizontal)、RelateiveLayout,可以满足各类产品的常见布局需求。

qq_36828822的博客 4122

Compose 是如何将数据转换成 UI 的?

Compose 是一个声明式的 UI 框架,提供了很多开箱即用的组件,比如 Text()、Button、Image() 等等,Compose 会经过几个不同的阶段,最终渲染出 UI 界面。

Fucking Android & Compose & Harmony & Spring Boot ... 3793

Compose:1.4 自定义 Composable

为什么要加 @Composable 注解才能用 Compose 的控件?在编译过程中这些组件会被 Compose 的编译器插件(Compiler Plugin)添加修改一些参数,调用的时候也是使用被修改参数后的函数。添加的参数准确的说是一个叫 Composer 类型的参数,加上它主要是有用到,而且通过注解的方式在编译时就加上,开发者也能很方便不用手动加上。这种方式就是典型的面向切面编程(AOP)。在我们熟知的 AOP 方案主要有两种:Annotation Processor 注解处理器修改字节码比如 ASM

Vincent的博客 908

【Android-JetpackCompose】4、可组合项的生命周期、@Composable 修饰符、附带效应

不过,在某些情况下,您可能希望在效应中捕获某个值,但如果该值发生变化,您不希望效应重启。Compose 编译器,用调用点标识,各可组合项。此外,更新 derivedStateOf 生成的状态不会导致可组合项在声明它的位置重组,Compose 仅会对返回状态为已读的可组合项(在本例中,指 LazyColumn 中的可组合项)进行重组。如果列表底部新增了一个 movie,Compose 可以重复使用组合中既有的实例,因为这些实例在列表中的位置没有发生变化,因此这些实例的 movie 输入是相同的。

呆呆的猫的博客 1635

Compose原理 - 整体架构与主流程

在重组过程中,如果遇到某个Composable读取了某个State,就会把该State存储到Composable对应的RecomposerScope内,并将该scope与state的对应关系存储在。换言之,父快照的值对子快照是可见的,子快照对父快照是不可见的,一直到子快照apply()。缓存,随后也会将本composition作为invalid加入到Recomposer的compositionInvalidations缓存。快照,同时,快照也是ThreadLocal的,即是线程隔离的。

temp7695的博客 2362
上一篇: Kotlin Jetpack 实战:01. Kotlin 基础
下一篇: 《将博客搬至优快云》
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值