Jetpack Compose 的简单 MVI 框架

news2024/11/24 15:51:59

Jetpack Compose 的简单 MVI 框架

在 Jetpack Compose 应用程序中管理状态的一种简单方法

选择正确的架构是至关重要的,因为架构变更后期代价高昂。MVP已被MVVM和MVI取代,而MVI更受欢迎。MVI通过强制实施结构化的状态管理方法,只在reducer中更新状态,并通过管道处理所有意图。相比之下,MVVM更新状态的位置更加自由,但理解和调试流程更加困难。状态管理对于应用程序的发展和扩展至关重要。

MVI简介

在MVI中有三个架构组件:

  • Model:表示应用程序或特定屏幕的状态以及生成该状态的业务逻辑。
  • View:表示用户交互的 UI。
  • Intent:这些是触发新模型的操作,可以是来自用户或外部的操作。
    需要注意的是,在MVI中状态是不可变的,并且MVI遵循单向数据流。这可以用以下图表表示:

基本流程如下:

生成初始模型,并将其推送到视图进行渲染。
用户或外部因素(例如网络加载完成)触发一个操作(即意图)。
意图在业务逻辑中进行处理,生成一个新的模型,并将其发布到视图。
循环无限重复。这种架构提供了明确的责任分离:视图负责渲染 UI,意图负责承载操作,而模型负责业务逻辑。

创建MVI框架的基本脚手架

我们将从为MVI框架创建基本脚手架开始。这里描述的解决方案是针对Android并专为Jetpack Compose应用程序量身定制的,但原则可以应用于任何移动应用或其他类型的应用。

我们将基于Android的ViewModel来构建模型,因为这个解决方案与Android框架很好地集成,并且具备生命周期感知能力,但需要注意的是,这并非必需,其他解决方案同样可行。

为了创建脚手架,我们需要以下组件:

  • 一个不可变的状态(模型),供视图观察。
  • 一个管道来推送意图(我将其称为操作,以避免与Android的Intent混淆)。
  • 一个减速器,用于根据现有状态和当前操作生成新的状态。
    由于这个解决方案专为Jetpack Compose而设计,我们将使用MutableState作为模型。对于管道,我们将使用MutableSharedFlow来提供给减速器。虽然不是必需的,但我还喜欢为状态和操作定义标记接口。让我们看看MVI脚手架的代码:
// 1
interface State

// 2
interface Action

// 3
interface Reducer<S : State, A : Action> {
    /**
     * Generates a new instance of the [State] based on the [Action]
     *
     * @param state the current [State]
     * @param action the [Action] to reduce the [State] with
     * @return the reduced [State]
     */
    fun reduce(state: S, action: A): S
}

private const val BufferSize = 64

// 4
open class MviViewModel<S : State, A : Action>(
    private val reducer: Reducer<S, A>,
    initialState: S,
) : ViewModel() {

    // 5
    private val actions = MutableSharedFlow<A>(extraBufferCapacity = BufferSize)

    // 6
    var state: S by mutableStateOf(initialState)
        private set

    init {
        // 7
        viewModelScope.launch {
            actions.collect { action ->
                state = reducer.reduce(state, action)
            }
        }
    }

    // 8
    fun dispatch(action: A) {
        val success = actions.tryEmit(action)
        if (!success) error("MVI action buffer overflow")
    }
}
  1. 我们为状态定义一个标记接口。

  2. 对于操作,我们也是如此做。

  3. 减速器将负责更新状态,它有一个单一的函数,该函数接受当前状态和操作,并生成一个新的状态。需要注意的是,减速器的reduce方法必须是纯函数——生成的状态只能依赖于输入的状态和操作,并且状态必须同步生成。

  4. MviViewModel是MVI框架的基础。 MviViewModel接收一个减速器和初始状态以启动MVI流程。

  5. 对于操作的管道,我们使用MutableSharedFlow,并设置了一个特定的缓冲容量。

  6. 状态保存在MutableState对象中,并作为只读属性公开。它在MviViewModel构造函数中初始化为提供的初始状态。

  7. 当ViewModel启动时,我们启动一个协程来收集来自MutableSharedFlow的操作,每次发出操作时,我们运行减速器并相应地更新状态。需要注意的是,对于这个简单的示例,我使用viewModelScope作为协程范围,但建议为了更好的可测试性提供一个专用的范围。本文末尾链接的完整示例展示了实现方式。

  8. 最后,我们需要一种方法将操作推送到管道中,我们使用dispatch方法来实现,它接受一个操作并将其推送到MutableSharedFlow中。如果缓冲区已满,这可能表示某种问题,因此我们选择在这种情况下抛出异常。

