Android笔记(十四):JetPack Compose中附带效应(一)

news2024/7/4 4:52:24

在Android应用中可以通过定义可组合函数来搭建应用界面。应用界面的更新往往是与可组合函数内部定义的状态值相关联的。当界面的状态值发生变更,会导致应用界面进行更新。在Android笔记(九):Compose组件的状态,对Compose组件的状态进行详细地介绍。理想状态下,可组合函数使用是定义范围内的状态值,通过内部状态的变更,修改可组合项构成的界面。但是,在有些特殊场景下,需要在可组合项中运行一些在可组合函数作用域外的一些应用状态。简单地说,附带效应就是在可组合函数中一些可组合函数作用域外的应用状态的变化。这些效应在可组合函数范围外,对于可组合函数来说并不好控制,容易造成过度使用。因此需要结合Effect API来对这些效应进行可预测地控制和处理。

一、什么是附带效应

在前述中对附带效应进行简单说明,在下面需要通过应用实例来解释:
运行下列代码块:

//全局变量,在MainScreen作用域外
val timer = mutableIntStateOf(0)

@Composable
fun MainScreen(){
    //函数作用域内
    var runningState = remember{mutableStateOf(false)}
    val scope = rememberCoroutineScope()

    Box(modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center){
        Column(modifier = Modifier.fillMaxWidth(),
               horizontalAlignment = Alignment.CenterHorizontally,
               verticalArrangement = Arrangement.Center){
            Text(text = "计时器",fontSize=30.sp,color=MaterialTheme.colorScheme.primary)
            Text(text = "${timer.value}秒",fontSize = 24.sp)
            Row(modifier = Modifier.fillMaxWidth(),
                horizontalArrangement = Arrangement.Center){
                IconButton(modifier =  Modifier.width(100.dp),
                    onClick={
                        runningState.value = true
                        scope.launch {
                            while(runningState.value){
                                delay(1000)
                                timer.value +=1
                                Log.d("TAG","${timer} m")
                            }
                        }
                    }){
                    Row{
                        Icon(imageVector = Icons.Filled.Start,tint = Color.Green,contentDescription = null)
                        Text("计时")
                    }
                }
                IconButton(modifier = Modifier.width(100.dp),
                    onClick={
                        runningState.value  = false
                    }){
                    Row{
                        Icon(imageVector = Icons.Filled.Stop,
                            tint = Color.Green,contentDescription = null)
                        Text("计时")
                    }
                }
            }
        }
    }
}

在上述代码中,有几点需要注意:

1.rememberCoroutineScope函数
rememberCoroutineScope函数可以获取感知作用域,在可组合项外启动协程。在上述代码中定义的IconButton中的点击事件onClick的,处理timerState值的变化。因为下列的代码段:

while(runningState.value){
delay(1000)
timer.value +=1
Log.d(“TAG”,“${timer} m”)
}

delay函数是一个挂起函数,表示地是在不阻塞线程的情况下将协程延迟给定时间(例如代码中设置的1秒),并在指定时间后恢复。因此需要在协程的范围中调用delay这个挂起函数,因此利用rememberCoroutineScope函数获得一个CorountineScope协程范围,在该范围可以对挂起函数进行调用。

val scope = rememberCoroutineScope()
while(runningState.value){
delay(1000)
timer.value +=1
Log.d(“TAG”,“${timer} m”)
}

2.关于外部量的变化
定义了状态值timerState和MainScreen()这个可组合函数。timerState这个可变的状态值是一个全局量,在MainScreen()函数的作用域外。需要将timerState的值进行修改,会发生在两种情况:

(1)MainScreen()可组合函数调用时;
(2)MainScreen()函数进行重组时会调用timerState

而在第二种情况中,可组合函数重组时所触发timerState状态值的变化而变化的情况就是发生了附带效应。但是,上述代码定义的timerState值的变化是不可预知的,因为它的作用域在MainScreen函数外,外界也有可能存在促使timerState值变化的情况。因此需要一些方法对timerState值的变更情况做出预测并做出相应的处理。
在这里插入图片描述
图1 附带效应示意

