news 2026/9/15 21:55:53

UDS刷写日志离线分析:从CAN帧到NRC定位的工程实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
UDS刷写日志离线分析:从CAN帧到NRC定位的工程实践

前段时间同事扔给我一份刷写日志,整整 4 万多行,让我帮忙看下为什么刷到 60% 的时候整车控制器报错。我一开始也头大:文件里全是十六进制字节,CAN ID 混在一起,乍一看就是天书。后来把 ISO-TP 层和 UDS 层一层层剥开,用我自己维护的一款开源 UDS/ISO-TP 刷写日志离线分析工具跑了一遍,几十秒内就定位到了一条 0x31 例程控制返回 0x72 的负响应。这件事让我想认真聊聊这类工具背后的分析方法和实现思路。

所谓刷写日志离线分析工具,就是不依赖诊断仪、不连接实车,纯粹通过解析抓取的 CAN 总线日志文件,把复杂的刷写过程还原成人类能看懂的时间线、服务序列和错误码报告。它适合三类人:做嵌入式 Bootloader 开发的工程师、负责产线 EOL 或售后刷写问题的测试工程师,以及经常要帮客户排查刷写失败问题的技术支持。接下来我会从协议分层、解析实现、时序重建、工具选型到实战避坑,把整个思路完整讲一遍。

1. 刷写日志到底难在哪:先搞清楚我们要解决什么问题

1.1 刷写不是一个“传文件”动作

很多人以为 ECU 刷写就是把一个 bin 文件通过总线发过去,像 U 盘拷贝一样。实际上,一次完整的 UDS 刷写是一个有状态的多阶段会话:先切换会话模式,关闭相关通信,确认编程条件,然后解锁安全等级,写入指纹信息,通过 34/36/37 服务传输数据,最后执行例程校验并复位 ECU。任何一个环节失败,都会导致整个刷写流程中断。

理解这一点非常重要,因为离线分析工具的解析逻辑完全围绕刷写阶段来组织。如果你只盯着数据帧,不看前后状态,就无法判断一条 0x7F 负响应到底是安全等级不够,还是数据校验失败,亦或是服务不被支持。所以我在设计工具时,第一件事不是写解析器,而是把刷写流程拆成可识别的事件序列,然后让每个 UDS 服务都映射到对应的阶段和状态。

1.2 日志文件形态各异,统一格式是第一道坎

实际工作中拿到的日志远不止一种格式。Vector 的 ASC 和 BLF 最常见,但不同的采集设备导出的 ASC 文件字段顺序都可能不一样;PCAN 导出的是 CSV 或 TXT;还有一些 OEM 的工具会输出带时间戳的私有文本格式,甚至把多个 ECU 的日志混在一个文件里。

这就带来一个很现实的问题:分析工具不能只支持一种格式。我在项目里做了一层格式抽象,把 ASC、CSV、TXT 甚至部分 BLF 都统一转换成内部 RawFrame 对象,每个对象只保留时间戳、通道、CAN ID、数据长度和数据字节。后续的 ISO-TP 和 UDS 解析完全不关心原始文件来自哪里,这样每新增一种日志格式,只需要写一个很小的 parser。

1.3 离线分析的真正价值:可复现、可批量、可自动化

在线诊断工具当然也能看服务响应,但它要求实车、诊断仪、供电环境都就位,出了问题还得现场抓包,效率很低。离线分析工具的价值在于,你可以把现场抓到的日志拿回来慢慢看,定位问题后再回到环境里复现;也可以一次性批量扫描几十份日志,把 NRC 出现频次、刷写时长、失败阶段统计出来,形成自动化回归测试。

我在实际项目中就遇到过这样的情况:同一款 ECU,产线上偶尔出现刷写失败,但问题不是必现的。用离线工具批跑了一周积累的日志,才发现失败总是集中在某个特定的软件版本里,而且都发生在 34 服务扩展地址之后。这个结论在线下反复测试都复现不出来,但离线分析给了很明确的指向。

2. ISO-TP 层:把裸总线字节流还原成完整报文

2.1 为什么绕不开分段机制

