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

在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.mfbz.cn/a/184343.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

数据库实验五 数据库设计

数据库实验五 数据库设计 一、实验目的二、实验内容三、实验内容四、验证性实验五、设计性实验 一、实验目的 1.了解E-R图构成要素以及各要素图元。 2.掌握概念模型E-R图的绘制方法。 3.掌握概念模型向逻辑模型的转换原则和步骤。 4.运用sql编程实现 二、实验内容 1.选取一个…

TCP 重传、滑动窗口、流量控制、拥塞控制的剖析

TCP 是一个可靠传输的协议&#xff0c;那它是如何保证可靠的呢&#xff1f; 为了实现可靠性传输&#xff0c;需要考虑很多事情&#xff0c;例如数据的破坏、丢包、重复以及分片顺序混乱等问题。如不能解决这些问题&#xff0c;也就无从谈起可靠传输。 那么&#xff0c;TCP 是…

使用骨传导耳机会伤耳朵吗?一文读懂骨传导耳机有哪些优点

首先说明&#xff0c;如果是正确的使用骨传导耳机是不会伤耳朵。 一、骨传导耳机的传声原理是什么&#xff1f; 声音的传播需要介质&#xff0c;传统的耳机是通过空气来进行传播&#xff0c;也被称为“空气传导耳机”&#xff0c;而骨传导耳机最大的特别之处就在于&#xff0…

linux安装zsh、oh-my-zsh及常用插件

大家好&#xff0c;我叫徐锦桐&#xff0c;个人博客地址为www.xujintong.com&#xff0c;github地址为https://github.com/xjintong。平时记录一下学习计算机过程中获取的知识&#xff0c;还有日常折腾的经验&#xff0c;欢迎大家访问。 一、安装zsh 这个不用多说了&#xff0…

[HOW TO]-VirtualBox的虚拟机通过宿主机上网

快速链接: . &#x1f449;&#x1f449;&#x1f449; [专栏目录]-环境搭建安装问题笔记目录 &#x1f448;&#x1f448;&#x1f448; 付费专栏-付费课程 【购买须知】:&#x1f449;&#x1f449;&#x1f449; 个人博客笔记导读目录(全部) &#x1f448;&#x1f448;&a…

uni-app,nvue中text标签文本超出宽度不换行问题解决

复现&#xff1a;思路&#xff1a; 将text标签换为rich-text&#xff0c;并给rich-text增加换行的样式class类名解决&#xff1a;

nf_conntrack内核模块常见问题

nf_conntrack内核模块常见问题 问题描述排查步骤前置条件&#xff1a;启用nf_conntrack内核模块检查nf_conntrack配置 解决办法1:半数减少nf_conntrack buckets的值解决办法2:加倍调大m.min_free_kbytes值解决办法3:Linux社区权威答复-忽略告警 问题描述 内核报错 falling bac…

【C++初阶】四、类和对象(构造函数、析构函数、拷贝构造函数、赋值运算符重载函数)

相关代码gitee自取&#xff1a; C语言学习日记: 加油努力 (gitee.com) 接上期&#xff1a; 【C初阶】三、类和对象 &#xff08;面向过程、class类、类的访问限定符和封装、类的实例化、类对象模型、this指针&#xff09; -CSDN博客 引入&#xff1a;类的六个默认成员函数…

Web(6)Metasploit缓冲区溢出漏洞

利用ms17_010漏洞建立会话&#xff1a; 原理&#xff1a; 因为window低级版本中存在漏洞&#xff0c;端口445.SMB是一个协议名&#xff0c;全称是Server Message Block&#xff08;服务器消息快协议&#xff09;&#xff0c;用于计算机之间共享文件&#xff0c;打印机&#x…

智驾芯片全矩阵「曝光」,这家企业的车载品牌正式官宣

随着汽车智能化加速&#xff0c;智能驾驶芯片格局逐渐清晰。 针对L0-L2&#xff0c;业内基本采用智能前视一体机方案&#xff1b;要实现高速NOA、城市NOA等更为高阶的智驾功能等&#xff0c;则基本采用域控制器方案。从前视一体机至域控&#xff0c;再逐步演进到舱驾一体、中央…