二、LaunchedEffect

LaunchedEffect函数可以在某个可组合项的作用域内运行挂起函数时,它会启动内部的代码块到协程上下文CoroutineContext中。当函数的key1的值发生变化,会重构LaunchedEffect。这时,LaunchedEffect原来启动的协程会被取消然后又重新启动。当LaunchedEffect退出组合项时,协程会被取消。LaunchedEffect可组合函数定义如下:

@Composable @NonRestartableComposable
@OptIn(InternalComposeApi::class) fun LaunchedEffect(
key1: Any?,
block: suspend CoroutineScope.() -> Unit ) {
val applyContext = currentComposer.applyCoroutineContext
remember(key1) { LaunchedEffectImpl(applyContext, block)
}
}

其中,LaunchedEffect函数中的参数:

key1:表示关键字可以是任何类型,如果是可变的状态值时,可以根据可变状态值的变化,取消原有协程并启动新的协程。 当key1为Unit或true时,LaunchedEffect函数将与当前重组函数保持一致的生命周期。
block:表示要调用的挂起函数。需要在协程范围中运行;

修改上述计时器的代码段,代码如下所示:

//全局变量,在MainScreen作用域外
val timer = mutableIntStateOf(0)

@Composable
fun MainScreen(){
    //函数作用域内
    var runningState = remember{mutableStateOf(false)}
    val scope = rememberCoroutineScope()
    LaunchedEffect(key1 = timer.value){ //一旦发生修改,不断取消原有协程,创建新的协程,
        scope.launch {
                delay(1000)
                timer.value +=1 //会使得LaunchedEffect不断重构
        }
    }
    Box(modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center){
        Column(modifier = Modifier.fillMaxWidth(),
               horizontalAlignment = Alignment.CenterHorizontally,
               verticalArrangement = Arrangement.Center){
            Text(text = "计时器",fontSize=30.sp,color=MaterialTheme.colorScheme.primary)
            Text(text = "${timer.value}秒",fontSize = 24.sp)
            Row(modifier = Modifier.fillMaxWidth(),
                horizontalArrangement = Arrangement.Center){
                IconButton(modifier =  Modifier.width(100.dp),
                    onClick={
                        runningState.value = true
                        timer.value +=1 //点击按钮控制第一次值的修改
                    }){
                    Row{
                        Icon(imageVector = Icons.Filled.Start,tint = Color.Green,contentDescription = null)
                        Text("计时")
                    }
                }
                IconButton(modifier = Modifier.width(100.dp),
                    onClick={
                        runningState.value  = false
                        timer.value +=1
                    }){
                    Row{
                        Icon(imageVector = Icons.Filled.Stop,
                            tint = Color.Green,contentDescription = null)
                        Text("停止")
                    }
                }
            }
        }
    }
}

在上述代码中可以发现,当点击计时按钮时,修改了LaunchedEffect函数中的键参数timer.value的值,使得,LaunchedEffect函数timer.value的值的变化,使得原有协程取消,启动新的协程,并在新的协程中修改了timer.value的值,LaunchedEffect函数的不断重构,使得状态值不断修改,导致计时界面MainScreen不断重构和刷新。通过这样的方式,达到对timer.value修改的可预测控制。但是这种不断切换协程的方式并不可取,可以修改代码成如下的形式:

//全局变量,在MainScreen作用域外
val timer = mutableIntStateOf(0)