标准 CAN 数据帧的数据场最长只有 8 字节,而实际有效负载还要去掉 1 字节的 PCI(协议控制信息),所以单帧最多携带 7 字节应用数据。可 UDS 的服务动辄几十上百字节,比如 34 服务请求带地址和长度信息,36 服务一次只能发 7 字节,0x31 例程控制的参数也可能超过 7 字节。所以 ISO-TP(ISO 15765-2)必须把长报文拆成多帧传输。

很多做应用层的人容易忽略这一点:刷写日志里看到的一连串相同 CAN ID 的帧,并不是重复报文,而是同一个 UDS 消息的分段。如果不按照 ISO-TP 的规则重组,你看到的就是一堆无法解读的碎片。离线分析工具的第一层核心任务,就是把这些碎片重新拼成完整的 ISO-TP 报文。

ISO-TP 定义了四种帧类型:单帧(SF)、首帧(FF)、流控帧(FC)和连续帧(CF)。单帧里 1 字节 PCI 高四位为 0,剩余 7 字节直接承载应用数据;首帧 PCI 高四位为 1,后跟 12 位总长度,最多表示 4095 字节的大报文;流控帧由接收方发出,用于告诉发送方“我准备好了,你可以继续发”,其中包含块大小(BS)和最小间隔时间(STmin);连续帧则按顺序携带剩余数据。

2.2 重组算法:不能只按 CAN ID 简单拼接

重组 ISO-TP 报文的核心逻辑不复杂,但细节很多。每个逻辑连接通常由源地址、目标地址和地址模式(物理寻址或功能寻址)共同标识。收到首帧后,创建重组缓冲区,记录期望的总长度;收到连续帧后,按顺序填入缓冲区,并校验帧序号。

这里最容易被坑的是连续帧的序号。连续帧 PCI 的低四位是计数器,从 1 开始,循环使用 0~15;也就是说 1、2、3...15、0、1、2...这样递增。如果解析器用简单的累加,遇到回绕就会出错。另外,流控帧的 BS 表示发送方最多连续发送多少个 CF 后要等待新的流控帧;STmin 表示两个连续帧之间的最小间隔时间。离线分析时虽然不需要真正控制节奏,但要解析这些参数,因为刷写工具经常因为流控参数配置不合理导致总线拥塞或超时。

我在工具里实现了一个IsoTpReassembler类,以 (channel, tx_id, rx_id) 为 key 维护多个重组上下文。收到 SF 直接产出消息;收到 FF 则初始化缓冲区;收到 FC 更新发送状态;收到 CF 填充数据,填满后按照协议计算完整 payload 并产出。每个上下文还记录首帧时间戳和最后一片时间戳,方便后续算传输耗时。

2.3 日志时间戳:微秒还是毫秒,差距很大

CAN 日志的时间戳通常来自抓包设备的硬件时钟,精度可以到微秒,但不同工具导出的单位不一样。ASC 文件一般是秒和微秒分开的字段,CSV 可能是浮点秒,有的厂商文本格式竟然是毫秒。如果解析器把单位搞错,重组的时序会彻底乱掉,尤其影响流控超时判断。

我的做法是在格式解析阶段就把所有时间戳统一转换成微秒整型,并保留硬件时间戳与系统时间戳的映射。后面做超时分析、时序重建时,全部基于微秒计算。建议大家在写解析器时,任何时间相关字段都显式标注单位,不要用无单位浮点数,否则后续维护一定会踩坑。

3. UDS 服务层翻译:从 SID 到可读诊断动作

3.1 刷写相关的核心服务

ISO-TP 重组出来的是一个个完整的 UDS 消息,接下来要做的是解析 SID、子功能、数据参数,并翻译成可读的诊断动作。UDS 服务号很多,但刷写场景里高频出现的其实就十几个。

SID服务名称刷写中的典型用途
0x10诊断会话控制切换默认/编程/扩展会话
0x11ECU 复位刷写完成后复位
0x14清除诊断信息刷写前清 DTC
0x19读取诊断信息读 DTC 快照
0x22按 ID 读数据读软件版本/硬件版本
0x27安全访问种子与密钥解锁
0x28通信控制关闭/开启通信
0x2E按 ID 写数据写 VIN、写指纹
0x31例程控制启动编程条件检查、校验
0x34请求下载开始数据传输
0x36传输数据按块传输固件内容
0x37请求传输退出结束数据传输
0x3E测试器在线保持会话活跃
0x85控制 DTC 设置刷写期间关闭 DTC