有了这个脚手架,我们现在可以创建一个简单的应用程序。我们将创建您选择的典型架构的示例应用程序,一个带有两个按钮的计数器,一个用于增加计数,一个用于减少计数。

基本示例应用程序
对于我们的示例应用程序,我们需要以下几个组件:

  • State
  • Actions
  • Reducer
  • ViewModel
  • UI

让我们从定义我们的状态开始。对于这个非常简单的示例,我们的状态只需保存一个属性,即当前计数值:

// 1
data class CounterState(
    // 2
    val counter: Int,
    // 3
) : State {
    companion object {
        // 4
        val initial: CounterState = CounterState(
            counter = 0,
        )
    }
}
  1. 我们使用数据类作为状态,这样我们可以利用生成的函数,比如我们将用它来从现有状态创建新状态的复制函数。

  2. 我们的状态只有一个属性,即计数器的值。

  3. 该状态继承了我们的标记接口。

  4. 最后,我们提供一个默认值,作为起点使用。

接下来,我们将定义操作。对于这个示例,我们只有两个操作,一个用于增加状态,一个用于减少状态:

// 1
sealed interface CounterAction : Action {
    // 2
    data object Increment : CounterAction
    // 3
    data object Decrement : CounterAction
}
  1. 我们为计数器操作使用了一个密封接口,该接口继承自我们的标记接口。

  2. 我们所需的操作不携带任何有效负载,因此我们为增加操作创建了一个数据对象。在大多数应用程序中,当需要有效负载时,我们会同时使用数据对象和数据类。

  3. 对于减少操作,我们也是同样的做法。

现在我们有了状态和操作,我们可以构建我们的减速器,它负责根据当前状态和操作生成一个新状态。让我们来看看代码:

// 1
class CounterReducer : Reducer<CounterState, CounterAction> {
    // 2
    override fun reduce(state: CounterState, action: CounterAction): CounterState {
        // 3
        return when (action) {
            CounterAction.Decrement -> state.copy(counter = state.counter - 1)
            CounterAction.Increment -> state.copy(counter = state.counter + 1)
        }
    }
}
  1. CounterReducer实现了Reducer接口。

  2. 我们重写了reduce函数,它负责生成状态。

  3. 我们对操作进行了全面的when操作,在每个操作中,根据当前状态生成一个新状态。

我们只剩下两个部分了,ViewModel和UI。让我们首先创建我们的viewModel:

class CounterViewModel(
    // 1
    reducer: CounterReducer = CounterReducer(),
    // 2
) : MviViewModel<CounterState, CounterAction>(
    reducer = reducer,
    initialState = CounterState.initial,
) {
    // 3
    fun onDecrement() {
        dispatch(CounterAction.Decrement)
    }

    // 4
    fun onIncrement() {
        dispatch(CounterAction.Increment)
    }
}
  1. CounterviewModelCounterReducer作为构造函数参数接收。在这个示例中,我们在构造函数中实例化了减速器,但在真实的应用程序中,我们将使用依赖注入。

  2. CounterviewModel继承自我们的基本MviViewModel,提供了减速器和初始状态。

  3. 我们定义了一个名为onDecrement的方法,它将向MVI管道推送减少操作。

  4. 我们对增加操作也是同样处理,定义了相应的onIncrement方法。

我们所剩下的就是UI,我会简要介绍一下,因为当涉及到MVI框架时,我们实际上如何将状态呈现到UI中的细节并不重要。这里有一个简单的UI显示计数器和两个按钮来增加/减少它:

@Composable
fun CounterScreen(
    viewModel: CounterViewModel,
    modifier: Modifier = Modifier,
) {
    CounterScreen(
        state = viewModel.state,
        onDecreaseClick = viewModel::onDecrement,
        onIncreaseClick = viewModel::onIncrement,
        modifier = modifier,
    )
}

@Composable
fun CounterScreen(
    state: CounterState,
    onDecreaseClick: () -> Unit,
    onIncreaseClick: () -> Unit,
    modifier: Modifier = Modifier,
) {
    Row(
        modifier = modifier,
        verticalAlignment = Alignment.CenterVertically,
    ) {
        IconButton(onClick = onDecreaseClick) {
            Icon(
                imageVector = Icons.Default.RemoveCircleOutline,
                contentDescription = "decrement"
            )
        }
        Text(
            text = state.counter.toString(),
            style = MaterialTheme.typography.displaySmall,
            modifier = Modifier.padding(horizontal = 16.dp),
        )
        IconButton(onClick = onIncreaseClick) {
            Icon(
                imageVector = Icons.Default.AddCircleOutline,
                contentDescription = "increment"
            )
        }
    }
}

通过这个基本的MVI框架和一个样例应用程序,我们已经搭建好了。但是我们的解决方案缺少处理异步(或长时间运行的)操作的能力,因为我们的减速器正在同步地更新状态。接下来,我们将看到如何增强我们的MVI框架以支持异步工作。

处理异步工作

为了在MVI框架中处理异步工作,我们将添加一个新概念,即Middleware。Middleware是一个组件,它被插入到MVI管道中,并可以异步执行操作。Middleware通常会在其工作开始、期间和结束时发出自己的操作(例如,如果我们有一个需要进行网络调用的操作,则Middleware可能会发出一个操作来指示网络加载已开始,可能会发出其他操作来更新网络加载中的进度指示器,并在网络加载完成时发出最终加载完成的操作)。

与其他组件一样,我们将为Middleware创建一个基类:

// 1
interface Dispatcher<A : Action> {
    fun dispatch(action: A)
}

// 2
abstract class Middleware<S : State, A : Action>() {
    // 3
    private lateinit var dispatcher: Dispatcher<A>
  
    // 4
    abstract suspend fun process(state: S, action: A)

    // 5
    protected fun dispatch(action: A) = dispatcher.dispatch(action)

    // 6
    internal fun setDispatcher(
        dispatcher: Dispatcher<A>,
    ) {
        this.dispatcher = dispatcher
    }
}
  1. Middleware需要分发自己的Actions,因此我们定义了一个Dispatcher接口(稍后我们将看到如何使用它)。

  2. Middleware类是在State和Action上进行参数化的,与减速器类似。

  3. Middleware将接收Dispacher以将操作推送到MVI管道。

  4. 挂起的process方法是异步工作将要进行的地方。

  5. 这是一个将操作推送到MVI框架的实用方法,以便我们可以将Dispatcher保持私有。

  6. 最后,我们有一个方法用于初始化Middleware中使用的Dispatcher。

接下来,让我们看看如何更新我们的MviViewModel以在MVI流程中插入Middleware:

open class MviViewModel<S : State, A : Action>(
    private val reducer: Reducer<S, A>,
    // 1
    private val middlewares: List<Middleware<S, A>> = emptyList(),
    initialState: S,
) : ViewModel(), Dispatcher<A> {

    // 2
    private data class ActionImpl<S : State, A : Action>(
        val state: S,
        val action: A,
    )

    private val actions = MutableSharedFlow<ActionImpl<S, A>>(extraBufferCapacity = BufferSize)

    var state: S by mutableStateOf(initialState)
        private set

    init {
        // 3
        middlewares.forEach { middleware -> middleware.setDispatcher(this) }
        // 4
        viewModelScope.launch {
            actions
                .onEach { actionImpl ->
                    // 5
                    middlewares.forEach { middleware ->
                        // 6
                        middleware.process(actionImpl.state, actionImpl.action)
                    }
                }
                .collect()
        }
        viewModelScope.launch {
            actions.collect {
                state = reducer.reduce(state, it.action)
            }
        }
    }

    override fun dispatch(action: A) {
        val success = actions.tryEmit(action)
        if (!success) error("MVI action buffer overflow")
    }
}
  1. 现在,MviViewModel接收一个中间件列表,默认为空列表,因为并不是所有屏幕都有异步工作。ViewModel还实现了Dispatcher接口。

  2. 我们定义了一个包装器类,它包装了当前状态和操作,我们将其推送到管道上,以便在接收操作时拥有状态的副本。

  3. 在init块中,我们循环遍历中间件,并为每个中间件设置调度器,即ViewModel本身。

  4. 接下来,我们启动一个协程来观察MutableSharedFlow的Action和State的发射。

  5. 对于每个发射,我们遍历所有中间件。

  6. 对于每个中间件,我们调用其process方法来处理操作。