@Composable
fun MainScreen(){
    //函数作用域内
    var runningState = remember{mutableStateOf(false)}
    val scope = rememberCoroutineScope()
    LaunchedEffect(key1 = runningState.value){
        scope.launch {
            while(runningState.value){
                delay(1000)
                timer.value +=1
                Log.d("TAG","${timer} m")
            }
        }
    }
    Box(modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center){
        Column(modifier = Modifier.fillMaxWidth(),
               horizontalAlignment = Alignment.CenterHorizontally,
               verticalArrangement = Arrangement.Center){
            Text(text = "计时器",fontSize=30.sp,color=MaterialTheme.colorScheme.primary)
            Text(text = "${timer.value}秒",fontSize = 24.sp)
            Row(modifier = Modifier.fillMaxWidth(),
                horizontalArrangement = Arrangement.Center){
                IconButton(modifier =  Modifier.width(100.dp),
                    onClick={
                        runningState.value = true
                    }){
                    Row{
                        Icon(imageVector = Icons.Filled.Start,tint = Color.Green,contentDescription = null)
                        Text("计时")
                    }
                }
                IconButton(modifier = Modifier.width(100.dp),
                    onClick={
                        runningState.value  = false
                    }){
                    Row{
                        Icon(imageVector = Icons.Filled.Stop,
                            tint = Color.Green,contentDescription = null)
                        Text("停止")
                    }
                }
            }
        }
    }
}

运行效果是:
在这里插入图片描述
在上述代码段中,按钮的点击动作只是修改了可组合函数内部的runningState状态值。但是,

LaunchedEffect(key1 = runningState.value){
scope.launch {
while(runningState.value){
delay(1000)
timer.value +=1
Log.d(“TAG”,“${timer} m”)
}
}
}

如果点击按钮,会修改键参数runingState.value的值。而在上述代码的LaunchedEffect函数中监控到runningState.value值的变化会将原来的协程取消,然后再重新启动新的协程,在新的协程中修改MainScreen可组合函数外部的timerState状态值,从而达到计时的功能实现。在这里,是通过控制runningState来达到修改外部timerState的目的。如果runningState.value值没有发生任何变化,那么原有的协程不会取消,会继续运行。如果runningState.value的值为false,那么界面将不会发生变化。通过这样的方式,LaunchedEffect监控key1的变化,使得对timerState.value修改可以控制。

三、rememberUpdatedState

在上述代码中,LaunchedEffect函数会根据关键字的值的变化,重启协程。但是,在某些情况下,并不希望LaunchedEffect重启,但是却需要LaunchedEffect函数中变更的状态的值。因此,可以考虑使用rememberUpdatedState函数用于创建可捕获和更新该值的引用。但是注意这样的处理方式代价会比较高昂。

//全局变量,在MainScreen作用域外
val timer = mutableIntStateOf(0)

@Composable
fun MainScreen(){
    //函数作用域内
    var runningState = remember{mutableStateOf(true)}
    val scope = rememberCoroutineScope()
    val timerState = rememberUpdatedState(newValue = timer)

    LaunchedEffect(Unit){
        scope.launch {
            while(runningState.value){
                delay(1000)
                timerState.value.value +=1
                Log.d("TAG","${timer.value} m")
            }
        }
    }
    Box(modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center){
        Column(modifier = Modifier.fillMaxWidth(),
               horizontalAlignment = Alignment.CenterHorizontally,
               verticalArrangement = Arrangement.Center){
            Text(text = "计时器",fontSize=30.sp,color=MaterialTheme.colorScheme.primary)
            Text(text = "${timerState.value.value}秒",fontSize = 24.sp)
            Row(modifier = Modifier.fillMaxWidth(),
                horizontalArrangement = Arrangement.Center){
                IconButton(modifier =  Modifier.width(100.dp),
                    onClick={
                        runningState.value = true
                    }){
                    Row{
                        Icon(imageVector = Icons.Filled.Start,tint = Color.Green,contentDescription = null)
                        Text("计时")
                    }
                }
                IconButton(modifier = Modifier.width(100.dp),
                    onClick={
                        runningState.value  = false
                        timer.value +=1
                    }){
                    Row{
                        Icon(imageVector = Icons.Filled.Stop,
                            tint = Color.Green,contentDescription = null)
                        Text("停止")
                    }
                }
            }
        }
    }
}

在上述代码中,MainScreen可组合函数内部定义:

