2024年3月

引言

在JDK1.2之前Java并没有提供软引用、弱引用和虚引用这些高级的引用类型。而是提供了一种基本的引用类型,称为
Reference
。并且当时Java中的对象只有两种状态:被引用和未被引用。当一个对象被引用时,它将一直存在于内存中,直到它不再被任何引用指向时,才会被垃圾回收器回收。而被引用也就是强引用。

而在JDK1.2之后对引用的概念进行了扩充,分为了强引用(
StrongReference
)、软引用(
SoftReference
)、弱引用(
WeakReference
)和虚引用(
PhantomReference
),这4种引用的强度依次减弱。他们的关系如下如:
image.png

强引用

强引用是Java中最常见的引用类型。当你创建一个对象并将其赋值给一个变量时,这个变量会持有该对象的强引用。

Order order = new Order(); // 只要order还指向Order对象,那么Order对象就不会被回收
order = null; // 强引用都被设置为 null 时,不可达,则Order对象被回收

只要存在强引用指向对象,垃圾回收器将永远不会回收该对象,即使内存不足也不会回收。这可能导致内存溢出,因为即使内存不足,JVM也不会回收强引用对象。当强引用都被设置为null时,对象变成不可达状态,垃圾回收器会在适当的时候将其回收。

比如以下示例,我们创建一个2M的数组,但是我们设置JVM参数:
-Xms2M -Xmx3M
,将JVM的初始内存设为2M,最大可用内存为3M。

public static void main(String[] args) {  
    //定义一个2M的数组  
    byte[] objects = new byte[1024 * 1024 * 2];  
}

此时我们执行方法后,发现报错:

image.png
对于强引用,即使内存不够使用,直接报错OOM,强引用也不会被回收。

对于强引用,就好比生活中,当我们拥有家里的钥匙时,我们可以随时进入你的家,即使我们不需要进入,也能确保我们可以进入。钥匙是我们进入家的强引用。只有当我们不再拥有钥匙时,我们才无法进入家,类似于当没有强引用指向一个对象时,该对象才能被垃圾回收。

软引用

在JDK1.2之后,用
java.lang.ref.SoftReference
类来表示软引用。软引用允许对象在内存不足时被垃圾回收器回收。如果一个对象只有软引用指向它,当系统内存不足时,垃圾回收器会尝试回收这些对象来释放内存,如果回收了软引用对象之后仍然没有足够的内存,才会抛出内存溢出异常。软引用适用于需要缓存大量对象,但又希望在内存不足时释放部分对象以避免内存溢出的情况,用于实现缓存时,当内存紧张时,可以释放部分缓存对象以保证系统的稳定性。

以下示例我们设置JVM参数为:
-Xms3M -Xmx5M
,然后连续创建了10个大小为1M的字节数组,并赋值给了软引用,然后循环遍历将这些对象打印出来。

private static final List<Object> list = Lists.newArrayList();  
  
public static void main(String[] args) {  
    IntStream.range(0, 10).forEach(i -> {  
        byte[] buff = new byte[1024 * 1024];  
        SoftReference<byte[]> sr = new SoftReference<>(buff);  
        list.add(sr);  
    });  
  
    System.gc(); // 主动通知垃圾回收  
  
    list.forEach(l -> {  
        Object obj = ((SoftReference<?>) l).get();  
        System.out.println("Object: " + obj);  
    });  
}

然后我们执行代码之后:

image.png
对于打印结果中,只有最后一个对象保留了下来,其他的obj全都被置空回收了。即说明了在内存不足的情况下,软引用将会被自动回收。

对于弱引用,就像我们医药箱里的备用药,当我们需要药品时,我们会先看看医药箱里是否有备用药。如果医药箱里有足够的药品(内存足够),我们就可以使用备用药;但如果医药箱里的备用药不够了(内存不足),我们可能会去药店购买。在内存不足时,垃圾回收器可能会回收软引用对象,类似于我们在医药箱里的备用药被用完时去药店购买。

弱引用

JDK1.2之后,用
java.lang.ref.WeakReference
来表示弱引用。弱引用与软引用类似,但强度更弱。即使内存足够,只要没有强引用指向一个对象,垃圾回收器就可以随时回收该对象。弱引用适用于需要临时引用对象的场景,如临时缓存或临时存储对象。也可以用于解决对象之间的循环引用问题,避免内存泄漏。

对于上述示例中,我们将数组赋值给弱引用

private static final List<Object> list = Lists.newArrayList();  
  
public static void main(String[] args) {  
    IntStream.range(0, 10).forEach(i -> {  
        byte[] buff = new byte[1024 * 1024];  
        WeakReference<byte[]> sr = new WeakReference<>(buff);  
        list.add(sr);  
    });  
  
    System.gc(); // 主动通知垃圾回收  
  
    list.forEach(l -> {  
        Object obj = ((WeakReference<?>) l).get();  
        System.out.println("Object: " + obj);  
    });  
}

执行结果发现所有的对象都是null,即都被回收了。

image.png

对于弱引用,就像我们正在旅行,使用一张一次性地图。我们只在需要导航时使用地图,一旦旅行结束,我们就不再需要地图了。这时我们可以选择扔掉地图,类似于弱引用,在垃圾回收器运行时,无论内存是否充足,对象都可能被回收。

虚引用

在 JDK1.2之后,用
java.lang.ref.PhantomReference
类来表示虚引用。虚引用是最弱的引用类型,它几乎对对象没有任何影响,不能通过虚引用获取对象,也不能通过它来阻止对象被垃圾回收。从源码中可以看出它只有一个构造函数和一个 get() 方法,而且它的 get() 方法仅仅是返回一个null,也就是说将永远无法通过虚引用来获取对象,虚引用必须要和 ReferenceQueue 引用队列一起使用。