这种方法的思想是,我们将拥有一组中间件,每个中间件负责应用程序的一部分业务逻辑;每个中间件将观察来自MVI管道的操作,当它负责的操作被发射时,它将启动异步操作。在一个大型应用程序中,我们可以将屏幕分成几个部分,由各自独立的中间件处理,或者我们可以根据它们执行的业务逻辑来分离中间件。思想是拥有小而精细的中间件,每个中间件只执行一个或一小组操作,而不是一个处理所有异步工作的大型中间件。

更新计数器应用程序

通过中间件和更新的MviViewModel,MVI框架已经完成,但是通过一个示例可以更容易理解,因此我们将在计数器屏幕上添加一个按钮,用于为计数器生成一个随机值。我们假设生成这个随机值是一个需要在后台线程上运行的长时间运行过程,因此我们将为此操作创建一个中间件。由于这是一个长时间运行的操作,我们将在执行工作时显示进度指示器。

我们将首先更新计数器状态,以包括加载指示器:

data class CounterState(
    // 1
    val loading: Boolean,
    val counter: Int,
) : State {
    companion object {
        val initial: CounterState = CounterState(
            // 2
            loading = false,
            counter = 0,
        )
    }
}
  1. 我们给状态添加了一个加载标志。
  2. 并将初始值更新为将该标志设置为false。
    接下来,我们需要一个新的操作来生成随机计数器值,因此我们将其添加到封闭的层次结构中。同样地,当数字准备好时,我们需要更新状态,所以我们需要另一个操作来触发更新。对于这个第二个操作,我们有一个有效载荷,即随机生成的计数器值,因此我们将使用一个数据类。最后,我们希望在后台任务运行时显示加载指示器,所以我们将添加第三个操作来显示进度指示器:
sealed interface CounterAction : Action {
    data object Loading : CounterAction
    data object Increment : CounterAction
    data object Decrement : CounterAction
    data object GenerateRandom : CounterAction
    data class CounterUpdated(val value: Int) : CounterAction
}

接下来,我们将创建中间件,负责生成随机数。当我们开始时,我们将发出一个加载操作,并在结束时发出CounterUpdated操作。我们将使用延迟来模拟长时间操作:

// 1
class CounterMiddleware : Middleware<CounterState, CounterAction>() {

    // 2
    override suspend fun process(state: CounterState, action: CounterAction) {
        // 3
        when (action) {
            CounterAction.GenerateRandom -> generateRandom()
            else -> { /* no-op */ }
        }
    }

    private suspend fun generateRandom() {
        // 4
        dispatch(CounterAction.Loading)
        // 5
        delay(500L + Random.nextLong(2000L))
        val counterValue = Random.nextInt(100)
        // 6
        dispatch(CounterAction.CounterUpdated(counterValue))
    }
}
  1. CounterMiddleware扩展了我们的中间件基类。
  2. 我们重写了负责异步工作的process方法。
  3. 我们检查操作,并只处理GenerateRandom操作。
  4. 当我们收到正确的操作时,我们发出Loading操作,这将触发状态更新以显示进度指示器。
  5. 接下来,我们开始工作,在这里通过延迟操作进行模拟。
  6. 而当工作完成时,我们通过一个新的操作发出结果。
    这就是CounterMiddleware的全部内容。接下来,我们需要更新reducer以处理我们之前定义的额外操作。reducer不必处理所有的操作,GenerateRandom操作仅在中间件处处理,因此它将不执行任何操作。让我们看一下代码:
class CounterReducer : Reducer<CounterState, CounterAction> {

    override fun reduce(state: CounterState, action: CounterAction): CounterState {
        return when (action) {
            // 1
            CounterAction.Loading -> state.copy(loading = true)
            CounterAction.Decrement -> state.copy(counter = state.counter - 1)
            CounterAction.Increment -> state.copy(counter = state.counter + 1)
            // 2
            is CounterAction.CounterUpdated -> state.copy(
                loading = false,
                counter = action.value,
            )
            // 3
            CounterAction.GenerateRandom -> state
        }
    }
}
  1. 当接收到Loading操作时,状态会更新以指示正在进行的长时间运行操作。
  2. 当接收到CounterUpdated操作时,我们清除加载标志,并使用操作有效载荷更新计数器值。
  3. GenerateRandom不在reducer中处理,因此返回现有状态。
    接下来,我们需要更新viewmodel,将中间件提供给基类,并添加一个新方法来处理生成随机数的操作。让我们看一下更新:
class CounterViewModel(
    // 1
    middleware: CounterMiddleware = CounterMiddleware(),
    reducer: CounterReducer = CounterReducer(),
) : MviViewModel<CounterState, CounterAction>(
    reducer = reducer,
    // 2
    middlewares = listOf(middleware),
    initialState = CounterState.initial,
) {
    fun onDecrement() {
        dispatch(CounterAction.Decrement)
    }

    fun onIncrement() {
        dispatch(CounterAction.Increment)
    }

    // 3
    fun onGenerateRandom() {
        dispatch(CounterAction.GenerateRandom)
    }
}
  1. 我们在构造函数中提供CounterMiddleware。像reducer一样,这通常是注入的,但为了简单起见,我们在此处实例化。
  2. 我们将中间件(在我们的情况下只有一个)提供给基类,以插入到MVI流中。
  3. 最后,我们有一个新的方法来处理生成随机计数器值。
    这基本上结束了示例。最后一步是更新UI,以提供一个触发器来生成随机数字,并在应用程序忙于长时间运行的操作时显示进度指示器。以下代码展示了一个可能的实现方式:
@Composable
fun CounterScreen(
    state: CounterState,
    onDecreaseClick: () -> Unit,
    onIncreaseClick: () -> Unit,
    onGenerateRandom: () -> Unit,
    modifier: Modifier = Modifier,
) {
    Box(
        contentAlignment = Alignment.Center,
        modifier = modifier,
    ) {
        Column(
            horizontalAlignment = Alignment.CenterHorizontally,
        ) {
            Row(
                verticalAlignment = Alignment.CenterVertically,
            ) {
                IconButton(
                    onClick = onDecreaseClick,
                    enabled = !state.loading,
                ) {
                    Icon(
                        imageVector = Icons.Default.RemoveCircleOutline,
                        contentDescription = "decrement"
                    )
                }
                Text(
                    text = state.counter.toString(),
                    style = MaterialTheme.typography.displaySmall,
                    modifier = Modifier.padding(horizontal = 16.dp),
                )
                IconButton(
                    onClick = onIncreaseClick,
                    enabled = !state.loading,
                ) {
                    Icon(
                        imageVector = Icons.Default.AddCircleOutline,
                        contentDescription = "increment"
                    )
                }
            }
            Button(
                onClick = onGenerateRandom,
                enabled = !state.loading,
                modifier = Modifier.padding(top = 16.dp),
            ) {
                Text(
                    text = "Generate Random"
                )
            }
        }
        if (state.loading) {
            CircularProgressIndicator()
        }
    }
}

下面是一个MVI架构的示例代码,给感兴趣的读者作为参考。

GitHub

https://github.com/fvilarino/Weather-Sample

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

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

相关文章

Linux 快捷键

1、快捷键小操作 1.1、ctrl c 强制停止 Linux某些程序的运行&#xff0c;如果想要强制停止它&#xff0c;可以使用快捷键ctrl c 命令输入错误&#xff0c;也可以通过快捷键ctrl c&#xff0c;退出当前输入&#xff0c;重新输入 1.2、ctrl d 退出或登出 可以通过快捷键&…