val timerState = rememberUpdatedState(newValue = timer)
而在LaunchedEffect代码部分定义为:
LaunchedEffect(Unit){
scope.launch {
while(runningState.value){
delay(1000)
timerState.value.value +=1
Log.d(“TAG”,“${timer.value} m”)
}
}
}
LaunchedEffect函数中的键参数为Unit,这表示在MainScreen函数被调用时或重新组合时,才会加载LaunchedEffect函数。并不存在键参数值的变化重新加载协程代码的可能。但是,通过rememberUpdatedState(newValue = timer)函数,一致可以通过timerState.value来获取变化的状态timer.
因此,在上述代码中,由于runningState.value初始值为true,因此一启动MainScreen,就会显示显示动态计时的效果。但是,当点击停止按钮runningState.value的值设置为false,导致代码段停止运行。然后再点击"计时“按钮,可以发现,LaunchedEffect函数并没有重启。

四、DisposableEffect清理效应

对于需要在键发生变化或可组合项退出组合后进行清理的附带效应,可以通过DisposableEffect来实现。DisposableEffect是组合的附效应。DisposableEffect函数定义如下:

@Composable
@NonRestartableComposable
fun DisposableEffect(
key1: Any?,
effect: DisposableEffectScope.() -> DisposableEffectResult ) {
remember(key1) {
DisposableEffectImpl(effect)
}
}
当key1是键参数,可以为任何值。当key1的值发生变化或者DisposableEffect离开可组合项,则必须执行撤销或清除操作。

//全局变量,在MainScreen作用域外
val timer = mutableIntStateOf(0)

@Composable
fun MainScreen(){
    //函数作用域内
    var runningState = remember{mutableStateOf(true)}
    val scope = rememberCoroutineScope()

    LaunchedEffect(Unit){
        scope.launch {
            while(runningState.value){
                delay(1000)
                timer.value+=1
                Log.d("TAG","${timer.value} m")
            }
        }
    }

    DisposableEffect(key1 = timer.value){
        onDispose {
            timer.value = 0
            runningState.value = false
        }
    }

    Box(modifier = Modifier.fillMaxSize(),
        contentAlignment = Alignment.Center){
        Column(modifier = Modifier.fillMaxWidth(),
               horizontalAlignment = Alignment.CenterHorizontally,
               verticalArrangement = Arrangement.Center){
            Text(text = "计时器",fontSize=30.sp,color=MaterialTheme.colorScheme.primary)
            Text(text = "${timer.value}秒",fontSize = 24.sp)
            Row(modifier = Modifier.fillMaxWidth(),
                horizontalArrangement = Arrangement.Center){
                IconButton(modifier =  Modifier.width(100.dp),
                    onClick={
                        runningState.value = true
                    }){
                    Row{
                        Icon(imageVector = Icons.Filled.Start,tint = Color.Green,contentDescription = null)
                        Text("计时")
                    }
                }
                IconButton(modifier = Modifier.width(100.dp),
                    onClick={
                        runningState.value  = false
                        timer.value +=1
                    }){
                    Row{
                        Icon(imageVector = Icons.Filled.Stop,
                            tint = Color.Green,contentDescription = null)
                        Text("停止")
                    }
                }
            }
        }
    }
}

上述代码的界面没有任何变化,这是因为下列代码

LaunchedEffect(Unit){
scope.launch {
while(runningState.value){
delay(1000)
timer.value+=1
Log.d(“TAG”,“${timer.value} m”)
}
}
}

DisposableEffect(key1 = timer.value){
    onDispose {
        timer.value = 0
        runningState.value = false
    }
}

启动MainScreen时,timer.value修改为1,但是DisposableEffect发现到timer.value变更,就会执行清除操作,修改timer.value=0和runningState.value=false。在后续中将无法动态显示计时效果。DisposableEffect往往和生命周期进行关联,当键参数为生命周期拥有者对象时,可以利用DisposableEffect对生命周期拥有者的变化执行撤销或清理的工作。
在下面定义两个活动MainActivity和OtherActivity,这两个活动分别为不同的生命周期拥有者,具有不同的生命周期对象。具体定义如下:
(1)MainActivity的定义
首先定义依附MainActivity的界面HomeScreen的定义