eclipse项目移到idea上部署运行

1.配置web模块 另外&#xff0c;模块这里&#xff0c;也要加上Spring 2.配置Artifact &#xff08;用于tomcat&#xff09; 就是从上面配置的web模块&#xff0c;产生的工件 3.添加lib 一般是在web-inf/lib &#xff0c; 遇到的坑&#xff1a; jdk版本问题&#xff0c;这里…

加速软件开发:自动化测试在持续集成中的重要作用!

持续集成的自动化测试 如今互联网软件的开发、测试和发布&#xff0c;已经形成了一套非常标准的流程&#xff0c;最重要的组成部分就是持续集成&#xff08;Continuous integration&#xff0c;简称CI&#xff0c;目前主要的持续集成系统是Jenkins&#xff09;。 那么什么是持…

6.显示评论 + 添加评论

1.显示评论 数据层&#xff1a;根据实体查询一页评论数据、根据实体查询评论的数量业务层&#xff1a;处理查询评论的业务、处理查询评论数量的业务表现层&#xff1a;显示帖子详情数据时&#xff0c;同时显示该帖子所有的评论数据 1.1 数据访问层 entity_type&#xff1a;实体…

linux 内存回收mglru算法代码注释2

mglru与原lru算法的兼容 旧的lru算法有active与inactive两代lru&#xff0c;可参考linux 内存回收代码注释&#xff08;未实现多代lru版本&#xff09;-CSDN博客 新的算法在引入4代lru的同时&#xff0c;还引入了tier的概念。 新旧算法的切换的实现在lru_gen_change_state&a…

机器学习探索计划——数据集划分

文章目录 导包手写数据划分函数使用sklearn内置的划分数据函数stratifyy理解举例 导包 import numpy as np from matplotlib import pyplot as plt from sklearn.datasets import make_blobs手写数据划分函数 x, y make_blobs(n_samples 300,n_features 2,centers 3,clus…

【UE5】资源(Asset)

了解UE游戏的基本构成 资源&#xff08;Asset&#xff09;: 在UE中&#xff0c;资源&#xff08;Asset&#xff09;是指游戏中使用到的各种素材&#xff0c;例如模型、纹理、材质、声音、动画、蓝图、数据表格、关卡等&#xff08;通常以uasset结尾&#xff09;&#xff0c;他…

freeswitch设置多个execute_on_media

概述 freeswitch是一款简单好用的VOIP开源软交换平台。 fs中有非常多的接口和通道变量&#xff0c;使用方式多变。 官方文档有时候也仅仅是介绍了最基本的使用方法和格式。 环境 centos&#xff1a;CentOS release 7.0 (Final)或以上版本 freeswitch&#xff1a;v1.6 G…

办公技巧:Word中插入图片、形状、文本框排版技巧

目录 一、插入图片排版技巧 二、添加形状排版技巧 三、插入“文本框”排版技巧 我们平常在制作word时候经常会遇到插入选项卡下的图片、形状和文本框这三种情况下&#xff0c;那么如何使得Word文档当中添加这三个元素的同时&#xff0c;又能保证样式美观呢&#xff0c;今天小…

Leetcode200. 岛屿数量

Every day a Leetcode 题目来源&#xff1a;200. 岛屿数量 解法1&#xff1a;深度优先搜索 设目前指针指向一个岛屿中的某一点 (i, j)&#xff0c;寻找包括此点的岛屿边界。 从 (i, j) 向此点的上下左右 (i1,j)&#xff0c;(i-1,j)&#xff0c;(i,j1)&#xff0c;(i,j-1) …

静态链表的结构设计与主要操作功能的实现(初始化,头插,尾插,判空,删除,输出,清空,销毁)

目录 一.静态链表的结构设计 二.静态链表的结构设计示意图 三.静态链表的实现 四.静态链表的总结 一.静态链表的结构设计 typedef struct SNode {int data;//数据int next;//后继指针(下标) }SNode,SLinkList[MAXSIZE]; 二.静态链表的结构设计示意图 0:有效数据链的头节点;…