Unity中【UniTask异步流程】如何进行【步骤分段】、【步骤撤销】、【步骤跳转】、【取消异步任务】

一、UniTask和Task

UniTask是Unity中的Task实现,Task是C#中实现异步操作的一个模块(类)。UniTask与Task有着同样的使用思路(使用习惯,常用API等),可以说UniTask是借鉴Task而开发出来的。

二、需求的来源

以前有一个实验,操作就是点击物体,执行动画,点击物体,执行动画…如此子子孙孙无穷循环,直到地球爆炸(实验结束)。

2.1 原来的脚本

于是很容易就用UniTask的await把所有操作连成一片,写在一个脚本里,甚至一整个实验就一个脚本。

比如下面:

1)、面板参数定义

面板用到的参数全部释放在Inspector上面,代码的话带上注释和空格将近1800行
在这里插入图片描述

2)、异步流程的组织

操作流程的话,写在一个异步方法里,浩浩荡荡写了上千行…写起来倒是比较滑溜了,但是调试和复用的话,就有点…
在这里插入图片描述
在这里插入图片描述

2.2 要改造成什么样

如果用户说他要【上一步】、【下一步】和【跳步】
思路:把一串流程按照操作逻辑分块,不同的块编一个号,放到一个执行列表里面。

1)、从前的脚本是这样:

/// <summary>
/// 主流程:所有流程连成一片,无法分步执行
/// </summary>
/// <returns></returns>
public async UniTask Flow()
{
    //第一步
    await UniTask.Delay(1000);
    //...

    //第二步
    await UniTask.Delay(1000);
    //...

    //第三步
    await UniTask.Delay(1000);
    //...

    //...

    //第N步
    await UniTask.Delay(1000);
    //...
}

2) 、分块思路

  • (1)定义一个列表StepInfosList,用来装载要执行的步骤。
    /// <summary>
    /// 步骤信息表:系列化到面板,用于调试和观察,后期执行
    /// </summary>
    [Header("步骤信息表")]
    public List<StepInfo> StepInfosList = new List<StepInfo>();

每一个节点StepInfo的结构如下:
在这里插入图片描述

3) 、分块后的脚本

/// <summary>
/// 步骤信息Class
/// </summary>
[Serializable]
public class StepInfo
{
    /// <summary>
    /// 当前序号
    /// </summary>
    [Header("当前步骤的序号")]
    public int index;

    /// <summary>
    /// 步骤的名字
    /// </summary>
    [Header("步骤的名字")]
    public string name;

    /// <summary>
    /// 正常流程
    /// </summary>
    public Func<UniTask> Flow;

    /// <summary>
    /// 恢复到【初始状态】的流程
    /// </summary>
    public Action Init;

    /// <summary>
    /// 跳到【结束状态】的流程
    /// </summary>
    public Action Final;
}
  • (2)拆分步骤

把脚本按照逻辑分成不同的块(步骤),装入到执行步骤列表里

/// <summary>
/// 添加步骤:新改的写法——各个步骤单独分开,逐个添加到执行列表里
/// </summary>
/// <param name="ctk"></param>
/// <returns></returns>
public async UniTask AddSteps(CancellationToken ctk)
{
    //第一步
    var step = StepInfosList.AddFlow("第一步");
    step.Flow = async () =>
    {
        Debug.Log("Flow() - 第一步");
        await UniTask.Delay(1000, cancellationToken: ctk);
    };   

    //第二步
    step = StepInfosList.AddFlow("第二步");
    step.Flow = async () =>
    {
        Debug.Log("Flow() - 第二步");
        await UniTask.Delay(1000, cancellationToken: ctk);
    }; 

    //第三步
    step = StepInfosList.AddFlow("第三步");
    step.Flow = async () =>
    {
        Debug.Log("Flow() - 第三步");
        await UniTask.Delay(1000, cancellationToken: ctk);
    };

    //第N步
    step = StepInfosList.AddFlow("第N步");
    step.Flow = async () =>
    {
        Debug.Log("Flow() - 第N步");
        await UniTask.Delay(1000, cancellationToken: ctk);
    };
}