@Composable
fun HomeScreen(lifecycleOwner:
               LifecycleOwner = LocalLifecycleOwner.current,
               onStart:(MutableState<Int>)->Unit,
               onStop:(MutableState<Int>)->Unit){
    val startAction by rememberUpdatedState(newValue = onStart)
    val stopAction by rememberUpdatedState(newValue = onStop)
    val context  = LocalContext.current

    var timer = remember{mutableIntStateOf(0)}
    //以键参数为生命周期拥有者lifecycleOwner
    DisposableEffect(key1 = lifecycleOwner){
        //定义生命周期观察者
        val observer  = LifecycleEventObserver{_,event->
            if(event == Lifecycle.Event.ON_RESUME){
                    startAction(timer)
            }else if(event == Lifecycle.Event.ON_STOP){
                    stopAction(timer)
            }
        }
        //生命周期拥有者lifecycleOwner的生命周期lifecycle加入新的观察者observer,观察者可以观察生命周期的变化
        lifecycleOwner.lifecycle.addObserver(observer)

        onDispose {
            //当离开可组合项执行清理处理
            Log.d("TAG","清理完毕")
            timer.value  = 0
            lifecycleOwner.lifecycle.removeObserver(observer)
        }
    }

    Box(contentAlignment= Alignment.Center,
        modifier = Modifier.fillMaxSize()){
        Column{
            Text(text = "${timer.value}秒",fontSize=30.sp)
            Button(onClick = {
                //跳转到其他活动,即修改了生命周期拥有者对象
                val intent = Intent(context, OtherActivity::class.java)
                context.startActivity(intent)
            }){
                Text("跳转到其他活动")
            }
        }
    }
}

再定义主活动MainActivity

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            Ch06_DemoTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    HomeScreen(onStart = ::onStartCall, onStop = ::onStopCall )
                }
            }
        }
    }

    fun onStartCall(timer: MutableState<Int>){
        Log.d("TAG","startCall")
        thread{
            while(timer.value<100){
                timer.value++
                Thread.sleep(1000)
            }
        }
    }

    fun onStopCall(timer: MutableState<Int>){
        //start停止
        Log.d("TAG","stopCall")
    }
}

(2)OtherActivity的定义

class OtherActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent{
            Box(contentAlignment = Alignment.Center,
                modifier = Modifier.wrapContentSize()){
                Text("OtherActivity",fontSize = 30.sp)
            }
        }
    }
}

运行情况说明:
(1)点击运行的效果如下图所示:
在这里插入图片描述
(2)当点击”跳转其他活动“按钮,跳转到OtherActivity的界面,跟踪的日志如下:
日志显示
在这里插入图片描述

(3)当彻底退出关闭这个应用,则在日志中显示:
在这里插入图片描述
这是因为,在跳转到OtherActivity时,LifecycleOwner虽然已经切换到OtherActivity,但是界面HomeScreen已经在后台,因此并没有执行onDispose的代码块。只有在彻底退出应用,彻底离开可组合函数HomeScreen,可组合函数HomeScreen中的DisposableEffect中监测到代码生命周期拥有者的对象发生了变化,因此执行onDispose代码块。

五、SideEffect

可组合函数进行重组时并不是每次都成功,如果出现界面状态值发生变化,但是界面会进行重组。但是,在重组的过程中,一些状态数据又发生了变化,导致上次重组没有完全完成。这就使得一些与界面重组无关的数据和代码也会被多次调用,这种情况显然是没有必要的。在这样的前提下,可以使用SideEfffect。

SideEffect表示“副作用”,它将 Compose 的状态发布为非 Compose 代码。如需与非 Compose 管理的对象共享 Compose 状态,可使用 SideEffect 中可组合项,因为只有每次成功重组时才会调用该可组合项

SideEffect函数定义如下:

@Composable
@NonRestartableComposable
@OptIn(InternalComposeApi::class)
fun SideEffect(effect: () -> Unit ) {
currentComposer.recordSideEffect(effect)
}