image.png

虚引用可以用于在对象被回收时进行后续操作,如对象资源释放或日志记录,常用于跟踪对象被垃圾回收的状态,执行一些清理工作。

而对于弱引用,就像我们去商店,商店入口处的门闩并不直接影响你进入房屋,但它会在有人进入或离开时发出声音,提醒你有人进店(欢迎光临)或者离开(欢迎再来)。类似地,虚引用并不直接影响对象的生命周期,但它可以在对象被回收时发出通知,让你有机会进行一些后续操作,比如资源释放或者记录日志。

引用队列

引用队列(
ReferenceQueue
)是Java中的一个特殊队列,用于配合软引用、弱引用和虚引用,实现更灵活的对象引用和回收管理。

引用队列的主要作用是跟踪对象的垃圾回收过程。当一个软引用、弱引用或虚引用指向的对象被垃圾回收器回收时,如果它们与一个引用队列关联,那么这些引用就会被自动加入到引用队列中。通过监视引用队列中的对象,我们可以了解到对象的回收状态,从而执行一些额外的操作,比如资源释放或日志记录等。

总结

Java中的四种引用类型各具特点,可根据程序需求选择合适的引用类型。强引用保证对象不被意外回收,软引用和弱引用用于实现缓存或解决内存敏感问题,而虚引用则用于对象回收后的通知和清理操作。合理使用引用类型可以更好地管理内存和避免内存泄漏问题。

本文已收录于我的个人博客:
码农Academy的博客,专注分享Java技术干货,包括Java基础、Spring Boot、Spring Cloud、Mysql、Redis、Elasticsearch、中间件、架构设计、面试题、程序员攻略等

写在前面

在去年的S13全球总决赛中,Faker今年面对LPL最强的队伍,一号种子JDG时,语出惊人:
我见证了许多队伍的兴衰,但是浪花淘尽,唯有我屹立不倒

当然最后Faker更是一路高歌猛进,捧起了人生的第四座召唤师奖杯,赛后王多多老师的结束语更是让笔者有了无限的感慨,其中最让笔者感触的一句话就是:
真正的英雄往往是以平凡的身躯对抗岁月的麻木不仁。


为什么笔者会把电竞和软工这样两个看似毫不相干的领域绑在一起,因为这两个至少有一点是比较相似的,那就是从
外表上看是吃青春饭
的,确实如果在电竞中一天是当八天用,那么在软工这个领域,一天也是当三天用,或许这个时候同学们已经注意到,笔者用的是外表上看这样的修饰,那不用我说我相信同学们也已经有所感触了,即便是在八倍速的世界里,Faker这样一个96年的老头仍然有着绝对的统治力。因此,在年龄焦虑这场考试中,Faker成功交出了答案。换个角度说,我们面对年龄危机并非无解。

但是笔者想说的是,面对年龄危机不是靠一腔热血就能完成的,这是一场考试,我们需要给出答案才可以,这就是笔者开张的原因,今天第一篇先随便聊聊吧。


首先还是讲大家最关心的问题吧,求职,如果连工作都没有那就一切免谈,提到工作大家最关心的无疑是学历,证书,项目这些东西

那今天先谈一下最简单的证书【为啥不谈学历呢,因为笔者自己都还没考上研究生,没对比所以没资格谈,丢人了,先欠着,以后考上了再补】

价值在影响力

很多同学肯定都想问,考出来这个证书,那个证书有没有用,肯定是很多同学都关心的一个问题。包括很多同学听机构或者哪个大厂员工说,考过了什么什么证书就可以让面试官跪下叫爸爸,最后考出来发现面试官不仅没跪下,还一脚给你踢出大门了。最后怪机构坑钱,到网上散步一波负能量结束了。

那么笔者想说的是什么呢,首先不知道大家有没有注意到一件事情

在国内,法学行业有法律职业资格考试,财会专业有注册会计培训师这样决定命运的国家认可的证书,这些证书一过就代表了你有资格上岗,没有你就只能靠边站,但是我们编程是没有的,可能有同学提到软考,计算机二级,这些虽然是国家认可的证书,似乎对你工作都没啥太大影响。

原因是什么,很简单嘛,咱们用的所有技术都是国外的公司设计的东西,Python是Python Software Foundation(PSF)研发的,
Java是由Sun Microsystems公司开发的,虽然人家现在放开给咱们用,但是仍然是私人的物品,那怎么可能会有国家层面认可的东西。

好比英雄联盟,很多同学被他的声势浩大产生的错觉所迷惑,完全忘了这只是拳头公司研发的一款游戏而已,这是这款游戏的影响力实在是太大了。

所以,我们首先搞清楚一件事,机构介绍的这样那样的考试,都是公司推出的证书,认可程度纯粹看的是公司的影响力大还是小。

那这样就很好理解了,比方说你考一张oracle公司推出的OCP考试肯定比考一张笔者推出的Hello World技术考试认可度要高得多。

机构是否靠谱

那这些机构有没有说谎呢,在笔者眼里看来,其实比较大规模的机构并没有说谎,从笔者的经验来看,国外的这些考试大部分都是通过国内的机构代理的。理由也很简单,毕竟人家公司在国外,直接把手伸到国内国家也不答应,而且来回成本太高了,所以他们一般都会找国内的机构合作。