表格只是索引,真正重要的是理解服务之间的依赖关系。比如 34 服务必须在编程会话下,通常还要先通过 27 服务解锁,否则 ECU 会返回 0x33(安全访问被拒绝)。离线分析工具会把每个服务的响应状态记录下来,形成服务链,方便快速定位是哪一环断了。

3.2 负响应结构和 NRC 解读

当 ECU 不认可某个请求时,会返回 0x7F + 请求的 SID + 1 字节 NRC。NRC 是刷写失败分析里最有价值的信息。很多新手只看“返回 7F 就是失败”,但没去查 NRC 具体含义,导致问题定位不准。

NRC含义常见触发场景
0x10一般拒绝请求条件不满足
0x12子功能不支持会话模式不对
0x13消息长度错误参数数量或格式不对
0x22条件不满足未满足刷写前置条件
0x24请求序列错误没走正常顺序
0x31请求超出范围地址、长度超出 Flash 范围
0x33安全访问被拒绝密钥错误或未解锁
0x35密钥无效安全等级不匹配
0x36超过重试次数解锁尝试次数过多
0x37请求时间延迟超时超时未收到后续请求
0x70上传下载不可接受编程会话未激活
0x71传输数据暂停接收缓冲区满
0x72编程错误Flash 擦写失败、校验失败
0x73块序号错误36 服务块计数不对
0x78请求被接收但仍在处理需要等待后续响应

0x72 是刷写日志里最常见的失败码之一,也是最容易被误判的。它可能表示 Flash 驱动问题、写入地址越界、校验失败、甚至供电不稳。单看 NRC 很难确定根因,但配合前后文就能缩小范围:如果 0x72 出现在 36 服务发送了大量数据之后,大概率是写入或校验阶段出错;如果出现在 31 服务启动例程时,可能是例程执行超时或依赖条件不满足。

3.3 解析服务参数时的细节

每个服务都有各自的参数布局,解析时不能只看 SID。比如 34 服务请求的格式是34 + 数据格式标识符 + 地址与长度格式标识符 + 内存地址 + 内存大小,地址和长度的字节数是根据格式标识符动态解析的。OEM 变体很多,有的是 4 字节地址、4 字节长度,有的是 2 字节地址、2 字节长度。如果工具写死了偏移位置,换一个 ECU 就会报错。

36 服务则是36 + 块序列计数器 + 数据块,块计数从 1 开始,每块加 1。我在解析时会把块序号转换成长度信息,并且检查序号是否连续。如果日志中出现跳号,往往说明有丢帧或发送方逻辑异常。

另外,0x2E 写数据服务经常被用来写指纹信息,其2E + DID + 数据的格式中 DID 是两字节,不同 OEM 对 DID 的定义完全不同。离线工具可以提供 DID 映射配置,让用户自己定义哪些 DID 代表软件版本、硬件版本、序列号、刷写时间等,这样报告里就能直接显示可读信息。

4. 时序重建:把几十万行日志拼成一眼能看懂的刷写流程

4.1 按服务特征识别刷写阶段

重组并翻译出所有 UDS 消息后,下一步是重建刷写流程的时序。这里我用的是“阶段识别”的思路:根据服务特征和状态机,把整个刷写过程切成预编程、编程、后编程三个阶段,然后进一步识别出每次会话切换、安全解锁、数据传输等关键事件。

预编程阶段通常包含 10 01(切到默认会话)、10 03(切到扩展会话)、85 02(关闭 DTC)、28 03(关闭非诊断通信)、3E 00(保持在线)等。进入编程阶段前会连续出现 27 服务的安全访问交互,之后是 2E 写指纹、34/36/37 传输固件、31 01 02 03 等例程控制。后编程阶段则是 10 02(切到编程会话后的复位)或 10 01(回默认会话)、14 清 DTC、19 读 DTC、11 01 复位等。