三、跳步会有哪些操作

3.1 跳步的分类

在这里插入图片描述

3.2 跳步的实现

  • 【1】获取目标步骤信息
  • 【2】处理中间步骤的状态
  • 【3】执行目标步骤的流程
/// <summary>
/// 给定步骤名字,执行指定的步骤【所谓任意跳步】
/// </summary>
/// <param name="taskName"></param>
async UniTask RunTask(string taskName)
{
    if (cts.IsCancellationRequested)
    {
        Debug.Log("所有的任务已经被取消了!!");
        return;
    }

    //【1】获取目标步骤信息
    var targetStep = StepInfosList.First(x => x.name.Equals(taskName.Trim()));

    //【2】处理中间步骤的状态
    Debug.Log($"#################### 要执行的目标步骤为:targetStep = {targetStep.index}  {targetStep.name}  currentIndex = {currentIndex} ");
    if (currentIndex == targetStep.index)  //本步骤重新执行:恢复到本步骤初始状态
    {
        Debug.Log($"回到【{targetStep.name}】初始状态");
        if (targetStep.Init == null)
        {
            Debug.LogWarning($"步骤【{targetStep.index} {targetStep.name}】的Init函数为空,请补全!");
        }
        else
        {
            targetStep.Init();
        }
    }
    if (currentIndex < targetStep.index)   //往后跳步:中间步骤的状态快速补足
    {
        StepInfosList
            .Where(x => (x.index >= currentIndex) && (x.index < targetStep.index)).ToList()
            .ForEach(s =>
            {
                Debug.Log($"回到【{s.name}】结束状态");
                if (s.Final == null)
                {
                    Debug.LogWarning($"步骤【{s.index} {s.name}】的Final函数为空,请补全!");
                }
                else
                {
                    s.Final();
                }
            });
    }
    if (currentIndex > targetStep.index)   //往前跳步:中间步骤的状态快速撤销
    {
        StepInfosList
            .Where(x => (x.index >= targetStep.index) && (x.index <= currentIndex))
            .OrderByDescending(x=>x.index).ToList()        //注意倒序排列——由后往前执行
            .ForEach(s =>
            {
                Debug.Log($"回到【{s.name}】初始状态");
                if (s.Init == null)
                {
                    Debug.LogWarning($"步骤【{s.index} {s.name}】的Init函数为空,请补全!");
                }
                else
                {
                    s.Init();
                }
            });
    }

    //【3】执行目标步骤的流程
    await targetStep.Flow();
    currentIndex = targetStep.index;
    Debug.Log($"执行步骤:{targetStep.index}  {targetStep.name} 执行完毕,当前步骤currentIndex = {currentIndex}!");
}

3.3 执行所有步骤

用异步等待来逐个执行所有的步骤,如下所示:

/// <summary>
/// 执行所有的步骤
/// </summary>
/// <returns></returns>
private async UniTask RunAllTasks()
{
    foreach (var step in StepInfosList)
    {
        Debug.Log($"当前执行的步骤:{step.index} {step.name}");
        currentIndex = step.index;
        await step.Flow();
    }
}

3.4 如何取消所有步骤

  • 1)定义一个CancellationTokenSource(暂且称它为-异步任务取消标记)
    异步操作必须有取消,如果你不取消,当切换场景的时候,异步流程还在运行,则会出现资源的空引用。

举个不恰当的例子:你在家里玩电脑,然后让你的5岁的娃娃去楼下接妈妈回家。结果你媳妇从地下室坐电梯上来了,结局就是你的娃娃一直没等到他妈妈,结果呢,他呆坐在门口一直等,一直等,天黑也不回家。

问题在哪里:你指派给你小孩接妈妈这个异步操作,没有附加异步取消的令牌。下次你应该对他说:天黑还没等到妈妈的话,你就直接回家吃饭。或者我打电话给你,你就回家。