观察下列代码运行结果和日志:

@Preview
@Composable
fun SideScreen(){
    //可组合的状态
    var timer by remember{ mutableIntStateOf(0) }
    var running by remember{mutableStateOf(false)}

    Log.d("TAG","在SideEffect前的日志")

    SideEffect{
        Log.d("TAG","执行SideEffect函数:running:${running}")
        if(running) {
            timer++
            Thread.sleep(1000)
        }
    }

    Log.d("TAG","在SideEffect函数后的日志")

    Box(contentAlignment= Alignment.Center,
        modifier = Modifier.fillMaxSize()){
        Column{
            Text(text = "${timer}秒",fontSize=30.sp)
            Row(horizontalArrangement = Arrangement.Center){
                Button(onClick = {
                    running  = true
                    Log.d("TAG","running:${running}")
                }){
                    Text("计时")
                }

                Button(onClick = {
                    running = false
                    timer = 0
                    Log.d("TAG","running:${running}")
                }){
                    Text("停止")
                }
            }
        }
    }
}

上述代码的运行结果如下所示:
在这里插入图片描述
并没有发生动态更新计时时间的显示。这是因为,running的值为false,在条件判断时,使得timer状态值无法发生变化,状态值没有变化,使得界面只会在调用时刷新界面。即使点击”计时“按钮,修改了running的值为true,因为影响界面重组的状态值timer并没有变化,因此界面没有发生重组。这与日志显示是一致的。下图显示的日志就是这样的效果。
在这里插入图片描述
观察日志,可以发现调用可组合函数SideScreen时,重组成功后,才会调用SideEffect函数。因为日志输出SideEffect函数内部的Lambda代码段中的日志是最后调用的。
再来观察下列代码
(1)定义数据类Timer