工具识别出这些阶段后,会生成一张时间线,把每个阶段用不同的标记展示。比如预编程是灰色,传输数据是蓝色,例程校验是橙色,负响应是红色。一旦刷写失败,红色的 7F 节点会非常显眼,基本可以做到“一眼定位”。

4.2 服务链与超时窗口

单纯按时间排 UDS 消息还不够,还需要把请求和响应配对,形成服务链。物理寻址通常是请求 0x7E0、响应 0x7E8 这种一对一的配对关系;功能寻址则是请求发到 0x7DF,多个 ECU 各自响应,这种情况下不能按“收到一个响应就匹配完成”来处理,而应该等待一段窗口期。

超时窗口的判断也很关键。UDS 协议里,ECU 收到请求后默认要在 50ms 内给出响应,某些慢服务允许扩展延迟,响应前会先发 0x7F + SID + 0x78(请求被接收但仍在处理)。离线分析工具要识别这种 0x78 中间响应,并在最终响应之前不判定超时。很多第三方工具在这里做得不好,把 0x78 当成普通负响应,导致刷写流程分析错误。

我在实现阶段识别时,维护了一个状态机:当前会话模式、安全等级、传输状态(是否已请求下载、当前块计数)、例程状态。每收到一条 UDS 请求或响应,就根据状态机更新状态并打上阶段标签。这样即便日志里包含多个 ECU 的消息,也能按逻辑连接分别维护各自的状态机。

4.3 失败归因:从 NRC 往前推 100ms

定位刷写失败有一套很实用的经验法则:先找到最近一条 0x7F 负响应,确认 NRC;然后往前找 100ms 到 500ms 窗口内的所有相关帧,看失败前最后一条成功的服务是什么;再判断是该服务本身失败,还是前序服务没完成导致条件不满足。

举个例子,有一次日志里反复出现31 01 02 03启动例程返回 0x22(条件不满足),从表面看是例程没通过。但往前翻了几百条记录,发现刷写工具根本没发 27 服务的安全解锁请求,只是在广播会话切换后直接尝试启动例程。这种情况下 NRC 是 0x22 而不是 0x33,很容易误导人。工具如果能把“安全等级状态”自动关联进报告,就能直接提示“当前安全等级未解锁,不建议启动该例程”。这类关联分析才是离线工具的真正价值。

5. 开源工具链选型:不是每个环节都要自己造轮子

5.1 CAN 日志解析层的选择

开源社区里已经有不少现成的轮子,没必要全部从零写。CAN 日志解析层面,can-utils提供了candump等命令行工具,但它主要面向在线抓取和简单文本输出;Wireshark自带的 ISO-TP dissector 可以解析 ASC 和 pcap 文件,还能图形化查看 ISO-TP 重组,但批量分析和生成自定义报告的能力比较弱。

我在项目里最初尝试过直接调 Wireshark 的tshark命令行导出解析结果,优点是省力,缺点是日志格式兼容性受限于 Wireshark 的 dissector,而且对私有格式的支持很差。后来决定自己写解析层,但参考了 Wireshark 的协议字段设计思路,保证后续可以对接。如果你想快速验证一段日志内容,用tshark -r file.asc -V看 ISO-TP 的解码结果是很好的起步方式,但如果你想做自动化的批量分析工具,还是需要自建一层解析。

5.2 Python 生态:python-can 与 udsoncan 的边界

python-can是 Python 生态里最常用的 CAN 工具库,支持读取 ASC、BLF 等格式,也支持多种硬件接口。但要注意,python-can的 BLF 读取能力依赖canio或者原生支持,某些老的日志版本会读取失败。我建议将它作为日志读取的“可选后端”之一,而不是唯一路径。

udsoncan是一个非常完整的 UDS 在线诊断库,支持发包、收包、服务封装,但它针对的是在线交互场景,并没有为离线日志分析做设计。你不能直接拿udsoncan去解析一份日志文件,因为它默认所有服务都是一问一答的在线对话。不过可以借鉴它对服务的参数定义和编解码逻辑,把其中的消息类抽出来复用,避免自己重写一套 34/36/37 服务的参数编解码。