/// <summary>
/// 异步任务取消的标记
/// </summary>
private CancellationTokenSource cts;
  • 2)取消异步任务
    单凡用到上面cts的await,只要发现cts.IsCancellationRequested变成true,就会停止执行
/// <summary>
/// 取消所有的任务
/// </summary>
void CancelAllTasks()
{
    cts.Cancel();
}
  • 3) CancellationTokenSource是啥,如何服用?
    CancellationTokenSource就是一个遥控器,遥控器一按关机,那么凡是被该遥控器遥控的电视都关机。那么如何把遥控器绑定给一个电视机呢,也就是如何把一个CancellationTokenSource绑定给一坨异步操作。
    秘诀就是凡是await 操作的地方,你都绑一个CancellationTokenSource的token给它。
var cts = new CancellationTokenSource();
cancelButton.onClick.AddListener(() =>
{
    cts.Cancel();
});
await UnityWebRequest.Get("http://google.co.jp").SendWebRequest().WithCancellation(cts.Token);

await UniTask.DelayFrame(1000, cancellationToken: cts.Token);
  • 4)把所有的步骤绑定在一个CancellationTokenSource上
/// <summary>
/// 创建任务,用一个新的【CancellationTokenSource】控制这些异步任务
/// </summary>
/// <returns>cts</returns>
CancellationTokenSource CreatTasks()
{
    var cts = new CancellationTokenSource();
    StepInfosList.Clear();
    AddSteps(cts.Token).Forget();
    return cts;
}

四、测试步骤

4.1 测试脚本

//加载步骤流程到执行列表中
Debug.Log($"【1】************加载步骤流程到执行列表中:{Time.realtimeSinceStartup}");
cts = CreatTasks();

//执行一遍所有的任务
Debug.Log($"【2】************执行一遍所有的任务:{Time.realtimeSinceStartup}");
await RunAllTasks();

//等待3秒钟,跳步到第三步执行
Debug.Log($"【3】************等待3秒钟,跳步到第三步执行:{Time.realtimeSinceStartup}");
await UniTask.Delay(3000, cancellationToken: cts.Token);
await RunTask("第三步");

//取消所有任务
Debug.Log($"【4】************取消所有任务:{Time.realtimeSinceStartup}");
CancelAllTasks();

//等待3秒钟,跳步到第1步执行
Debug.Log($"【5】************等待3秒钟,跳步到第1步执行:{Time.realtimeSinceStartup}");
await UniTask.Delay(3000, cancellationToken: cts.Token);
await RunTask("第一步");

4.2 测试结果

在这里插入图片描述

五、附录:脚本源码

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;

/// <summary>
/// 步骤信息Class
/// </summary>
[Serializable]
public class StepInfo
{
    /// <summary>
    /// 当前序号
    /// </summary>
    [Header("当前步骤的序号")]
    public int index;

    /// <summary>
    /// 步骤的名字
    /// </summary>
    [Header("步骤的名字")]
    public string name;

    /// <summary>
    /// 正常流程
    /// </summary>
    public Func<UniTask> Flow;

    /// <summary>
    /// 恢复到【初始状态】的流程
    /// </summary>
    public Action Init;

    /// <summary>
    /// 跳到【结束状态】的流程
    /// </summary>
    public Action Final;
}

/// <summary>
/// 扩展方法
/// </summary>
public static class ExtensionMethods
{
    /// <summary>
    /// 在步骤列表MySteps中生成一个只包含步骤名的空步骤
    /// </summary>
    /// <param name="MySteps">步骤列表</param>
    /// <param name="stepName">步骤名字</param>
    /// <returns></returns>
    public static StepInfo AddFlow(this List<StepInfo> MySteps, string stepName)
    {
        int idx = MySteps.Count == 0 ?
            0
            :
            MySteps.Max(x => x.index) + 1;

        var item = new StepInfo();
        item.index = idx;
        item.name = stepName;
        item.Flow = null;
        item.Final = null;
        item.Init = null;
        MySteps.Add(item);
        return item;
    }
}