/**
 * counter:Int记录已经计时的时间
 * timerInterval:Int时间间隔
 * /
data class Timer(var counter:Int,val timeInterval:Int=1000)

(2)定义将Compose状态返回非Compose的值

@Composable
fun rememberTimer(counter:Int):Timer{
    //定义可组合的对象
    val timer = remember{Timer(0)}

    SideEffect{
        timer.counter = counter
    }

    return timer
}

在上述的可组合函数rememberTimer中生成并返回了一个非Compose的Timer对象

(3)在界面中进行测试

@Preview
@Composable
fun SideScreen(){
    //非状态的对象
    var timer = rememberTimer(counter = 0)
    val scope = rememberCoroutineScope()
    var running by remember{mutableStateOf(true)}

    Box(contentAlignment= Alignment.Center,
        modifier = Modifier.fillMaxSize()){
        Column{
            Text(text = "${timer.counter}秒",fontSize=30.sp)
            Row(horizontalArrangement = Arrangement.Center){
                Button(onClick = {
                    running  = true
                    scope.launch {
                        while(running){
                            delay(1000)
                            timer.counter++
                             Log.d("TAG",""+timer.counter)

                        }
                    }
                    Log.d("TAG","running:${running}")
                }){
                    Text("计时")
                }

                Button(onClick = {
                    running = false
                    timer.counter = 0
                    Log.d("TAG","running:${running}")
                }){
                    Text("停止")
                }
            }
        }
    }
}

运行效果如上图一致。
执行点击”计时“按钮,即使running的值为true,界面不会重组。这是因为影响界面的timer已经是一个非Compose的对象,并不能对界面的重组起到作用。观察日志,可以发现timer.counter值的确每秒进行变更了。
在这里插入图片描述

参考文献

1.Compose 中的附带效应
https://developer.android.google.cn/jetpack/compose/side-effects?hl=zh-cn

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1246203.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

定制嵌入式主板:满足客户的实际需求

随着物联网、智能家居、智能工厂等应用的不断发展&#xff0c;嵌入式系统的应用场景也越来越广泛。嵌入式系统的核心部分是嵌入式主板&#xff0c;而定制化的嵌入式主板已经成为了很多企业和项目的首选。本文将从以下几个方面介绍定制嵌入式主板。 一、定制嵌入式主板的概念 定…

华为云人工智能入门级开发者认证学习笔记

人工智能入门级开发者认证 人工智能定义 定义 人工智能 (Artificial Intelligence) 是研究、延伸和扩展人的智能的理论、方法、技术及应用系统的一门新的技术科学。 强人工智能 vs 弱人工智能 强人工智能&#xff1a;强人工智能观点认为有可能制造出真正能推理&#xff08…

星河创新,开拓新纪!2023“星河产业应用创新奖”报名全面开启!

科技的浪潮汹涌而至&#xff0c;人工智能正悄无声息地渗透进我们生活的每一个角落&#xff0c;成为推动社会奔腾向前的强大引擎。 随着大模型时代到来&#xff0c;更多的创新者涌现出来&#xff0c;他们正积极探索AI与实体的深度融合&#xff0c;解决行业难题&#xff0c;开拓…

实时语音克隆:5 秒内生成任意文本的语音 | 开源日报 No.84

CorentinJ/Real-Time-Voice-Cloning Stars: 43.3k License: NOASSERTION 这个开源项目是一个实时语音克隆工具&#xff0c;可以在5秒内复制一种声音&#xff0c;并生成任意文本的语音。 该项目的主要功能包括&#xff1a; 从几秒钟的录音中创建声纹模型根据给定文本使用参考…

Talk | 牛津大学博士后研究员边佳旺:SC-DepthV3-动态场景中的自监督单目深度估计

本期为TechBeat人工智能社区第550期线上Talk。 北京时间11月23日(周四)20:00&#xff0c;牛津大学博士后研究员—边佳旺的Talk已准时在TechBeat人工智能社区开播&#xff01; 他与大家分享的主题是: “SC-DepthV3&#xff1a;动态场景中的自监督单目深度估计”&#xff0c;介绍…

前置微小信号放大器在生物医学中有哪些应用

前置微小信号放大器在生物医学领域中具有广泛的应用。生物医学信号通常具有较小的振幅和较低的幅频响应&#xff0c;因此需要借助放大器来增强信号以便进行准确的测量、监测和分析。以下是前置微小信号放大器在生物医学中的主要应用。 心电图&#xff08;ECG&#xff09;放大器…

window.requestAnimationFrame+localStorage+canvas实现跨窗口小球连线效果

文章目录 前言效果代码后言 前言 hello world欢迎来到前端的新世界 &#x1f61c;当前文章系列专栏&#xff1a;前端系列文章 &#x1f431;‍&#x1f453;博主在前端领域还有很多知识和技术需要掌握&#xff0c;正在不断努力填补技术短板。(如果出现错误&#xff0c;感谢大家…

Golang并发模型:Goroutine 与 Channel 初探

文章目录 goroutinegoexit() channel缓冲closerangeselect goroutine goroutine 是 Go 语言中的一种轻量级线程&#xff08;lightweight thread&#xff09;&#xff0c;由 Go 运行时环境管理。与传统的线程相比&#xff0c;goroutine 的创建和销毁的开销很小&#xff0c;可以…

kali部署ARL灯塔资产系统及使用教程

网上有很多ARL部署到centos系统的教程,但是部署到ubuntu或kali linux系统的教程都是乱七八糟,互相抄,而且没有一个能部署成功,鉴于此,写下此教程,帮助大家出坑 一、安装docker环境(网上什么弄钥匙呀,什么稳定源啊都是垃圾) 准备一个纯净的最新的kali linux系统 1、配…

关于python 语音转字幕,字幕转语音大杂烩

文字转语音 Python语音合成之第三方库gTTs/pyttsx3/speech横评(内附使用方法)_python_脚本之家 代码示例 from gtts import gTTStts gTTS(你好你在哪儿&#xff01;,langzh-CN)tts.save(hello.mp3)import pyttsx3engine pyttsx3.init() #创建对象"""语速"…

docker安装nacos,实现和mysql容器的通信

1.下载nacos镜像 docker pull nacos/nacos-server2. 启动nacos 启动命令如下&#xff1a; docker run -d -p 8848:8848 --name nacos \ -e JVM_XMS256m \ -e JVM_XMX256m \ -e MODEstandalone \ -e SPRING_DATASOURCE_PLATFORMmysql \ -e MYSQL_SERVICE_HOST192.168.131.223…

抖音本地生活服务商申请入口关闭?聚合服务商将成本地生活新模式

近年来&#xff0c;随着抖音本地生活服务为用户提供了便捷的生活方式相继支付宝、微信陆续推出了本地生活服务。然而&#xff0c;对于许多创业者而言&#xff0c;申请成为抖音本地生活服务商却面临着一定的门槛。因此&#xff0c;如何降低这些门槛&#xff0c;让更多的商家能够…

vue+SpringBoot的图片上传

前端VUE的代码实现 直接粘贴过来element-UI的组件实现 <el-uploadclass"avatar-uploader"action"/uploadAvatar" //这个action的值是服务端的路径&#xff0c;其他不用改:show-file-list"false":on-success"handleAvatarSuccess"…

【数据资产入表培训】推进数据资产入表,助力广西数字经济高质量发展

为了贯彻执行《中共中央国务院关于构建数据基础制度更好发挥数据要素作用的意见》&#xff0c;深入理解《企业数据资源相关会计处理暂行规定》等政策&#xff0c;提升对数据资产入表重要理论意义和实践价值的认识&#xff0c;引导企业构建数据资产化机制&#xff0c;推动数字经…

Qt5.15编译工程报APK 的 API 级别设定低于套件所需的最低要求

APK 的 API 级别设定低于套件所需的最低要求。 套件所需的最低 API 级别是 21。 Error while building/deploying project qtpdfium (kit: 安卓 Qt 5.15.2 Clang Multi-Abi) When executing step "构建安卓 APK" 修改xml 工程中是16 修改为21 重新编译&#xff0c;问…

下一代ETL工具:微服务架构的全新数据集成平台

当前对于大型企业来说数据的整合和加工变得越来越重要。随着业务需求的不断增长&#xff0c;企业数据量越来越大&#xff0c;数据管道越来越多&#xff0c;现有的ETL&#xff08;抽取、转换、加载&#xff09;工具已不再满足实时、高性能和微服务架构等现代化需求。因此&#x…

TensorFlow实战教程(二十五)-基于BiLSTM-CRF的医学命名实体识别研究(下)模型构建

这篇文章写得很冗余,但是我相信你如果真的看完,并且按照我的代码和逻辑进行分析,对您以后的数据预处理和命名实体识别都有帮助,只有真正对这些复杂的文本进行NLP处理后,您才能适应更多的真实环境,坚持!毕竟我写的时候也看了20多小时的视频,又写了20多个小时,别抱怨,加…

产品经理面试必看!To B和To C产品的隐秘差异,你了解多少?

大家好&#xff0c;我是小米&#xff0c;一位对技术充满热情的产品经理。最近在和小伙伴们交流中发现一个热门话题&#xff1a;To B&#xff08;面向企业&#xff09;和To C&#xff08;面向消费者&#xff09;的产品经理究竟有何异同&#xff1f;这可是我们产品经理面试中的经…

如何挑选最适合的APP开发公司

随着科技的不断发展&#xff0c;app开发公司如雨后春笋般涌现&#xff0c;让人眼花缭乱。如何挑选最合适的app开发公司&#xff0c;成为了很多项目负责人的难题。本文将为你提供挑选app开发公司的三大秘籍&#xff0c;让你轻松找到最合适的合作伙伴&#xff0c;让你的项目飞起来…

从裸机启动开始运行一个C++程序(十三)

前序文章请看&#xff1a; 从裸机启动开始运行一个C程序&#xff08;十二&#xff09; 从裸机启动开始运行一个C程序&#xff08;十一&#xff09; 从裸机启动开始运行一个C程序&#xff08;十&#xff09; 从裸机启动开始运行一个C程序&#xff08;九&#xff09; 从裸机启动开…