我的最终架构是:格式解析层用自研代码加python-can兜底;ISO-TP 重组层完全自研;UDS 服务编解码参考udsoncan的参数定义;报告生成层用纯 Python 生成 JSON、CSV 和独立 HTML 文件。整个项目以 CLI 方式运行,也提供了一组可导入的 Python API,方便接入其他自动化测试框架。

5.3 中间数据模型的设计

离线分析工具最容易犯的错误,是把“日志格式解析”和“业务分析”耦合在一起。如果 RawFrame 直接塞给报告模块,后面每扩展一种日志格式或分析方法都会牵一发动全身。我设计了四层数据模型:

  • RawFrame:原始帧,包含时间戳、通道、CAN ID、数据。
  • IsoTpMessage:重组后的完整消息,包含源地址、目标地址、时间戳、payload。
  • UdsMessage:UDS 层的请求或响应,包含 SID、子功能、参数解析结果、NRC。
  • SessionEvent:刷写阶段和高层语义事件,包含阶段标签、状态机快照、关联 UDS 消息。

每一层只消费上一层的输出。比如 UDS 层不关心原始 CAN 帧的 DLC 是多少,ISO-TP 层不关心 SID 的含义。这样每一层都可以独立测试,也方便别人基于这个项目扩展自己的分析规则。

5.4 二次开发的扩展点

开源工具最终能不能被大家用起来,取决于扩展点设计得好不好。我在项目里预留了几个关键扩展点:日志格式解析器注册表、DID 映射表、NRC 补充说明字典、服务序列阶段识别规则。每一个都是简单的 Python 字典或函数注册,用户加一个厂商私有协议时,不用改动核心代码,只要新增一个解析规则文件就行。

举个例子,某厂商在 34 服务之前多加了一个 2E 写刷写使能标志的步骤,未做使能直接发 34 会返回 0x24。默认的阶段识别规则识别不到这个私有服务,但通过配置,用户可以把该 2E 请求标记为“刷写使能”,并设定它必须在 34 服务之前完成。这样报告里就能自动标出“刷写使能未设置”这类提示。

6. 实测避坑录:我在解析真实刷写日志时踩过的坑

6.1 时间戳单位混用导致时序乱掉

我第一次拿真实 BLF 日志做回归测试时,发现报告的时序完全对不上,有些响应竟然出现在请求之前。排查了半天,问题出在 BLF 内部的 time stamp 是以 10 纳秒为单位的 tick,我用脚本读取时当成微秒直接除以 1000,导致所有时间戳缩放了 100 倍。后来我统一在格式解析层做一次时间基准归一化,并且针对不同格式分别写单测。

这件事给我的教训是:任何来源的日志都要先打印前几帧的时间戳间隔,肉眼确认数量级是否合理。比如两个连续 CAN 帧之间通常间隔几毫秒,如果看到间隔是几微秒或几百秒,大概率是单位解析错了。

6.2 多 ECU 日志混杂与同一 CAN ID 不同节点的干扰

产线日志经常是一个文件包含多个 ECU 甚至多个通道的数据,不同 ECU 可能使用相同的 CAN ID 范围,尤其是在功能寻址广播时,一个请求会带出好几个响应。如果解析器只按 CAN ID 分组,必然会把不同节点的数据混在一起。

解决思路是引入“逻辑节点”概念:每个 ECU 节点由通道号和诊断物理请求/响应 ID 对唯一标识。比如节点 A 是ch1 0x7E0/0x7E8,节点 B 是ch2 0x7E0/0x7E8,虽然 CAN ID 相同,但通道不同,视为不同节点。功能寻址响应则通过响应 ID 的不同来区分节点。这样时间线就能按节点分层展示,避免互相干扰。

6.3 协议变体和私有服务的兼容

不同 OEM 的刷写时序差异非常大,有的在 34 服务前需要先发特定 DID 长度信息,有的把 31 例程的例程 ID 定义成私有值。如果你的工具只按标准协议解析,碰到私有服务就会漏报或误报。最稳妥的做法是:未知服务不强行解析,只输出原始 hex 和 SID 号,同时标出“未知服务”,并允许用户通过配置补充说明。

我在项目里增加了“协议配置文件”机制,用户可以针对特定 ECU 定义一个 JSON,里面写明私有 DID、私有例程 ID、允许的服务序列等。分析报告会优先使用配置文件里的解释,未匹配的才回退到标准定义。实测下来,这种方式能覆盖绝大多数 OEM 变体,同时保持核心代码简洁。