/// <summary>
/// 步骤分步,任意跳转:一段操作流程,包含多个步骤,可以任意跳转到某个步骤执行。
/// 点击【上一步】时,需要对当前步骤所操作的内容进行撤销或者状态还原。
/// 点击【下一步】时,需要把跳过的步骤中的状态补足。
/// </summary>
public class FlowsDemo : MonoBehaviour
{
    /// <summary>
    /// 步骤信息表:系列化到面板,用于调试和观察,后期执行
    /// </summary>
    [Header("步骤信息表")]
    public List<StepInfo> StepInfosList = new List<StepInfo>();

    /// <summary>
    /// 当前执行的步骤
    /// </summary>
    private int currentIndex;

    /// <summary>
    /// 异步任务取消的标记
    /// </summary>
    private CancellationTokenSource cts;

    /// <summary>
    /// 取消所有的任务
    /// </summary>
    void CancelAllTasks()
    {
        cts.Cancel();
    }

    /// <summary>
    /// 创建任务,用一个新的【CancellationTokenSource】控制这些异步任务
    /// </summary>
    /// <returns>cts</returns>
    CancellationTokenSource CreatTasks()
    {
        var cts = new CancellationTokenSource();
        StepInfosList.Clear();
        AddSteps(cts.Token).Forget();
        return cts;
    }

    /// <summary>
    /// 执行所有的步骤
    /// </summary>
    /// <returns></returns>
    private async UniTask RunAllTasks()
    {
        foreach (var step in StepInfosList)
        {
            Debug.Log($"当前执行的步骤:{step.index} {step.name}");
            currentIndex = step.index;
            await step.Flow();
        }
    }

    /// <summary>
    /// 主流程:所有流程连成一片,无法分步执行
    /// </summary>
    /// <returns></returns>
    public async UniTask Flow()
    {
        //第一步
        await UniTask.Delay(1000);
        //...

        //第二步
        await UniTask.Delay(1000);
        //...

        //第三步
        await UniTask.Delay(1000);
        //...

        //...

        //第N步
        await UniTask.Delay(1000);
        //...
    }

    /// <summary>
    /// 添加步骤:新改的写法——各个步骤单独分开,逐个添加到执行列表里
    /// </summary>
    /// <param name="ctk"></param>
    /// <returns></returns>
    public async UniTask AddSteps(CancellationToken ctk)
    {
        //第一步
        var step = StepInfosList.AddFlow("第一步");
        step.Flow = async () =>
        {
            Debug.Log("Flow() - 第一步");
            await UniTask.Delay(1000, cancellationToken: ctk);
        };
        step.Init = () => { Debug.Log("Init() - 回到第一步的初始状态");};
        step.Final = () => { Debug.Log("Final() - 跳到第一步的结束状态"); };

        //第二步
        step = StepInfosList.AddFlow("第二步");
        step.Flow = async () =>
        {
            Debug.Log("Flow() - 第二步");
            await UniTask.Delay(1000, cancellationToken: ctk);
        };
        step.Init = () => { Debug.Log("Init() - 回到第二步的初始状态"); };
        step.Final = () => { Debug.Log("Final() - 跳到第二步的结束状态"); };

        //第三步
        step = StepInfosList.AddFlow("第三步");
        step.Flow = async () =>
        {
            Debug.Log("Flow() - 第三步");
            await UniTask.Delay(1000, cancellationToken: ctk);
        };

        //第N步
        step = StepInfosList.AddFlow("第N步");
        step.Flow = async () =>
        {
            Debug.Log("Flow() - 第N步");
            await UniTask.Delay(1000, cancellationToken: ctk);
        };
    }