使用Mybatis generator自动生成代码,仅限Oracle数据库

一、使用Mybatis generator自动生成代码&#xff0c;仅限Oracle数据库 使用Mybatis generator自动生成代码&#xff0c;仅限Oracle数据库 一、在pom.xml文件中引入所需要的依赖和插件 <dependency><groupId>org.mybatis.generator</groupId><artifactI…

VUE日期只选择日月,表格导入功能,表格下载模版功能

1.日期选择日月&#xff1a;参考https://blog.csdn.net/Oct_Somnus/article/details/129989865?spm1001.2101.3001.6661.1&utm_mediumdistribute.pc_relevant_t0.none-task-blog-2%7Edefault%7ECTRLIST%7ERate-1-129989865-blog-116654979.235%5Ev38%5Epc_relevant_sort_b…

轻松使用androidstudio交叉编译libredwg库

对于安卓或嵌入式开发者而言,交叉编译是再熟悉不过的操作了,可是对于一些刚入门或初级开发者经常会遇到这样的问题:如何交叉编译C++库来生成安卓下的so库呢? 最近有一些粉丝找到我求救,那么我最近刚好有空大致研究了下,帮他们成功编译了其中一个libredwg的C++库,这篇文章…

Java 21 / JDK 21 (LTS) GA

Java 21 / JDK 21 已正式 GA&#xff0c;此版本是继 JDK 17 后的长期支持版本 (LTS)&#xff0c;Oracle 将为其提供至少八年的技术支持和更新。 本版本是Java SE平台21版的参考实现&#xff0c;由Java社区流程中的JSR 396指定。 正式稳定功能 JEP 444&#xff1a;虚拟线程JEP…

测试C#图像文本识别模块Tesseract的基本用法

微信公众号“dotNET跨平台”的文章《c#实现图片文体提取》&#xff08;参考文献3&#xff09;介绍了C#图像文本识别模块Tesseract&#xff0c;后者是tesseract-ocr&#xff08;参考文献2&#xff09; 的C#封装版本&#xff0c;目前版本为5.2&#xff0c;关于Tesseract的详细介绍…

windows上搭建llama小型私有模型

导言 llama官网是需要多读读的 openAI的付费&#xff0c;让学习LLM的成本不可控。为了省钱&#xff0c;搭建本地LLAMA模型 我的笔记本是近10年前买的配置一般的windows 目标 本地llm可以运行使用llama-cpp-python调用本地llm使用langchain/openai调用本地llm 需要重点说下&…

ESP8266 WiFi物联网智能插座—上位机和下位机通信协议

目录 1、配置节点协议 2、控制节点继电器开关协议 3、节点周期上报数据协议 4、升级节点协议 5、重启节点 本项目自定义了一套上位机和下位机通信协议&#xff0c;协议并不复杂&#xff0c;包含&#xff1a;配置节点、控制节点继电器开关、节点周期上报数据、升级节点和重启节点…

设计模式篇---桥接模式

文章目录 概念结构实例总结 概念 桥接模式&#xff1a;将抽象部分与它的实现部分解耦&#xff0c;使得两者都能够独立变化。 毛笔和蜡笔都属于画笔&#xff0c;假设需要有大、中、小三种型号的画笔&#xff0c;绘画出12种颜色&#xff0c;蜡笔需要3*1236支&#xff0c;毛笔需要…

大数据之Flume

Flume概述 一个高可用&#xff08;稳定&#xff09;&#xff0c;高可靠&#xff08;稳定&#xff09;&#xff0c;分布式的海量日志采集&#xff0c;聚合和传输的系统。Flume基于流式架构&#xff0c;灵活简单。日志文件即txt文件&#xff0c;不能传输音频&#xff0c;视频&am…

【狼疮抗凝物-- 抗心磷脂抗体】