6.4 性能优化与超大日志处理

一份完整的产线刷写日志动辄几万帧,有些使用 CAN FD 加高波特率抓取的文件甚至超过 20 万帧。如果解析时把全部帧一次性载入内存再做重组,内存占用会非常夸张,而且分析速度慢得让人抓狂。

我的优化思路是:使用生成器按帧读取,边读边喂给 ISO-TP 重组器;重组器完成一个完整消息就立刻产出,交给 UDS 解析器处理,处理完的消息直接写入中间结果文件。这样任何时刻内存里只保留当前正在重组的少量上下文,几万帧的日志处理时间可以压到几秒内。另外,多通道日志可以用多进程按通道并行解析,最后再合并报告。

时间戳排序也是一个隐藏性能点。某些日志文件里帧并不严格按时间递增,比如导出工具合并多个通道时会出现小范围的乱序。我做分析前会先做一次稳定排序,同时对时间戳做去重和单调化处理,避免报告中出现类似“上一帧时间晚于下一帧”的诡异现象。

6.5 异常流控和丢帧场景

离线日志里经常能看到只有请求没有响应的情况,原因可能是总线丢帧、ECU 掉电、或者日志采集本身丢包。分析工具不能遇到这种场景就崩溃,而应该明确标记为“无响应”并继续解析后续内容。另外,流控帧缺失但连续帧仍然出现的情况也见过,这通常说明抓包工具漏采了部分帧,重组器要能容忍这种缺失并给出警告,而不是静默丢弃。

这类异常场景正是离线分析工具比人肉看日志强的地方:它能自动统计丢帧率、缺失响应数量、超时次数,然后在报告开头给一个健康度总览。我后来给项目加了一个“异常摘要”模块,专门把这些问题用列表打出来,测试同事看到摘要就能决定是否重新抓包,省下大量沟通时间。

说句实在话,这个工具本身并不复杂,最难的始终是把各家的日志格式、私有协议和千奇百怪的刷写时序都兼容进来。我后来把常见日志格式的解析器做成了插件式,谁在项目中遇到新格式,按模板补一个解析函数就能解决。如果你也想做类似的工具,我建议不要急着堆功能,先把 ISO-TP 重组和 NRC 关联这两块做扎实,再逐步扩展厂商私有协议。

最后再分享一个小技巧:分析刷写失败日志,永远先看最近一条 0x7F,然后再看它前面 100ms 内到底发生了什么。大部分刷写失败都不是“突然失败”,而是前置条件没满足。把这条经验写成规则放进工具里,比任何复杂的算法都管用。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 21:53:16

C#图标管理系统:可编译、可继承、可主题化的桌面UI资产方案

简介:这是一份面向.NET开发者(尤其是WinForm、Web项目初学者与中级工程师)的C#图标资源库,解决UI开发中图标素材匮乏、尺寸适配繁琐、调用封装不统一等常见问题。资源包含3800个专业设计的1616与3232像素PNG图标,全部由…

作者头像 李华
网站建设 2026/9/15 21:52:19

抖音去水印下载完整指南:5 步跑通无水印批量下载

抖音去水印下载完整指南:5 步跑通无水印批量下载 【免费下载链接】douyin-downloader A practical Douyin downloader for both single-item and profile batch downloads, with progress display, retries, SQLite deduplication, and browser fallback support. 抖…

作者头像 李华
网站建设 2026/9/15 21:51:24

基于S7-200 PLC的三泵变频恒压供水系统设计与PID调试实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/15 21:50:20

移动端渲染发热优化:纹理压缩与后处理带宽的减负实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/15 21:49:59

Loop 径向菜单窗口管理:一个按键让 macOS 窗口各就各位

Loop 径向菜单窗口管理:一个按键让 macOS 窗口各就各位 【免费下载链接】Loop Window management made elegant. 项目地址: https://gitcode.com/GitHub_Trending/lo/Loop 如果你受够了在 macOS 上手动拖动、缩放每一个窗口,开源应用 Loop 可能正…

作者头像 李华