    /// <summary>
    /// 给定步骤名字,执行指定的步骤【所谓任意跳步】
    /// </summary>
    /// <param name="taskName"></param>
    async UniTask RunTask(string taskName)
    {
        if (cts.IsCancellationRequested)
        {
            Debug.Log("所有的任务已经被取消了!!");
            return;
        }

        //【1】获取目标步骤信息
        var targetStep = StepInfosList.First(x => x.name.Equals(taskName.Trim()));

        //【2】处理中间步骤的状态
        Debug.Log($"#################### 要执行的目标步骤为:targetStep = {targetStep.index}  {targetStep.name}  currentIndex = {currentIndex} ");
        if (currentIndex == targetStep.index)  //本步骤重新执行:恢复到本步骤初始状态
        {
            Debug.Log($"回到【{targetStep.name}】初始状态");
            if (targetStep.Init == null)
            {
                Debug.LogWarning($"步骤【{targetStep.index} {targetStep.name}】的Init函数为空,请补全!");
            }
            else
            {
                targetStep.Init();
            }
        }
        if (currentIndex < targetStep.index)   //往后跳步:中间步骤的状态快速补足
        {
            StepInfosList
                .Where(x => (x.index >= currentIndex) && (x.index < targetStep.index)).ToList()
                .ForEach(s =>
                {
                    Debug.Log($"回到【{s.name}】结束状态");
                    if (s.Final == null)
                    {
                        Debug.LogWarning($"步骤【{s.index} {s.name}】的Final函数为空,请补全!");
                    }
                    else
                    {
                        s.Final();
                    }
                });
        }
        if (currentIndex > targetStep.index)   //往前跳步:中间步骤的状态快速撤销
        {
            StepInfosList
                .Where(x => (x.index >= targetStep.index) && (x.index <= currentIndex))
                .OrderByDescending(x=>x.index).ToList()        //注意倒序排列——由后往前执行
                .ForEach(s =>
                {
                    Debug.Log($"回到【{s.name}】初始状态");
                    if (s.Init == null)
                    {
                        Debug.LogWarning($"步骤【{s.index} {s.name}】的Init函数为空,请补全!");
                    }
                    else
                    {
                        s.Init();
                    }
                });
        }

        //【3】执行目标步骤的流程
        await targetStep.Flow();
        currentIndex = targetStep.index;
        Debug.Log($"执行步骤:{targetStep.index}  {targetStep.name} 执行完毕,当前步骤currentIndex = {currentIndex}!");
    }

    /// <summary>
    /// 要测试的【步骤名字】
    /// </summary>
    [Header("要执行的【步骤名字】")]
    public string flowName;


#if UNITY_EDITOR
    [ContextMenu("跳到指定的步骤")]
#endif
    void TestFlow()
    {
        RunTask(flowName).Forget();
    }

#if UNITY_EDITOR
    [ContextMenu("取消所有任务")]
#endif
    void TestFlow1()
    {
        CancelAllTasks();
    }

#if UNITY_EDITOR
    [ContextMenu("Demo测试")]
#endif
    void TestFlow3()
    {
        Func<UniTask> TestDemo = async () =>
         {
            //加载步骤流程到执行列表中
            Debug.Log($"【1】************加载步骤流程到执行列表中:{Time.realtimeSinceStartup}");
            cts = CreatTasks();

            //执行一遍所有的任务
            Debug.Log($"【2】************执行一遍所有的任务:{Time.realtimeSinceStartup}");
            await RunAllTasks();

            //等待3秒钟,跳步到第三步执行
            Debug.Log($"【3】************等待3秒钟,跳步到第三步执行:{Time.realtimeSinceStartup}");
            await UniTask.Delay(3000, cancellationToken: cts.Token);
            await RunTask("第三步");

            //取消所有任务
            Debug.Log($"【4】************取消所有任务:{Time.realtimeSinceStartup}");
            CancelAllTasks();

            //等待3秒钟,跳步到第1步执行
            Debug.Log($"【5】************等待3秒钟,跳步到第1步执行:{Time.realtimeSinceStartup}");
            await UniTask.Delay(3000, cancellationToken: cts.Token);
            await RunTask("第一步");
         };
        
        Debug.Log($"开始测试:{Time.realtimeSinceStartup}");
        TestDemo();
    }
}

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

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