狼疮抗凝物属于易栓症的一种. 狼疮抗凝物 &#xff08;Lupus Anticoagulant LAC&#xff09;是一种作用于磷脂的IgG或IgM的抗磷脂抗体&#xff0c;在体内和体外 凝血试验中&#xff0c;磷脂对凝血酶原酶复合体活化起模板作用。狼疮抗凝物是抗磷脂的成分的抗 体&#xff0c;在多…

消息队列中,如何保证消息的顺序性?

本文选自&#xff1a;advanced-java 作者&#xff1a;yanglbme 问&#xff1a;如何保证消息的顺序性&#xff1f; 面试官心理分析 其实这个也是用 MQ 的时候必问的话题&#xff0c;第一看看你了不了解顺序这个事儿&#xff1f;第二看看你有没有办法保证消息是有顺序的&#xf…

大范围XSS扫描工具:XSS-Freak,BurpSuite随机用户代理,Hades 静态代码审核系统

大范围XSS扫描工具&#xff1a;XSS-Freak&#xff0c;BurpSuite随机用户代理&#xff0c;Hades 静态代码审核系统。 #################### 免责声明&#xff1a;工具本身并无好坏&#xff0c;希望大家以遵守《网络安全法》相关法律为前提来使用该工具&#xff0c;支持研究学习…

华为云云耀云服务器L实例评测|centos7.9 配置python虚拟环境 运行django

文章目录 ⭐前言⭐安装python&#x1f496; wget下载&#x1f496; 选择安装位置 ⭐pip安装虚拟环境&#x1f496; pip3安装 virtualenv&#x1f496; 创建目录存放python虚拟环境 ⭐安装django&#x1f496; 指定端口运行django&#x1f496; 远程访问 ⭐总结⭐结束 ⭐前言 大…

STL函数对象和谓词

目录 函数对象 谓词 函数对象 概念: 重载函数调用操作符的类&#xff0c;其对象常称为函数对象 函数对象使用重载的()时&#xff0c;行为类似函数调用&#xff0c;也叫仿函数 本质: 函数对象(仿函数)是一个类&#xff0c;不是一个函数 特点: …

Linux进程创建、进程终止、进程等待、进程程序替换

目录 进程创建fork函数fork函数返回值fork创建子进程的目的之一fork调用失败的原因写实拷贝 进程终止进程执行结果进程退出码进程终止的理解进程的退出方式 进程等待进程等待的必要性进程等待的概念wait方法获取子进程status 进程程序替换替换原理替换函数函数解释命名理解单进…

深度学习自学笔记六:深层神经网络

一、深层神经网络概述 深层神经网络&#xff08;Deep Neural Networks&#xff0c;DNN&#xff09;是一种机器学习模型&#xff0c;由多个神经网络层组成。与传统的浅层神经网络相比&#xff0c;深层神经网络具有更多的隐藏层&#xff0c;使其能够进行更复杂、更抽象的特征学习…

【07】FISCOBCOS一键部署前的准备工作ubuntu安装,mysql,python,PyMySQL,java

官方文档最下面https://webasedoc.readthedocs.io/zh_CN/latest/docs/WeBASE/install.html JAVA已经安装好了,可以看控制台那篇文章【03】 安装mysql 下载并解压mysql sudo apt install -y mysql-server mysql-client libmysqlclient-dev安装 MySQL 服务器、MySQL 客户端和…

【C/C++笔试练习】——printf在使用%的注意事项、for循环语句的三个条件、运算符优先级、删除公共字符

文章目录 C/C笔试练习1.%符号在printf用作格式说明符的注意事项&#xff08;1&#xff09;输出%5.3s&#xff08;2&#xff09;判断%中小数点含义 2.for循环语句的三个条件&#xff08;3&#xff09;判断循环次数&#xff08;4&#xff09;判断循环次数 3.运算符优先级&#xf…

交换奇偶位:交换一个整数的二进制的奇偶位置(仅考虑正数情况)

方法二&#xff1a; 设计思想&#xff1a; 0xAAAAAAAA 的二进制表示为 10101010...&#xff08;从最低位开始&#xff09; 0x55555555 的二进制表示为 01010101...&#xff08;从最低位开始&#xff09; 问题&#xff1a;更加想不到掩码&#xff01;&#xff01;&#xf…