国内在计算机方面,分为两种机构
【当然部分比较大的机构二者都有】
,一种是
培训机构,
就是大家很熟悉的科班和培训班的博弈,这个问题以后再聊,另一种就是
备考机构
,这些机构尽管也上课,但是他们是为了应试而产生的。就好比大家报的考研辅导班,只是送你过考试。并不是为了教你学会什么。笔者认为这些机构,同学们可以去相信他们。因为这些机构往往和国外的公司是有合作的,他们可以帮你牵线搭桥去预约考试,这些流程他们都已经很熟悉了,保姆式服务可以让你安心准备考试,而不用被这些繁琐的流程所困扰。

考完证书以后

那既然机构没有说谎,肯定有同学要问了,为什么机构说出来的话似乎和结果对不上,考出来的证书似乎没有决定性作用。同学们是否还记得,笔者在前面说过这些考试都是国外的考试。

在国外其实没有应试这个说法,这个时候可以套用张雪峰的一句概况,
在国外是想着怎么把题目做对,在国内是想着这个题目我不会还能做对
,所以机构为了让你过考试,教的都是应试教育,

至此同学们就明白区别在哪了吧,如果我们现在在大学考期末考试,在国外大家是把整本书学完了通过考试,而国内大家是只学划重点就通过考试,因此出来的硬实力是不一样的。笔者给大家打个比方,如果说通过考试是上车,那么在国外是先交票后上车,而在国内就是先上车后补票。很多同学都没有完成补票这个环节,那可不就得上了车也要被赶下去嘛。

但是同学们其实也没法过多的去道德绑架他们,因为机构的任务就是送你过考试,考试通过了他们的任务也就完成了,你不能说他们没有尽心尽责。这是国情导致的,确实没有办法,国内的人口体量确实比较庞大,大家无论做什么都是希望在最短的时间里出结果。所以,在细节上就没法尽善尽美。因此,笔者也希望大家不要过多的去抱怨环境,哪怕我们现在是封建制度的皇帝,很多事情也不是想怎么样就怎么样的。

给读者的建议

笔者,之前听到的一个论述可以给大家介绍一下,只要是中国参与的考试,不管国内外的什么考试,都在于淘汰,将在于将一定比例的人淘汰,笔者还是那句话,国情所制,国内目前的人均经济在全球排名仍然不算靠前,因此注定了不能所有人都享受到最好的结果。所以,笔者给大家的建议是什么呢,前面也已经提到了,先上车后补票。

上车:就是所谓的应试,不要跟大多数人走,要做大多数人都做不到的事情。

  • 大多数人坚持不到最后,你不用管你坚持到最后就行了
  • 大多数人认为很多知识点看一看就好了,不用敲
  • 大多数人喜欢偏科,对于分数占比比较低的学科不去较多地下功夫
  • ....

笔者就一句话,我不知道这些所谓的经验贴对你有没有用,乍一听甚至是很合理的,笔者只告诉大家大多数人是这么想的,而大多数人是过不了的。

补票:考完以后不是万事大吉了,虽然前面笔者说不要跟大多数人走,要学大多数人不愿意去学的东西,但是基本上仍然停留在考纲内。现在既然考试通过了,我们就要想办法成为真正的大师,那这个时候就没有什么诀窍了,就是静下心来“两耳不闻窗外事,一心只读圣贤书”,借用大家最喜欢的一句话吧:真正的大师总是怀着一颗学徒的心。

1、准备材料

正点原子stm32f407探索者开发板V2.4