相关文章

Maven内网开发使用离线仓库

Maven内网开发使用离线仓库 离线或者内网环境开发与外网不通&#xff0c;中央仓库连不上&#xff0c;使用 Maven 管理项目会遇到很多问题。 比如&#xff1a;依赖包缺失&#xff0c;内网的Nexus私服的包老旧&#xff0c;很久没有维护&#xff0c;项目无法运行打包&#xff0c;…

PDF Expert for mac(专业pdf编辑器)苹果电脑

PDF Expert for Mac 是一款功能强大、界面简洁的PDF阅读、编辑和转换工具&#xff0c;为Mac用户提供了全面而便捷的PDF处理体验。无论是日常工作中的文档阅读、标注&#xff0c;还是专业需求下的编辑、转换&#xff0c;PDF Expert 都能满足您的各种需求。 首先&#xff0c;PDF…

UWB人员定位系统的原理与应用

uwb定位技术源码 uwb高精度定位系统源码 uwb人员定位系统基于什么原理&#xff1f; UWB人员定位系统基于超宽带(Ultra WideBand)技术进行位置定位。它利用超短脉冲信号&#xff0c;通过测量信号的到达时间差和信号强度等信息&#xff0c;实现对目标位置的定位。UWB技术具有高…

由于找不到dll无法执行代码的解决方法,深度解析5个dll修复方法

在使用计算机的过程中&#xff0c;我们经常会遇到一些错误提示&#xff0c;其中之一就是“找不到dll无法执行代码”。这个错误通常发生在程序运行时&#xff0c;系统无法找到所需的动态链接库&#xff08;DLL&#xff09;文件。这个问题可能由多种原因引起&#xff0c;包括缺少…

C/C++数据结构之链表题目答案与解析

个人主页&#xff1a;点我进入主页 专栏分类&#xff1a;C语言初阶 C语言程序设计————KTV C语言小游戏 C语言进阶 C语言刷题 数据结构初阶 欢迎大家点赞&#xff0c;评论&#xff0c;收藏。 一起努力&#xff0c;一起奔赴大厂。 目录 1.前言 2.题目…

Redis的三种特殊数据类型

文章目录 一、Redis geospatial 地理位置二、Redis Hyperloglog 基数统计的算法三、Redis Bitmaps 位存储&#xff08;0、1&#xff09;总结 一、Redis geospatial 地理位置 1.geoadd&#xff1a;将指定的地理空间位置&#xff08;纬度、经度、名称&#xff09;添加到指定的ke…

12. 定时器按键消抖

12. 定时器按键消抖 定时器按键消抖简介定时器消抖配置步骤程序编写bsp_keyfilter.hbsp_keyfilter.cmain 定时器按键消抖简介 使用延时函数消抖会浪费 CPU 性能&#xff0c;因为延时函数就是空跑。如果按键是使用中断的方式实现的&#xff0c;就更不能在中断服务函数中使用延时…

深入浅出Python异常处理 - 你所不知道的Python异常

深入浅出Python异常处理 - 你所不知道的Python异常 前言 在Python编程开发中&#xff0c;异常处理扮演者至关重要的角色。合适的异常处理不仅可以提高代码的健壮性&#xff0c;还能增强程序的可读性和可维护性。在Python编程中&#xff0c;有效地管理异常是提高代码质量的关键…

使用c++解压rar文件,基于UnRAR64,非命令行

最近项目需要解压缩rar文件&#xff0c;我们都知道rar是闭源收费软件&#xff0c;如果直接采用命令行可能会有限制&#xff0c;或者盗版问题&#xff0c;使用正版的winrar命令行解压rar文件是否有限制&#xff0c;这个我没来得及测试&#xff0c;但是从交互体验上来说&#xff…

EFCore: The ConnectionString property has not been initialized.

使用NuGet的程序包管理控制台执行命令“update-database”的时候报出该错误 经过检查发现是optionsBuilder.UseSqlServer(strConn);中的strConn没有写

