第2章_freeRTOS入门与工程实践之单片机程序设计模式
本教程基于韦东山百问网出的 DShanMCU-F103开发板 进行编写,需要的同学可以在这里获取: https://item.taobao.com/item.htm?id=724601559592
配套资料获取:https://rtos.100ask.net/zh/freeRTOS/DShanMCU-F103
freeRTOS系列教程之freeRTOS入门与工程实践章节汇总: https://blog.csdn.net/qq_35181236/article/details/132842016
第2章 单片机程序设计模式
本章目标
- 理解裸机程序设计模式
- 了解多任务系统中程序设计的不同
2.1 裸机程序设计模式
裸机程序的设计模式可以分为:轮询、前后台、定时器驱动、基于状态机。前面三种方法都无法解决一个问题:假设有A、B两个都很耗时的函数,无法降低它们相互之间的影响。第4种方法可以解决这个问题,但是实践起来有难度。
假设一位职场妈妈需要同时解决2个问题:给小孩喂饭、回复工作信息,场景如图所示,后面将会演示各类模式下如何写程序:
2.1.1 轮询模式
示例代码如下:
// 经典单片机程序: 轮询
void main()
{while (1){喂一口饭();回一个信息();}
}
在main函数中是一个while循环,里面依次调用2个函数,这两个函数相互之间有影响:如果“喂一口饭”太花时间,就会导致迟迟无法“回一个信息”;如果“回一个信息”太花时间,就会导致迟迟无法“喂下一口饭”。
使用轮询模式编写程序看起来很简单,但是要求while循环里调用到的函数要执行得非常快,在复杂场景里反而增加了编程难度。
2.1.1 前后台
所谓“前后台”就是使用中断程序。假设收到同事发来的信息时,电脑会发出“滴”的一声,这时候妈妈才需要去回复信息。示例程序如下:
// 前后台程序
void main()
{while (1){// 后台程序喂一口饭();}
}// 前台程序
void 滴_中断()
{回一个信息();
}
- main函数里while循环里的代码是后台程序,平时都是while循环在运行;
- 当同事发来信息,电脑发出“滴”的一声,触发了中断。妈妈暂停喂饭,去执行“滴_中断”给同事回复信息;
在这个场景里,给同事回复信息非常及时:即使正在喂饭也会暂停下来去回复信息。“喂一口饭”无法影响到“回一个信息”。但是,如果“回一个信息”太花时间,就会导致 “喂一口饭”迟迟无法执行。
继续改进,假设小孩吞下饭菜后会发出“啊”的一声,妈妈听到后才会喂下一口饭。喂饭、回复信息都是使用中断函数来处理。示例程序如下:
// 前后台程序
void main()
{while (1){// 后台程序}
}// 前台程序
void 滴_中断()
{回一个信息();
}// 前台程序
void 啊_中断()
{喂一口饭();
}
main函数中的while循环是空的,程序的运行靠中断来驱使。如果电脑声音“滴”、小孩声音“啊”不会同时、相近发出,那么“回一个信息”、“喂一口饭”相互之间没有影响。在不能满足这个前提的情况下,比如“滴”、“啊”同时响起,先“回一个信息”时就会耽误“喂一口饭”,这种场景下程序遭遇到了轮询模式的缺点:函数相互之间有影响。
2.1.2 定时器驱动
定时器驱动模式,是前后台模式的一种,可以按照不用的频率执行各种函数。比如需要每2分钟给小孩喂一口饭,需要每5分钟给同事回复信息。那么就可以启动一个定时器,让它每1分钟产生一次中断,让中断函数在合适的时间调用对应函数。示例代码如下:
// 前后台程序: 定时器驱动
void main()
{while (1){// 后台程序}
}// 前台程序: 每1分钟触发一次中断
void 定时器_中断()
{static int cnt = 0;cnt++;if (cnt % 2 == 0){喂一口饭();}else if (cnt % 5 == 0){回一个信息();}
}
- main函数中的while循环是空的,程序的运行靠定时器中断来驱使。
- 定时器中断每1分钟发生一次,在中断函数里让cnt变量累加(代码第14行)。
- 第15行:进行求模运算,如果对2取模为0,就“喂一口饭”。这相当于每发生2次中断就“喂一口饭”。
- 第19行:进行求模运算,如果对5取模为0,就“回一个信息”。这相当于每发生5次中断就“回一个信息”。
这种模式适合调用周期性的函数,并且每一个函数执行的时间不能超过一个定时器周期。如果“喂一口饭”很花时间,比如长达10分钟,那么就会耽误“回一个信息”;反过来也是一样的,如果“回一个信息”很花时间也会影响到“喂一口饭”;这种场景下程序遭遇到了轮询模式的缺点:函数相互之间有影响。
2.1.3 基于状态机
当“喂一口饭”、“回一个信息”都需要花很长的时间,无论使用前面的哪种设计模式,都会退化到轮询模式的缺点:函数相互之间有影响。可以使用状态机来解决这个缺点,示例代码如下:
// 状态机
void main()
{while (1){喂一口饭();回一个信息();}
}
在main函数里,还是使用轮询模式依次调用2个函数。
关键在于这2个函数的内部实现:使用状态机,每次只执行一个状态的代码,减少每次执行的时间,代码如下:
void 喂一口饭(void)
{static int state = 0;switch (state){case 0:{/* 舀饭 *//* 进入下一个状态 */state++;break;}case 1:{/* 喂饭 *//* 进入下一个状态 */state++;break;}case 2:{/* 舀菜 *//* 进入下一个状态 */state++;break;}case 3:{/* 喂菜 *//* 恢复到初始状态 */state = 0;break;}}
}void 回一个信息(void)
{static int state = 0;switch (state){case 0:{/* 查看信息 *//* 进入下一个状态 */state++;break;}case 1:{/* 打字 *//* 进入下一个状态 */state++;break;}case 2:{/* 发送 *//* 恢复到初始状态 */state = 0;break;}}
}
以“喂一口饭”为例,函数内部拆分为4个状态:舀饭、喂饭、舀菜、喂菜。每次执行“喂一口饭”函数时,都只会执行其中的某一状态对应的代码。以前执行一次“喂一口饭”函数可能需要4秒钟,现在可能只需要1秒钟,就降低了对后面“回一个信息”的影响。
同样的,“回一个信息”函数内部也被拆分为3个状态:查看信息、打字、发送。每次执行这个函数时,都只是执行其中一小部分代码,降低了对“喂一口饭”的影响。
使用状态机模式,可以解决裸机程序的难题:假设有A、B两个都很耗时的函数,怎样降低它们相互之间的影响。但是很多场景里,函数A、B并不容易拆分为多个状态,并且这些状态执行的时间并不好控制。所以这并不是最优的解决方法,需要使用多任务系统。
2.2 多任务系统
2.2.1 多任务模式
对于裸机程序,无论使用哪种模式进行精心的设计,在最差的情况下都无法解决这个问题:假设有A、B两个都很耗时的函数,无法降低它们相互之间的影响。使用状态机模式时,如果函数拆分得不好,也会导致这个问题。本质原因是:函数是轮流执行的。假设“喂一口饭”需要t1t5这5段时间,“回一个信息需要”tate这5段时间,轮流执行时:先执行完t1t5,再执行tate,如下图所示:
对于职场妈妈,她怎么解决这个问题呢?她是一个眼明手快的人,可以一心多用,她这样做:
- 左手拿勺子,给小孩喂饭
- 右手敲键盘,回复同事
- 两不耽误,小孩“以为”妈妈在专心喂饭,同事“以为”她在专心聊天
- 但是脑子只有一个啊,虽然说“一心多用”,但是谁能同时思考两件事?
- 只是她反应快,上一秒钟在考虑夹哪个菜给小孩,下一秒钟考虑给同事回复什么信息
- 本质是:交叉执行,t1t5和tate交叉执行,如下图所示:
基于多任务系统编写程序时,示例代码如下:
// RTOS程序
喂饭任务()
{while (1){喂一口饭();}
}回信息任务()
{while (1){回一个信息();}
}void main()
{// 创建2个任务create_task(喂饭任务);create_task(回信息任务);// 启动调度器start_scheduler();
}
- 第21、22行,创建2个任务;
- 第25行,启动调度器;
- 之后,这2个任务就会交叉执行了;
基于多任务系统编写程序时,反而更简单了:
- 上面第2~8行是“喂饭任务”的代码;
- 第10~16行是“回信息任务”的代码,编写它们时甚至都不需要考虑它和其他函数的相互影响。就好像有2个单板:一个只运行“喂饭任务”这个函数、另一个只运行“回信息任务”这个函数。
多任务系统会依次给这些任务分配时间:你执行一会,我执行一会,如此循环。只要切换的间隔足够短,用户会“感觉这些任务在同时运行”。如下图所示:
2.2.2 互斥操作
多任务系统中,多个任务可能会“同时”访问某些资源,需要增加保护措施以防止混乱。比如任务A、B都要使用串口,能否使用一个全局变量让它们独占地、互斥地使用串口?示例代码如下:
// RTOS程序
int g_canuse = 1;void uart_print(char *str)
{if (g_canuse){g_canuse = 0;printf(str);g_canuse = 1;}
}task_A()
{while (1){uart_print("0123456789\n");}
}task_B()
{while (1){uart_print("abcdefghij");}
}void main()
{// 创建2个任务create_task(task_A);create_task(task_B);// 启动调度器start_scheduler();
}
程序的意图是:task_A打印“0123456789”,task_B打印“abcdefghij”。在task_A或task_B打印的过程中,另一个任务不能打印,以避免数字、字母混杂在一起,比如避免打印这样的字符:“012abc”。
第6行使用全局变量g_canuse实现互斥打印,它等于1时表示“可以打印”。在进行实际打印之前,先把g_canuse设置为0,目的是防止别的任务也来打印。
这个程序大部分时间是没问题的,但是只要它运行的时间足够长,就会出现数字、字母混杂的情况。下图把uart_print函数标记为①~④个步骤:
void uart_print(char *str)
{if( g_canuse ) ①{g_canuse = 0; ②printf(str); ③g_canuse = 1; ④}
}
如果task_A执行完①,进入if语句里面执行②之前被切换为task_B:在这一瞬间,g_canuse还是1。
task_B执行①时也会成功进入if语句,假设它执行到③,在printf打印完部分字符比如“abc”后又再次被切换为task_A。
task_A继续从上次被暂停的地方继续执行,即从②那里继续执行,成功打印出“0123456789”。这时在串口上可以看到打印的结果为:“abc0123456789”。
是不是“①判断”、“②清零”间隔太远了,uart_print函数改进成如下的代码呢?
void uart_print(char *str)
{g_canuse--; ① 减一if( g_canuse == 0 ) ② 判断{printf(str); ③ 打印}g_canuse++; ④ 加一
}
即使改进为上述代码,仍然可能产生两个任务同时使用串口的情况。因为“①减一”这个操作会分为3个步骤:a.从内存读取变量的值放入寄存器里,b.修改寄存器的值让它减一,c.把寄存器的值写到内存上的变量上去。
如果task_A执行完步骤a、b,还没来得及把新值写到内存的变量里,就被切换为task_B:在这一瞬间,g_canuse还是1。
task_B执行①②时也会成功进入if语句,假设它执行到③,在printf打印完部分字符比如“abc”后又再次被切换为task_A。
task_A继续从上次被暂停的地方继续执行,即从步骤c那里继续执行,成功打印出“0123456789”。这时在串口上可以看到打印的结果为:“abc0123456789”。
从上面的例子可以看到,基于多任务系统编写程序时,访问公用的资源的时候要考虑“互斥操作”。任何一种多任务系统都会提供相应的函数。
2.2.3 同步操作
如果任务之间有依赖关系,比如任务A执行了某个操作之后,需要任务B进行后续的处理。如果代码如下编写的话,任务B大部分时间做的都是无用功。
// RTOS程序
int flag = 0;void task_A()
{while (1){// 做某些复杂的事情// 完成后把flag设置为1flag = 1;}
}void task_B()
{while (1){if (flag){// 做后续的操作}}
}void main()
{// 创建2个任务create_task(task_A);create_task(task_B);// 启动调度器start_scheduler();
}
上述代码中,在任务A没有设置flag为1之前,任务B的代码都只是去判断flag。而任务A、B的函数是依次轮流运行的,假设系统运行了100秒,其中任务A总共运行了50秒,任务B总共运行了50秒,任务A在努力处理复杂的运算,任务B仅仅是浪费CPU资源。
如果可以让任务B阻塞,即让任务B不参与调度,那么任务A就可以独占CPU资源加快处理复杂的事情。当任务A处理完事情后,再唤醒任务B。示例代码如下:
// RTOS程序
void task_A()
{while (1){// 做某些复杂的事情// 释放信号量,会唤醒任务B;}
}void task_B()
{while (1){// 等待信号量, 会让任务B阻塞// 做后续的操作}
}void main()
{// 创建2个任务create_task(task_A);create_task(task_B);// 启动调度器start_scheduler();
}
- 第15行:任务B运行时,等待信号量,不成功时就会阻塞,不在参与任务调度。
- 第7行:任务A处理完复杂的事情后,释放信号量会唤醒任务B。
- 第16行:任务B被唤醒后,从这里继续运行。
在这个过程中,任务A处理复杂事情的时候可以独占CPU资源,加快处理速度。
相关文章:
第2章_freeRTOS入门与工程实践之单片机程序设计模式
本教程基于韦东山百问网出的 DShanMCU-F103开发板 进行编写,需要的同学可以在这里获取: https://item.taobao.com/item.htm?id724601559592 配套资料获取:https://rtos.100ask.net/zh/freeRTOS/DShanMCU-F103 freeRTOS系列教程之freeRTOS入…...
python LeetCode 刷题记录 58
题目 给你一个字符串 s,由若干单词组成,单词前后用一些空格字符隔开。返回字符串中 最后一个 单词的长度。 单词 是指仅由字母组成、不包含任何空格字符的最大子字符串。 示例 输入:s "Hello World" 输出:5 解释&am…...
HarmonyOS开发:那些开发中常见的问题汇总(一)
前言 本来这篇文章需要讲述静态共享包如何实现远程依赖和上传以及关于静态共享包私服的搭建,非常遗憾的告诉大家,由于组织管理申请迟迟未通过,和部分文档官方权限暂未开放,关于这方面的讲解需要延后了,大概需要等到202…...
新能源汽车驱动电机的基本知识
学习目标:了解电机的基本知识。能力目标:培养学生搜集和整理相关资料的能力。素质目标:培养学生良好的职业素养。额定电店.在夫见定条件下电池工作的*于佳 电压知识准备术语和定义。 (1)驱动电机系统 通过有效的控制策略将动力蓄电池提供的直流电转化为交流实现电机的正转以及反…...
流媒体协议——RTSP
RTSP RTSP(Real Time Streaming Protocol):实时流媒体协议,是基于文本的协议,采用ISO10646字符集,使用UTF-8编码 RTSP-over-TCP,默认端口554 RTSP-over-HTTP,默认端口为8080 RTSP OVER WebSocket 默认端口为 80&am…...
Arcgis提取点数据经纬度
Arcgis提取点数据经纬度 现已打开tiff影像和采样点的shape文件。目标是提取采样点的经纬度信息然后导出。 打开数据管理工具-要素-添加XY坐标 在点的图层上右击打开属性表时,经纬度信息已经添加到属性表中。 在属性表的左上角中点击导出,导出为文本文…...
【小记录】jupyter notebook新版本
手欠升级 😅今天手贱,在anaconda navigator里面更新了最新版本的spyder,然后莫名奇妙地jupyter notebook就打不开了😅,报错说缺少模块”ModuleNotFoundError: No module named jupyter_server.contents“,…...
Ubuntu安装深度学习环境相关(yolov8-python部署)
Ubuntu安装深度学习环境相关(yolov8-python部署) 本文将从如下几个方面总结相关的工作过程: Ubuntu系统安装(联想小新pro16) 2.显卡驱动安装3.测试深度学习模型 1. Ubunut 系统安装 之前在台式机上安装过Ubuntu,以为再在笔记本上安装会是小菜一碟&…...
jmeter采集ELK平台海量业务日志( 采用Scroll)
由于性能测试需要,需采集某业务系统海量日志(百万以上)来使用。但Elasticsearch的结果分页size单次最大为10000(运维同事为保证ES安全)。为了能够快速采集ELK平台业务日志,可以使用以下2种方式采集…...
React 全栈体系(五)
第三章:React 应用(基于 React 脚手架) 一、使用 create-react-app 创建 react 应用 1. react 脚手架 xxx 脚手架: 用来帮助程序员快速创建一个基于 xxx 库的模板项目 包含了所有需要的配置(语法检查、jsx 编译、devServer…)下载好了所有…...
动态规划——状态机模型
什么是状态机模型?其实大部分dp问题都可以算是状态机,因为对于一个物品,例如01背包,无非是选与不选两种状态,这两种状态就构成了一个状态机。状态机就是一种用来描述对象或者系统在不同状态之间迁移的模型。 那么状态机…...
合宙Air724UG LuatOS-Air LVGL API控件-图片(Gif)
图片(Gif) GIF图片显示,core版本号要>3211 示例代码 方法一 -- 创建GIF图片控件 glvgl.gif_create(lvgl.scr_act()) -- 设置显示的GIF图像 lvgl.gif_set_src(g,"/lua/test.gif") -- gif图片居中 lvgl.obj_align(g, nil, lvgl…...
【C语言】指针和数组笔试题解析(2)
【C语言】指针和数组笔试题解析(1), 这是第一篇关于sizeof与strlen在指针中的应用,而这一篇主要讲解在各种情形下的灵活运用,也是大厂中经典的面试题 第一题: int main() {int a[5] { 1, 2, 3, 4, 5 };in…...
3.3 DLL注入:突破会话0强力注入
Session是Windows系统的一个安全特性,该特性引入了针对用户体验提高的安全机制,即拆分Session 0和用户会话,这种拆分Session 0和Session 1的机制对于提高安全性非常有用,这是因为将桌面服务进程,驱动程序以及其他系统级…...
C语言 —— 初步入门知识(内存、指针、结构体)
本篇文章将接着上篇继续介绍C语言的基础知识,那么对于C语言大部分初学者会觉得难以理解, 所以作者将指针单独拿出来写篇较短的文章进行讲解。 1.指针 1.1 内存 要学习指针,就先要了解内存。一起来看。 内存是计算机中的关键组成部分ÿ…...
PHP8中字符串与数组的转换-PHP8知识详解
在php8中使用explode()函数和implode()函数实现字符串和数组之间的转换。 1、使用explode()函数把字符串按照一定的规则拆分为数组中的元素,并且形成数组。 使用explode()函数把字符串转换数组,示范代码: <?php $string "html,cs…...
Wordtune:文本编辑工具
【产品介绍】 名称 Wordtune 上线时间 成立于2018年。 具体描述 Wordtune是一款基于人类智能的文本编辑工具,它可以帮助用户快速修改和重写英文,以改进文本的清晰度、流畅度和可读性。Wordtune使用先进的自然语言处理技术&#x…...
notifyIcon动态图标
定时器内调用下面代码 代码如下: if(DateTime.Now.Second % 2 0) {notifyIcon1.Icon new System.Drawing.Icon(Application.StartupPath "\abc.ico");}else{notifyIcon1.Icon new System.Drawing.Icon(Application.StartupPath "\abc2.ico"…...
2023年墨西哥 SP/BMV IPC 研究报告
第一章 指数概况 1.1 指数基本情况 墨西哥 S&P/BMV IPC 指数衡量在墨西哥证券交易所 (Bolsa Mexicana de Valores, BMV)上市,规模最大、流动性最高的股票表现。提供一个覆盖墨西哥股市的广泛、具有代表性且可轻易复制的指数。根据多元化要求,按市值…...
JWT生成与解析/JWT令牌前端存储
第一步:创建项目 添加Maven依赖: <dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.62</version> </dependency> <dependency><groupId>org.s…...
[交互]前端展示服务端获取的图片
可以通过以下步骤从服务端获取图片: 引入axios库:在前端代码中使用axios库来发送HTTP请求。可以通过以下方式引入axios: import axios from axios;发送请求:使用axios发送HTTP请求,获取图片文件的二进制数据。发送请求…...
LeetCode2.两数相加
一看完题,我的想法是先算出这两个链表表示的数,然后相加,然后把这个数一位一位的分配给第三个数组,这种方法应该很简单但是要遍历三次数组,于是我就想直接一遍遍历,两个链表同时往后面遍历,把这…...
Linux编译过程与交叉编译
一.GCC由来 GCC(GNU编译器套件)是一个自由开源的编程工具集,用于编译和链接C、C和其他编程语言的程序。它由理查德斯托曼(Richard Stallman)和其他自由软件基金会(Free Software Foundation)的…...
MediaPipe+OpenCV 实现实时手势识别(附Python源码)
MediaPipe官网:https://developers.google.com/mediapipe MediaPipe仓库:https://github.com/google/mediapipe 一、MediaPipe介绍 MediaPipe 是一个由 Google 开发的开源跨平台机器学习框架,用于构建视觉和感知应用程序。它提供了一系列预训…...
为什么选择C/C++内存检测工具AddressSanitizer?如何使用AddressSanitizer?
目录 1、C程序中的内存问题 2、AddressSanitizer是什么? 3、AddressSanitizer内存检测原理简述 3.1、内存映射 3.2、插桩 4、为什么选择AddressSanitizer? 4.1、Valgrind介绍 4.2、AddressSanitizer在速度和内存方面为什么明显优于Valgrind 4.3…...
获取vue当前页面url问号后面的参数
除了使用 window.location.search 或 Vue Router 的 $route.query 来获取 URL 问号后面的参数之外,您还可以使用 JavaScript 中的正则表达式来解析 URL 中的参数部分。以下是一个示例: // 获取当前页面的完整 URL const currentURL window.location.hre…...
Linux编程之线程池的设计与实现
Linux编程之线程池的设计与实现(C98) 代码 假设服务器的硬件资源“充裕”,那么提高服务器性能的一个很直接的方法就是空间换时间, 即“浪费”服务器的硬件资源,以换取其运行效率。 提升服务器性能的一个重要方法就是…...
stm32---定时器输入捕获
一、输入捕获介绍 在定时器中断实验章节中我们介绍了通用定时器具有多种功能,输入捕获就是其中一种。 STM32F1除了基本定时器TIM6和TIM7,其他定时器都具有输入捕获功能 。输入捕获可以对输入的信号的上升沿,下降沿或者双边沿进行捕获…...
打造生产级Llama大模型服务
对于任何想要尝试人工智能或本地LLM,又不想因为意外的云账单或 API 费用而感到震惊的人,我可以告诉你我自己的旅程是如何的,以及如何开始使用廉价的消费级硬件执行Llama2 推理 。 这个项目一直在以非常活跃的速度发展,这使得它非…...
Acwing 828. 模拟栈
Acwing 828. 模拟栈 题目要求思路讲解代码展示 题目要求 思路讲解 栈:先进后出 队列:先进先出 代码展示 #include <iostream>using namespace std;const int N 100010;int m; int stk[N], tt;int main() {cin >> m;while (m -- ){string o…...
wordpress属于区域连技术吗/上海网站seo外包
漏洞描述 GoCD plugin aip 参数中的 pluginName 参数存在任意文件读取漏洞,导致攻击者可以获取服务器中的任意敏感信息 fofa语法 title“Create a pipeline - Go” POC /go/add-on/business-continuity/api/plugin?folderName&pluginName../../../etc/pas…...
网站建设公司销售/如何交换友情链接
实验目的 1、掌握汇编语言程序的上机步骤 (1 )用编辑程序(如EDIT、记事本等)编辑汇编语言源程序(建立.ASM文件) (2 )用MASM程序产生OBJ文件 (3 )用LINK程序产生EXE文件 (4 )程序的运行(用DEBUG或在DOS下直接运行) 2、掌握DEBUG的使用方法 实验内容 1.在数据段DATA中有两个字…...
手机网站技术/百度免费推广怎么做
前言 面向对象程序设计有三个特征:封装、继承、多态,这三个特征即是语法也是手段,23种设计模板其实就是对这三种手段的灵活应用,今天不谈任何高大上的设计模式,仅仅谈谈代码封装。 引子 从三个特征的排序上ÿ…...
网站建设公司论坛/百度推广优化是什么?
前言 在实际js开发中对数组操作频率非常高,看过一些小伙伴的一些用法,挺有意思,在这里小记(不全)一下,备忘。 5个迭代方法:every、filter、forEach、map和some every():对数组中的每…...
企业网站建设必要性/天津网络推广公司
原文地址:ELF文件在带加载器的OS中和裸奔的加载及运行 作者:lelee007 工作关系,这个周花了一天时间好好研究了以下ELF文件及可执行ELF文件的加载。中间过程可谓收获不小,呵呵,因为之前搞linux驱动、ARM裸奔始终没有认真…...
2022年时事新闻摘抄/seo可以从哪些方面优化
所谓程序锁就是当用户启动某个程序的时候需要用户校验,如果校验成功,则进入应用程序. 也可以用于功能锁,也就是当用户使用程序的某个时,进行进行校验如果校验成功则进入该功能. 效果如下图所示: 该项目是google的开源项目. 下载地址:http://download.csdn.net/detail/johnny…...