STM32CubeMX软件(
Version 6.10.0

Keil µVision5 IDE(
MDK-Arm

野火DAP仿真器

XCOM V2.6串口助手

2、学习目标

本文主要学习 FreeRTOS 低功耗的相关知识,
包括HAL 库基础时钟、FreeRTOS 基础时钟、低功耗处理和 Tickless 模式等知识

3、前提知识

3.1、HAL 库基础时钟

当我们使用 STM32CubeMX 软件配置一个基本的工程时,往往需要首先在 Pinout & Configuration 页面 RCC 中配置 HSE 和 LSE ,然后在 SYS 中配置 Debug 和 Timebase Source,这些都是必不可少的配置步骤,其中 Timebase Source 可以选择默认的 SysTick ,也可以选择任何一个定时器外设

3.1.1、使用 SysTick 定时器

学习 STM32 HAL 库开发,在 SYS 中配置 Timebase Source 时,一般将时基源保持默认的 SysTick 即可,
那么这个默认的 SysTick 是如何被初始化以及使用呢?

3.1.1.1、工作原理

打开 “
STM32CubeMX教程1 工程建立
” 文章配置的 STM32 空工程,找到 main.c 文件中的 main() 主函数,SysTick 在主函数第一个被执行的函数HAL_Init() 中得到初始化,具体如下图所示

其中滴答定时器频率
uwTickFreq
参数默认为 HAL_TICK_FREQ_DEFAULT(1KHZ) ,当然也可以根据需要修改为 10HZ 和 100HZ,如下述枚举类型定义

typedef enum
{
  HAL_TICK_FREQ_10HZ         = 100U,
  HAL_TICK_FREQ_100HZ        = 10U,
  HAL_TICK_FREQ_1KHZ         = 1U,
  HAL_TICK_FREQ_DEFAULT      = HAL_TICK_FREQ_1KHZ
} HAL_TickFreqTypeDef;

当初始化完毕之后,滴答定时器就会以固定频率发生中断,然后进入中断回调函数 SysTick_Handler() 中,滴答定时器中断默认就会开启

3.1.1.2、中断处理

在 STM32CubeMX 软件的 NVIC 管理页面,可以发现默认开启的滴答定时器中断 Time base: System tick timer ,在软件上该中断不可关闭,但是可以设置中断优先级,具体如下图所示

在 stm32f4xx_it.c 文件中可以找到滴答定时器的回调函数 SysTick_Handler() ,其只调用了 HAL_IncTick() 函数,该函数只做了一件事情,就是每次发生滴答定时器中断的时候,将一个名为
uwTick
的全局变量加 1 (
uwTickFreq
参数默认为1),具体如下所示

根据这个全局变量的值我们就可以做一些延时的工作,比如常用到的 HAL_Delay() 延时函数就是通过滴答定时器中断来实现的,具体如下所述

/**
  * @brief  HAL 库延时函数
  * @param  Delay:延时时间,单位为ms
  * @retval None
  */
__weak void HAL_Delay(uint32_t Delay)
{
	uint32_t tickstart = HAL_GetTick();
	uint32_t wait = Delay;
	
	/* 最少等待一个频率时间 */
	if (wait < HAL_MAX_DELAY)
	{
		wait += (uint32_t)(uwTickFreq);
	}
	/* 空循环延时等待 */
	while((HAL_GetTick() - tickstart) < wait)
	{
	}
}

另外还有 HAL_SuspendTick() 和 HAL_ResumeTick() 两个控制滴答定时器中断停止和启动的函数,具体如下所述

/**
  * @brief  挂起滴答定时器中断
  * @retval None
  */
__weak void HAL_SuspendTick(void)
{
  /* 禁用 SysTick 中断 */
  SysTick->CTRL &= ~SysTick_CTRL_TICKINT_Msk;
}

/**
  * @brief  恢复挂起的滴答定时器中断
  * @retval None
  */
__weak void HAL_ResumeTick(void)
{
  /* 使能 SysTick 中断 */
  SysTick->CTRL  |= SysTick_CTRL_TICKINT_Msk;
}

3.1.2、使用其他定时器

当 SysTick 被其他软件使用时(比如本系列教程的 FreeRTOS),STM32 还可以选择任何一个定时器外设作为其 HAL 库的时基源,比如选择基础定时器 TIM6

3.1.2.1、工作原理

当在 STM32CubeMX 软件中配置 SYS 中的 Timebase Source 为 TIM6 然后生成工程之后,与 “3.1.1、使用 SysTick 定时器” 小节不同的是,其首先会在 Core 文件夹下多出一个名为 stm32f4xx_hal_timebase_tim.c 的文件,该文件中涉及了所有关于 TIM6 作为 HAL 库系统嘀嗒定时器的配置程序,使用 TIM6 作为 HAL 库系统嘀嗒定时器的初始化步骤如下图所示

上图内容其实就是将基础定时器 TIM6 初始化为一个周期为 1ms 的定时器,并且启动其周期中断回调,如果对上述代码不了解可以阅读 “
STM32CubeMX教程5 TIM 定时器概述及基本定时器
” 实验

3.1.2.2、中断处理

当选择基础定时器 TIM6 作为 SysTick 时,在STM32CubeMX软件的 NVIC 管理中 TIM6 的中断就会被强制打开并且软件内不可关闭,但是同样可以修改优先级,如下图所示

同样可以在 stm32f4xx_it.c 文件中可以找到 TIM6 的中断回调函数 TIM6_DAC_IRQHandler() ,该函数调用了定时器的统一中断处理函数 HAL_TIM_IRQHandler() ,该函数根据使用的不同定时器功能最终调用不同的中断回调函数,这里读者只需要知道其调用了定时器周期回调函数 HAL_TIM_PeriodElapsedCallback() 即可,该函数由 STM32CubeMX 软件在 main.c 文件中自动生成,具体如下所示

void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
  /* USER CODE BEGIN Callback 0 */

  /* USER CODE END Callback 0 */
  if (htim->Instance == TIM6) {
    HAL_IncTick();
  }
  /* USER CODE BEGIN Callback 1 */

  /* USER CODE END Callback 1 */
}

从函数体内内容可以看出其原理与 “3.1.1.2、中断处理” 小节所叙述的一致,故此处不再赘述

3.2、FreeRTOS 基础时钟

STM32CubeMX 软件配置使用 FreeRTOS 时,默认将 SysTick 滴答定时器分配给 FreeRTOS 使用,因此如果 HAL 库的时基源也为 SysTick 时,在生成工程代码时软件就会警告用户:“当使用 RTOS 时,强烈建议使用除 Systick 之外的 HAL 时基源,可以从 SYS 下的 Pinout 选项卡更改 HAL 时基源”,具体如下图所示

因此在 STM32 需要使用 FreeRTOS 时,一般将 SysTick 分配给 FreeRTOS 使用,而 HAL 库的时基源一般选择除 SysTick 之外的定时器外设
,同时如果用户明确自己不需要使用 HAL 库的 HAL_Delay() 延时函数,则可以关闭 HAL 库的时基源

3.2.1、工作原理

FreeRTOS 的 SysTick 系统时基源是在 vPortSetupTimerInterrupt() 函数中被初始化的
,在该函数中有一个名为
configUSE_TICKLESS_IDLE
参数用于设置是否使用 Tickless 模式,这是 FreeRTOS 中提供的一种低功耗模式,将在后面小节介绍,其对 SysTick 的初始化是直接对 SysTick 的寄存器进行操作的,其调用流程如下图所示

SysTick 的寄存器可以阅读
Arm® Cortex®-M4 Processor Technical Reference Manual
手册 “4.1 System control registers” 小节,如下图所示

3.2.2、中断处理

当将 SysTick 分配给 FreeRTOS 初始化并开启对应中断之后,SysTick 的中断会被定义在 cmsis_os2.c 文件中(从 FreeRTOS_v10.3.1 之后),该函数清除了中断标志然后调用了 FreeRTOS 定义的硬件接口文件中的 xPortSysTickHandler() 函数,在 xPortSysTickHandler() 函数中增加了 RTOS 滴答定时器计数量,然后挂起 PednSV 中断,请求上下文切换,具体如下所述

根据上面的分析我们知道了一件关于 FreeRTOS 很重要的事情,也就是:
FreeRTOS 的任务调度发起是在系统滴答定时器中断中发起的,然后真正进行上下文切换处理是在 PendSV 中断中执行的

3.3、低功耗处理

3.3.1、睡眠、停止和待机模式

在 “
STM32CubeMX教程25 PWR 电源管理 - 睡眠、停止和待机模式
” 文章中曾经介绍了关于 STM32 电源管理的睡眠、停止和待机三种低功耗模式,在一个由 FreeRTOS 管理的系统中,一般只使用其中的睡眠模式即可,因为停止和待机模式的唤醒条件相对较为苛刻,感兴趣的读者请自行阅读上述文章

3.3.2、低功耗思路

在一个 FreeRTOS 管理的多任务系统中,当所有任务处理完毕进入阻塞状态等待下次处理时机时,空闲任务会一直执行,如果同时使能了
configUSE_IDLE_HOOK
参数,则每当处理器将要进入空闲任务时,就会先进入空闲任务钩子函数中

因此我们可以在空闲任务钩子函数中设置处理器进入睡眠模式,但是同时也会存在一个问题,就是每次滴答定时器中断都会将处理器唤醒,这样其运行时序图应该如下图所示

3.3.3、Tickless 模式

上述低功耗思路中存在的一个问题:
“每次滴答定时器中断都会将处理器唤醒”
,FreeRTOS 提供了一个 Tickless 模式,当处理器空闲时会一直处于睡眠状态,然后在任务即将退出阻塞状态之前处理器提前被唤醒,理想的低功耗模式应该如下图所示

要使用 Tickless 模式只需要启用
configUSE_TICKLESS_IDLE
参数即可

,该参数可以通过 STM32CubeMX 软件设置,有三个可以配置的选项,选择 Built in functionality enabled 对应的参数值为 1 ,表示使用 FreeRTOS 内建的函数实现 Tickless 低功耗功能,选择 User defined functionality enabled 则对应的参数值为 2 ,表示使用用户自定义的函数实现 Tickless 低功耗功能,一般选择使用 FreeRTOS 现成的函数来实现 Tickless 低功耗功能,如下图所示

3.3.3.1、工作原理

当启用 Tickless 之后,系统满足以下两点时就会自动进入睡眠模式

  1. 空闲任务正在运行
  2. 可运行低功耗的时间大于参数 configEXPECTED_IDLE_TIME_BEFORE_SLEEP 设定值时(默认为2)

用户需要注意的是进入睡眠的时间有最大值 xMaximumPossibleSuppressedTicks 限制
,该变量在设置滴答定时器中断 vPortSetupTimerInterrupt() 函数中被计算,当 MCU 频率为168MHz,FreeRTOS 频率为 1000Hz 时,该值为 99 ,也即单次最长进入睡眠时间的最大值为 99 个节拍,具体如下所示

3.3.3.2、vPortSuppressTicksAndSleep() 函数详解

vPortSuppressTicksAndSleep() 是 Tickless 模式实现的具体函数,该函数会在启用 Tickless 模式后在空闲任务中被调用,具体可以参考 “
freeRTOS 低功耗模式 和 空闲任务
” 文章

4、实验一:Tickless 模式的使用

4.1、实验目标

  1. 创建任务 Task_Main,在任务中实现 GREEN_LED 和 RED_LED 的闪烁程序
  2. 启用/关闭 Tickless 模式,对比两种不同情况下开发板的工作电流

4.2、CubeMX相关配置

首先读者应按照 "
FreeRTOS教程1 基础知识
" 章节配置一个可以正常编译通过的 FreeRTOS 空工程,然后在此空工程的基础上增加本实验所提出的要求

本实验需要初始化开发板上 GREEN_LED 和 RED_LED 两个 LED 灯作为显示,具体配置步骤请阅读“
STM32CubeMX教程2 GPIO输出 - 点亮LED灯
”,注意虽开发板不同但配置原理一致,如下图所示

单击 Middleware and Software Packs/FREERTOS ,在 Configuration 中单击 Tasks and Queues 选项卡,双击默认任务修改其参数,如下所示

然后在 Configuration 中单击 Config parameters 选项卡,在 Kernel settings 中找到 USE_TICKLESS_IDLE 参数,将其设置为 Disabled 或者 Built in functionality enabled,进行对比实验

最后配置 Clock Configuration 和 Project Manager 两个页面,接下来直接单击 GENERATE CODE 按钮生成工程代码即可

4.3、添加其他必要代码

首先实现任务 Task_Main 使其每隔 500ms 改变一次 GREEN_LED 和 RED_LED 的状态,具体如下所述

void AppTask_Main(void *argument)
{
	/* USER CODE BEGIN AppTask_Main */
	/* Infinite loop */
	for(;;)
	{
		HAL_GPIO_TogglePin(GREEN_LED_GPIO_Port, GREEN_LED_Pin);
		HAL_GPIO_TogglePin(RED_LED_GPIO_Port, RED_LED_Pin);
		vTaskDelay(pdMS_TO_TICKS(500));
	}
	/* USER CODE END AppTask_Main */
}

然后再进入睡眠模式之前关闭系统滴答定时器,在退出睡眠模式之后开启系统滴答定时器,具体如下所述

__weak void PreSleepProcessing(uint32_t ulExpectedIdleTime)
{
/* place for user code */
	HAL_SuspendTick();
}

__weak void PostSleepProcessing(uint32_t ulExpectedIdleTime)
{
/* place for user code */
	HAL_ResumeTick();
}

4.4、烧录验证

在开启 Tickless 和关闭 Tickless 两种模式下读者可以自行测试开发板工作电流,对比开启和关闭两种模式下工作电流的变化

参考资料

STM32Cube高效开发教程(基础篇)

Mastering_the_FreeRTOS_Real_Time_Kernel-A_Hands-On_Tutorial_Guide.pdf

前言

文本主要讲
MinimalApis
中的使用自定义
IResultModel
和系统自带
IResult
做响应返回值。
MinimalApis
支持以下类型的返回值:

  • string
    - 这包括
    Task<string>

    ValueTask<string>

  • T
    (任何其他类型)- 这包括
    Task<T>

    ValueTask<T>

  • 基于
    IResult
    - 这包括
    Task<IResult>

    ValueTask<IResult>

    本文的完整源代码在文末

string
返回值

行为 Content-Type
框架将字符串直接写入响应。 text/plain

200
状态代码与
text/plain
Content-Type
标头和以下内容一起返回

Hello World

T(任何其他类型)返回值

我们上面说的
自定义 IResultModel
就是用这种模式处理的

行为 Content-Type
框架 JSON 序列化响应。 application/json

MinimalApis
框架
Json
序列化全局配置如下

    //通过调用 ConfigureHttpJsonOptions 来全局配置应用的选项
    builder.Services.ConfigureHttpJsonOptions(options =>
    {
        options.SerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;//忽略循环引用
        options.SerializerOptions.WriteIndented = true;
        options.SerializerOptions.IncludeFields = true;
        options.SerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
    });

返回 T


app.MapGet("/TestT", User () => new User() { Name = "Ruipeng", Email = "xxx@163.com", Age = 18 })
   .WithSummary("测试类")
   .Produces<User>();

返回值

{
  "name": "Ruipeng",
  "email": "xxx@163.com",
  "age": 18
}

200
状态代码与
application/json
Content-Type
标头和以下内容一起返回

这个
HttpCode
状态码只能返回
200
,且不支持多种返回形式,比较局限


统一响应格式代码


public interface IResultModel
{
    /// <summary>
    ///     是否成功
    /// </summary>
    bool? IsSuccess { get; }

    /// <summary>
    ///     错误信息
    /// </summary>
    string? Message { get; }

    /// <summary>
    ///     业务码,用于业务中自定义
    /// </summary>
    int? StatusCode { get; set; }

    /// <summary>
    ///     时间戳
    /// </summary>
    long? Timestamp { get; }
}

/// <summary>
///     返回结果模型泛型接口
/// </summary>
/// <typeparam name="T"></typeparam>
public interface IResultModel<out T> : IResultModel
{
    /// <summary>
    ///     返回数据
    /// </summary>

    T? Data { get; }
}

实现

public class ResultModel<T> : IResultModel<T>
{
    public ResultModel()
    {
        Timestamp = DateTimeOffset.Now.ToUnixTimeSeconds();
    }

    /// <summary>
    ///     处理是否成功
    /// </summary>
    public bool? IsSuccess { get; set; }

    /// <summary>
    ///     错误信息
    /// </summary>
    public string? Message { get; set; }

    /// <summary>
    ///     业务码
    /// </summary>
    public int? StatusCode { get; set; }

    /// <summary>
    ///     时间戳
    /// </summary>
    public long? Timestamp { get; }

    /// <summary>
    ///     返回数据
    /// </summary>
    public T? Data { get; set; }


    /// <summary>
    ///     成功
    /// </summary>
    /// <param name="Data"></param>
    public ResultModel<T> Success(T? data = default)
    {
        this.IsSuccess = true;
        StatusCode = 200;
        Data = data;
        return this;
    }

    /// <summary>
    ///     失败
    /// </summary>
    /// <param name="msg">说明</param>
    /// <param name="code"></param>
    public ResultModel<T> Failed(string? msg = "failed", int? code = 500)
    {
        IsSuccess = false;
        Message = msg;
        StatusCode = code;
        return this;
    }
}

/// <summary>
///     返回结果
/// </summary>
public static class ResultModel
{
    /// <summary>
    ///     数据已存在
    /// </summary>
    /// <returns></returns>
    public static IResultModel<string> HasExists => Failed("data already exists");

    /// <summary>
    ///     数据不存在
    /// </summary>
    public static IResultModel<string> NotExists => Failed("data doesn't exist");

    /// <summary>
    ///     成功
    /// </summary>
    /// <param name="data">返回数据</param>
    /// <returns></returns>
    public static IResultModel<T> Success<T>(T? data = default)
    {
        return new ResultModel<T>().Success(data);
    }

    /// <summary>
    ///     成功
    /// </summary>
    /// <param name="task">任务</param>
    /// <returns></returns>
    public static async Task<IResultModel<T>> SuccessAsync<T>(Task<T>? task = default)
    {
        return task is not null && task != default ? new ResultModel<T>().Success(await task) : new ResultModel<T>();
    }

    /// <summary>
    ///     成功
    /// </summary>
    /// <returns></returns>
    public static IResultModel<string> Success()
    {
        return Success<string>();
    }


    /// <summary>
    ///     失败
    /// </summary>
    /// <param name="error">错误信息</param>
    /// <returns></returns>
    public static IResultModel<T> Failed<T>(string? error = null)
    {
        return new ResultModel<T>().Failed(error ?? "failed");
    }

    /// <summary>
    ///     失败
    /// </summary>
    /// <returns></returns>
    public static IResultModel<string> Failed(string? error = null)
    {
        return Failed<string>(error);
    }

    /// <summary>
    ///     根据布尔值返回结果
    /// </summary>
    /// <param name="success"></param>
    /// <returns></returns>
    public static IResultModel<T> Result<T>(bool success)
    {
        return success ? Success<T>() : Failed<T>();
    }

    /// <summary>
    ///     根据布尔值返回结果
    /// </summary>
    /// <param name="success"></param>
    /// <returns></returns>
    public static async Task<IResultModel> Result(Task<bool> success)
    {
        return await success ? Success() : Failed();
    }

    /// <summary>
    ///     根据布尔值返回结果
    /// </summary>
    /// <param name="success"></param>
    /// <returns></returns>
    public static IResultModel<string> Result(bool success)
    {
        return success ? Success() : Failed();
    }

    /// <summary>
    /// 时间戳起始日期
    /// </summary>
    public static readonly DateTime TimestampStart = new(1970, 1, 1, 0, 0, 0, 0);


}


定义接口

app.MapGet("/TestResultModel", IResultModel (int age) =>
{
    List<User> users = [new User() { Name = "Ruipeng", Email = "xxx@163.com", Age = 18 }];
    return users.FirstOrDefault(_ => _.Age > age) is User user ? ResultModel.Success(user) : ResultModel.Failed();
})
   .WithSummary("测试自定义IResultModel")
   .Produces<IResultModel<User>>();

封装了一个静态类来简化自定义类的创建,支持多个返回类型

返回值

{
  "isSuccess": true,
  "statusCode": 200,
  "timestamp": 1711001093,
  "data": {
    "name": "Ruipeng",
    "email": "xxx@163.com",
    "age": 18
  }

自定义类的自动包装实现

创建一个
Attribute

[AttributeUsage(AttributeTargets.Method)]
public class EnableResponseWrapperAttribute : Attribute { }

创建中间件自动包装

public class ResponseWrapperMiddleware(RequestDelegate next)
{
    public async Task InvokeAsync(HttpContext context)
    {

        if (context.GetEndpoint()?.Metadata.GetMetadata<EnableResponseWrapperAttribute>() is not null)
        {
            // 存储原始响应体流
            var originalResponseBodyStream = context.Response.Body;
            try
            {
                // 创建内存流以捕获响应
                using var memoryStream = new MemoryStream();
                context.Response.Body = memoryStream;

                // 调用管道中的下一个中间件
                await next(context);

                // 恢复原始响应体流并写入格式化结果
                context.Response.Body = originalResponseBodyStream;

                // 重置内存流位置并读取响应内容
                memoryStream.Seek(0, SeekOrigin.Begin);
                var readToEnd = await new StreamReader(memoryStream).ReadToEndAsync();
                var objResult = JsonSerializer.Deserialize<dynamic>(readToEnd);
                var result = new ResultModel<object>
                {
                    Data = objResult,
                    IsSuccess = true,
                    StatusCode = context.Response.StatusCode
                };
                await context.Response.WriteAsJsonAsync(result as object);

            }
            finally
            {
                // 确保在出现异常时恢复原始响应体流
                context.Response.Body = originalResponseBodyStream;
            }
        }
        else
        {
            await next(context);
        }
    }
}

应用中间件

app.UseMiddleware<ResponseWrapperMiddleware>();

创建测试接口

app.MapGet("/TestTestAutoWarpper", [EnableResponseWrapper] User () => new User() { Name = "Ruipeng", Email = "xxx@163.com", Age = 18 }).WithSummary("测试类")
   .Produces<User>();

返回值

{
  "isSuccess": true,
  "statusCode": 200,
  "timestamp": 1711005201,
  "data": {
    "name": "Ruipeng",
    "email": "xxx@163.com",
    "age": 18
  }
}

为了方便测试在
MinimalApis
的接口上如果添加了
EnableResponseWrapperAttribute
则通过中间件自动包装返回值

IResult 返回值

行为 Content-Type
框架调用 IResult.ExecuteAsync 由 IResult 实现决定


dotNet7
之后多了一个
TypedResults
类来替代
Results

IResult
接口定义一个表示
HTTP
终结点结果的协定。 静态
Results
类和静态
TypedResults
用于创建表示不同类型的响应的各种
IResult
对象。

返回 TypedResults(而不是 Results)有以下优点:

  • TypedResults
    帮助程序返回强类型对象,这可以提高代码可读性、改进单元测试并减少运行时错误的可能性。
  • 实现类型会自动为
    OpenAPI
    提供响应类型元数据来描述终结点。
    实现在
    Microsoft.AspNetCore.Http.HttpResults
//Return IResult
app.MapGet("/IResult/TestResult", IResult () => Results.Ok(new User() { Name = "Ruipeng", Email = "xxx@163.com", Age = 18 }));

没有调用扩展方法
Produces

app.MapGet("/IResult/TestTypedResult", IResult () => TypedResults.Ok(new User() { Name = "Ruipeng", Email = "xxx@163.com", Age = 18 }));

可以看到 TypedResults 默认就会添加路由终结点的元数据描述

返回多个 IResult 实现类型

app.MapGet("/IResult/ReturnMultipleTypes", Results<Ok<User>, NotFound> (int age) =>
{
    List<User> users = [new User() { Name = "Ruipeng", Email = "xxx@163.com", Age = 18 }];
    return users.FirstOrDefault(_ => _.Age > age) is User user ? TypedResults.Ok(user) : TypedResults.NotFound();
});

图简单可以直接用
IResult
返回类型 但是,由于
TypedResults
帮助程序自动包含终结点的元数据,因此可以改为返回
Results<Ok<User>, NotFound>
联合类型

IResult 自定义响应

添加 Html 扩展


public static class ResultsExtensions
{
    public static IResult Html(this IResultExtensions resultExtensions, string html)
    {
        ArgumentNullException.ThrowIfNull(resultExtensions);

        return new HtmlResult(html);
    }
}


class HtmlResult(string html) : IResult
{
    private readonly string _html = html;

    public Task ExecuteAsync(HttpContext httpContext)
    {
        httpContext.Response.ContentType = MediaTypeNames.Text.Html;
        httpContext.Response.ContentLength = Encoding.UTF8.GetByteCount(_html);
        return httpContext.Response.WriteAsync(_html);
    }
}
app.MapGet("/IResult/Html", () => Results.Extensions.Html(@$"<!doctype html>
<html>
    <head><title>miniHTML</title></head>
    <body>
        <h1>Hello World</h1>
        <p>The time on the server is {DateTime.Now:O}</p>
    </body>
</html>"));

返回结果

<!DOCTYPE html>
<html>
  <head>
    <title>miniHTML</title>
  </head>
  <body>
    <h1>Hello World</h1>
    <p>The time on the server is 2024-03-21T17:31:36.2931959+08:00</p>
  </body>
</html>

自定义 Json 格式

上面写了
ConfigureHttpJsonOptions
方法来配置全局请求的 Json 格式,下面则是针对单个路由终结点请求,方便一些个性化接口的处理

var options = new JsonSerializerOptions(JsonSerializerDefaults.Web)
{ WriteIndented = true };

app.MapGet("/IResult/CustomJsonConfig", () =>
    TypedResults.Json(new User() { Name = "Ruipeng", Email = "xxx@163.com", Age = 18 }, options));

返回 ProblemDetail

app.MapGet("/IResult/ProblemDetail", () =>
{
    var problemDetail = new ProblemDetails()
    {
        Status = StatusCodes.Status500InternalServerError,
        Title = "内部错误"
    };
    return TypedResults.Problem(problemDetail);
});

返回值

{
  "type": "https://tools.ietf.org/html/rfc9110#section-15.6.1",
  "title": "内部错误",
  "status": 500
}


Microsoft.AspNetCore.Http.Results
的扩展下,TypedResults 有非常多扩展的方法,比如处理文件,回调,流以及登录认证等,大家可以根据需求使用.

最后

用那种方式还是取决于项目的实际情况,如果你的系统是
业务码

httpStateCode
要求分离的形式那建议用上面自定义统一响应的形式,要是没这方面的需求那
dotNet
自带的
TypedResults
使用起来就更合适。

官网文档
如何在最小 API 应用中创建响应

以下是本文的完整
源代码

希望本文对你有帮助!

为了优化我们公司网站的性能,我最近引入了浏览器预加载技术(Preload)。

这项技术可以显著
减少级联
情况,提高资源加载的
并行度
,从而加速网站的加载速度。

Preload的原理

Preload的原理是在浏览器解析HTML文档时,
提前加载
页面所需的关键资源,如样式表、脚本文件和字体等。

通过预加载这些关键资源,浏览器能够在页面加载时更快地获取所需资源,从而加速页面的渲染过程。下面是一个简单的预加载示例代码:

<!DOCTYPE html>
<htmllang="en">
<head>
    <metacharset="UTF-8">
    <metaname="viewport"content="width=device-width, initial-scale=1.0">
    <title>Optimized Website with Preload</title>
    
    <!--Preload CSS-->
    <linkrel="preload"href="styles.css"as="style">
    
    <!--Preload JavaScript-->
    <linkrel="preload"href="script.js"as="script">
    
    <!--Preload font-->
    <linkrel="preload"href="font.woff2"as="font"type="font/woff2">
    
    <!--Normal CSS-->
    <linkrel="stylesheet"href="styles.css">
</head>
<body>
    <!--Content of the webpage-->
    
    <!--Normal JavaScript-->
    <scriptsrc="script.js"></script>
</body>
</html>

crossorigin属性和as属性的可选值

crossorigin属性:该属性用于指定资源的跨域设置。可选值包括:

  • anonymous:表示资源会以匿名身份请求,不会包含凭据信息(如 cookies、HTTP 认证等)。通常用于不需要用户身份验证的公共资源。
  • use-credentials:表示资源会以凭据身份请求,浏览器会发送包含凭据信息的请求。适用于需要用户身份验证的私有资源。

as属性:该属性用于指定资源的类型。可选值包括:

  • audio:音频文件
  • document:HTML 文档
  • font:字体文件
  • image:图像文件
  • script:JavaScript 文件
  • style:样式表文件
  • video:视频文件
  • fetch:其他类型的网络请求

正确属性的重要性

如果设置错误的crossorigin和as属性,将导致预加载失效。

例如,如果预加载的资源是跨域的而没有设置正确的crossorigin,浏览器可能会拒绝加载该资源。

同样,如果as属性设置错误,告诉浏览器预加载的资源类型与实际类型不符,也会导致预加载失效。

效果

下面是优化前后的对比,可以看到优化后的并行度提升了很多