【python海洋专题四十六】研究区域示意放大图

【python海洋专题四十六】研究区域示意放大图 图片 往期推荐 图片 【python海洋专题一】查看数据nc文件的属性并输出属性到txt文件 【python海洋专题二】读取水深nc文件并水深地形图 【python海洋专题三】图像修饰之画布和坐标轴 【Python海洋专题四】之水深地图图像修饰 …

穿越内存迷宫:C语言地址与指针的的冒险之旅

前言 C语言的魅力在于其直接的内存控制&#xff0c;而地址和指针是这种控制的核心。深入了解这些概念&#xff0c;将帮助我们更好地理解和利用C语言的潜力。本文将带领你踏上地址与指针的奇妙之旅&#xff0c;揭示它们在程序设计中的神秘面纱。 奇妙指针之旅&#xff1a;解码…

日语形容词分类

かっこいい的否定变形是かっこよくない

Selenium自动化测试细节讲解

与以前瀑布式开发模式不同&#xff0c;现在软件测试人员具有使用自动化工具执行测试用例套件的优势&#xff0c;而以前&#xff0c;测试人员习惯于通过测试脚本执行来完成测试。 但自动化测试的目的不是完全摆脱手动测试&#xff0c;而是最大程度地减少手动运行的测试。自动化…

计算机毕业设计选题推荐-农产品销售微信小程序/安卓APP-项目实战

✨作者主页&#xff1a;IT研究室✨ 个人简介&#xff1a;曾从事计算机专业培训教学&#xff0c;擅长Java、Python、微信小程序、Golang、安卓Android等项目实战。接项目定制开发、代码讲解、答辩教学、文档编写、降重等。 ☑文末获取源码☑ 精彩专栏推荐⬇⬇⬇ Java项目 Python…

抖音小程序开发:打造高效餐饮团购平台的技术指南

在餐饮行业&#xff0c;通过抖音小程序开发一个高效的团购平台&#xff0c;可以为餐厅提供更广泛的曝光&#xff0c;增加销售机会。本文将从技术角度出发&#xff0c;为您提供一份详细的抖音小程序开发指南&#xff0c;助您打造一流的餐饮团购平台。 一、确定需求和功能 在开…

让各大运营商都默默流泪的 HTTPS 协议(HTTPS 的加密流程)

文章目录 前言1. 什么是 HTTPS1.1 臭名昭著的 "运营商劫持" 2. 什么是"加密"3. HTTPS 的加密流程3.1 对称加密用对称加密可行吗&#xff1f; 3.2 引入非对称加密用对称加密非对称加密可行吗&#xff1f; 3.3 中间人攻击如何证明浏览器收到的公钥一定是该网…

栈 和 队列

什么是栈? 一种特殊的线性表&#xff0c;只允许在固定的一端进行插入和删除元素操作。进行数据插入和删除操作的一端称为栈顶&#xff0c;另一端称为栈底。栈中的数据元素遵守后进先出&#xff08;LIFO - Last In First Out&#xff09;的原则。   从数据结构的角度来看&…

【图文详解】Android Studio(新版本) 配置OpenCV库,解决出现的各种问题

前言 写这篇文章的目的就是记录自己在配置OpenCV库时遇到的问题。在网上查找相关资料时&#xff0c;发现很多Android Studio都是老版本&#xff0c;并且出现的问题都不能被解决。自己在配置过程中出现的问题都进行记录下来并一一解决。 新建项目 点击 New Project 选择界面 …

STM32F4X SDIO(九) 例程讲解-SD卡擦除、读写

STM32F4X SDIO &#xff08;九&#xff09; 例程讲解-SD卡擦除、读写 例程讲解-SD卡擦除、读写SD卡擦除CMD32:ERASE_WR_BLK_START命令发送命令响应 CMD33:ERASE_WR_BLK_END命令发送命令响应CMD38:ERASE命令响应 CMD13:SD_CMD_SEND_STATUS命令发送命令回应 SD卡读数据CMD16:SET_…