:用 kqueue 捕获每张新截图,ScreenshotWatcher 监听原理全解)
Tendedero 源码剖析一用 kqueue 捕获每张新截图ScreenshotWatcher 监听原理全解【免费下载链接】tendederoScreenshots, hung out to dry. A tiny native macOS app that hangs every screenshot on a line at the top of your screen.项目地址: https://gitcode.com/gh_mirrors/te/tendederoTendedero 是一个免费开源的 macOS 小工具你截取的每一张截图都会自动挂到屏幕顶部的晾衣绳上。它从不自己截屏而是靠 ScreenshotWatcher.swift 中的ScreenshotWatcher类用 macOS 的 kqueue 文件系统事件监听截图保存目录实时捡起每一张新截图。本文带你逐行看懂这套截图监听的完整原理。先看看它长什么样下面这张图展示了 Tendedero 的核心交互光标停在菜单栏附近晾衣绳就带着挂着的截图滑落下来点击可复制拖到文件夹可保存离开即收起。挂上去之后能做什么点一下复制按住打开系统 Markup 标注拖进 App 发送副本拖进文件夹保存——所有截图都在本地处理没有网络、没有账号。核心在哪ScreenshotWatcher.swift 的定位整个项目的源码在 Sources/Tendedero/ 下按文件分工很清晰完整的文件职责表见 README.md 的 Inside the app 小节。与本文主题直接相关的只有三处文件职责ScreenshotWatcher.swift监听截图目录发现新截图就回调onNewInbox.swift把系统截图保存位置改到 Tendedero 自己的文件夹收件箱模式AppDelegate.swift创建并启动监视器把新截图挂上绳ScreenshotWatcher全文只有一百多行但设计非常讲究。它的设计哲学写在文件开头的注释里Tendedero 永远不自己截屏——你继续用 CmdShift3/4或者用 CleanShot 之类的任何工具它只管捡结果。第一步找到截图到底保存在哪监视之前得先知道目录。screenshotFolder()不猜、不硬编码而是直接读系统设置先通过CFPreferencesAppSynchronize同步com.apple.screencapture偏好域确保读到最新值ScreenshotWatcher.swift#L29-L43依次尝试两个键macOS 27 起叫location-screenshot更早版本叫location——这两个键正好对应 CmdShift5 里选项 → 存储位置的设置读到的路径有效就返回否则回退到桌面这样做的妙处用户在系统设置里改了截图位置Tendedero 下次启动就自动跟上无需任何配置界面。第二步kqueue 目录监听原理真正的监听逻辑在start()方法里ScreenshotWatcher.swift#L50-L68拆开看是四步1️⃣ 打开目录的文件描述符。关键参数是O_EVTONLY——这是 kqueue 专用的标志位表示这个 fd 只用来收事件不用来读写数据。2️⃣ 创建 GCD 文件监视源。核心一行DispatchSource.makeFileSystemObjectSource( fileDescriptor: fd, eventMask: [.write, .rename, .delete], queue: .main)DispatchSource.makeFileSystemObjectSource是 GCD 对 kqueue 文件事件的高层封装它在底层向内核 kqueue 注册了这个目录的NOTE_WRITE、NOTE_RENAME、NOTE_DELETE三种事件。目录内任何文件的创建、写入、改名、删除都会唤醒监视源。3️⃣ 事件触发后不立即处理而是预约一次扫描见下文。4️⃣ 注册取消回调。setCancelHandler { close(fd) }保证监视停止时文件描述符一定被释放不留资源泄漏。为什么监听目录而不是监听某个具体文件因为每次截图的文件名都是全新的截屏 2026-10-09 16.05.34.png……根本不知道下一个文件叫什么名字而目录级事件能捕获一切变化是最稳妥的监听粒度。第三步0.2 秒防抖为什么不能收到事件就处理事件回调并不直接扫描目录而是走scheduleScan()ScreenshotWatcher.swift#L76-L82DispatchQueue.main.asyncAfter(deadline: .now() 0.2, execute: work)每次事件触发都会先取消上一次还没执行的扫描再延迟 0.2 秒重新预约。这是典型的防抖而且注释点明了原因macOS 截图是先写一个隐藏临时文件再原子改名成最终文件名——一次截图往往连发多个事件写 改名立刻扫描很可能读到半成品。0.2 秒的窗口让文件落定多次事件也只合并成一次扫描省掉无谓的目录遍历。扫描本身scan()ScreenshotWatcher.swift#L84-L91逻辑直白列出目录内所有文件和已知的known集合比对凡是不认识且符合条件的就回调onNew最后把整个列表刷新进known。第四步如何判定一张真截图不是目录里冒出图片就算截图——桌面上随手存个 jpg 不该被挂上绳。isCandidate()做了两层过滤ScreenshotWatcher.swift#L103-L113扩展名白名单png、jpg、heic、tiff、gif、webp 等图片格式场景区分监视的是桌面时文件必须带 macOS 给真截图打的扩展属性com.apple.metadata:kMDItemIsScreenCapture用getxattr检查——系统截图都带这个标记手工存的图片没有监视的是专用截图文件夹收件箱模式时任何图片都算另外还有个细节known集合初始化时只收录启动时间之前就存在的文件并用文件创建时间判定ScreenshotWatcher.swift#L45-L56。这样即使应用在等待桌面访问权限期间截图已经落地启动后也不会漏捡。收件箱模式的双保险默认情况下截图保存在桌面ScreenshotWatcher就监视桌面。开启收件箱模式后Inbox.swift 会把系统截图存储位置改到Application Support/Tendedero/Screenshots并关掉系统那个要等五秒的浮动缩略图——截图写完文件就直接被捡走。但作者仍多留了一层保险AppDelegate.swift#L119-L138 中若收件箱模式生效且目标不是桌面会额外再启一个监视桌面的安全监视器——万一某个 macOS 版本没遵守存储位置设置截图落回桌面也照样能挂上绳。而且所有系统设置的改动在应用退出时都会被还原macOS 永远不会指向一个没人监视的文件夹。小结一条清晰的调用链把本文内容串起来一张新截图的旅程是macOS 把文件写进截图目录临时文件 → 原子改名kqueue 报NOTE_WRITE/NOTE_RENAMEGCD 监视源在主线程被唤醒0.2 秒防抖后scan()遍历目录与known集合比对通过扩展名 kMDItemIsScreenCapture双重校验回调onNew→AppDelegate.hangCapture()生成缩略图截图从截屏区域飞到晾衣绳上想动手读源码的话建议路径是ScreenshotWatcher.swift本文主角→ Inbox.swift系统设置读写→ AppDelegate.swift装配与动画触发。下一篇我们来看 Line.swift 和 PeggedView.swift截图是怎么飞上绳、又怎么随风轻摆的。【免费下载链接】tendederoScreenshots, hung out to dry. A tiny native macOS app that hangs every screenshot on a line at the top of your screen.项目地址: https://gitcode.com/gh_mirrors/te/tendedero创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考