CGEventTap(全称 Core Graphics Event Tap)是 macOS 提供的一种低层级机制,用于监听或拦截系统中的输入事件,如键盘按键、鼠标点击、滚轮滚动等,无论事件来自哪个 App 或窗口。
它常用于:
1、创建快捷键监听器(比如截图工具);
2、构建键盘记录器(需注意隐私合规);
3、实现全局快捷方式(如 command + shift + S);
4、做辅助功能(如屏幕阅读器、自动化工具)。
基本用法
使用CGEvent.tapCreate()创建:
let eventTap = CGEvent.tapCreate(
tap: .cgSessionEventTap, // 监听哪个层级(全局/session)
place: .headInsertEventTap, // 插入 Tap 的顺序
options: .defaultTap, // 是监听?还是可以修改?
eventsOfInterest: CGEventMask(...),// 要监听的事件类型(如 keyDown)
callback: { _, type, event, _ in // 回调处理函数
// 处理事件
return Unmanaged.passUnretained(event)
},
userInfo: nil
)
启动监听:
1、创建CFMachPort:
let runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0)
2、添加到 RunLoop:
CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, .commonModes)
CGEvent.tapEnable(tap: eventTap!, enable: true)
CFRunLoopRun()
Event Tap 会一直监听,直到关闭它或 App 被系统终止。
代码解析
1、创建事件Tap(CGEvent.tapCreate)
let eventTap = CGEvent.tapCreate(
tap: .cgSessionEventTap,
place: .headInsertEventTap,
options: .defaultTap,
eventsOfInterest: CGEventMask(...),
callback: { _, type, event, _ in
// 处理事件
return Unmanaged.passUnretained(event)
},
userInfo: nil
)
1、tap:监听 Session 层级事件(一般用于监听整个用户会话的事件)
.cghidEventTap:最底层,直接从硬件中获取事件(可拦截、修改)。多数情况用于模拟事件。需要辅助功能权限。
.cgSessionEventTap:会话层级,一般用于 App 层级的监听(可监听/修改)。
.cgAnnotatedSessionEventTap:更高层级的 UI 事件,仅能监听(不可修改)。
.cgHeadInsertEventTap(旧):早期版本,替代为 .cgSessionEventTap 。
监听全局用户输入时,一般使用 .cgSessionEventTap,想监听系统层最底层输入可用 .cghidEventTap(需特权权限或辅助功能权限)。
2、place::插入事件 tap 的顺序
.headInsertEventTap:插入事件队列的前端,优先接收事件。
.tailAppendEventTap:插入队列末尾,后处理事件。
3、options:Tap行为
.defaultTap:默认,可以监听并修改事件。
.listenOnly:只能监听事件,不可修改或丢弃(系统只读监听)。
4、eventsOfInterest:要监听的事件类型
例如:
let mask = (1 << CGEventType.keyDown.rawValue) |
(1 << CGEventType.keyUp.rawValue) |
(1 << CGEventType.flagsChanged.rawValue)
常见的事件类型:
.keyDown:按键按下;
.keyUp:按键抬起;
.flagsChanged:修饰键(Shift/Cmd等)变化;
.leftMouseDown:鼠标左键按下;
.mouseMoved:鼠标移动;
.scrollWheel:滚轮滚动。
5、callback:回调函数,每次有事件进入监听时,都会调用此函数。必须返回一个 Unmanaged<CGEvent>? 类型的值。
{ proxy, type, event, refcon in
// 这里可以判断 type 是不是关注的事件,比如 keyDown、mouseMoved 等
print("事件类型: \(type.rawValue)")
return Unmanaged.passUnretained(event) // 继续传递事件
// return nil 则拦截该事件,不让它传递
}
proxy:CGEventTapProxy类型,一个系统提供的代理对象,用于处理事件(一般不用管);
type:CGEventType类型,,表示事件类型,如 .keyDown、.mouseMoved;
event:CGEvent类型,就是当前事件的对象,可以读取/修改;
refcon:通过 userInfo 传入的自定义指针,可以转回 Swift 对象。
return:可以决定这个事件是否被传递下去,或是否被修改
1)Unmanaged.passUnretained(event):保持原事件,继续传递;
2)Unmanaged.passRetained(event):修改后的事件传递出去(需要保留控制);
3)nil:拦截并丢弃此事件,不会传递到后面的应用或系统。
6、userInfo:UnsafeMutableRawPointer? 类型,表示可以传入自定义数据,在回调时作为 refcon 参数使用。
可以通过:
Unmanaged.passUnretained(myObject).toOpaque()
传入 Swift/ObjC 对象,并在回调中取出。
2、将事件Tap添加到RunLoop
let runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0)
CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, .commonModes)
CFMachPortCreateRunLoopSource(…):将 eventTap 封装成一个 RunLoop Source,才能被当前 RunLoop 管理。
CFRunLoopAddSource(…):将这个 Source 加入当前线程的 RunLoop 中,以便持续监听事件。
3、启用事件Tap
CGEvent.tapEnable(tap: eventTap!, enable: true)
开启事件 Tap 的监听。
有些时候 tap 会因为系统安全性等原因被禁用,需要用这行手动开启。
CGEventMask监听事件
let mask = (1 << CGEventType.keyDown.rawValue) |
(1 << CGEventType.keyUp.rawValue) |
(1 << CGEventType.flagsChanged.rawValue)
let eventTap = CGEvent.tapCreate(
tap: .cgSessionEventTap,
place: .headInsertEventTap,
options: .defaultTap,
eventsOfInterest: CGEventMask(...), // CGEventMask
callback: { _, type, event, _ in
return Unmanaged.passUnretained(event)
},
userInfo: nil
)
mask使用位操作创建一个 CGEventMask,表示想监听哪些类型的事件。
enum CGEventType : UInt32 {
case null = 0
case leftMouseDown = 1
case leftMouseUp = 2
case rightMouseDown = 3
case rightMouseUp = 4
case mouseMoved = 5
case leftMouseDragged = 6
case rightMouseDragged = 7
case keyDown = 10
case keyUp = 11
// 还有更多...
}
位运算表达的意义:
let mask = (1 << CGEventType.keyDown.rawValue)
| (1 << CGEventType.keyUp.rawValue)
| (1 << CGEventType.mouseMoved.rawValue)
| (1 << CGEventType.leftMouseDown.rawValue)
实际执行过程:
每一个 (1 << rawValue) 表示 设置第 N 位为 1,其中 N 是该事件的 rawValue。
1 << 10 → 表示监听 .keyDown
1 << 11 → 表示监听 .keyUp
1 << 5 → 表示监听 .mouseMoved
1 << 1 → 表示监听 .leftMouseDown
这些位被 |(按位或)连接在一起,合并成一个 64 位整数,表示要监听这几类事件。
CGEventMask(mask) 是什么?
CGEventMask 实际上是 UInt64 类型的别名:
typealias CGEventMask = UInt64
所以创建的是一个 “事件掩码”,用于传递给 CGEvent.tapCreate 的 eventsOfInterest 参数,告诉系统想监听哪些事件。
注意事项
1、权限要求:监听全局事件需要启用“按键接收”(在「系统设置 – 隐私与安全性 – 输入监控」中授权);


2、不能在沙盒环境中监听其他 App 的事件(App Store 限制);
3、滥用可能会导致拒审或系统警告(如创建键盘记录器);
使用示例
1、监听全局快捷键Option+A并调用截图方法:
let mask: CGEventMask = (1 << CGEventType.keyDown.rawValue)
let eventTap = CGEvent.tapCreate(
tap: .cgSessionEventTap,
place: .headInsertEventTap,
options: .listenOnly, // 改为 .defaultTap 可拦截事件(会影响其他App),一般用 listenOnly
eventsOfInterest: mask,
callback: { _, type, event, _ in
print("监听到键盘被按下")
guard type == .keyDown else { return Unmanaged.passUnretained(event) }
// 获取按键
let keyCode = event.getIntegerValueField(.keyboardEventKeycode)
// 获取修饰键状态
let flags = event.flags
// Option + A 判断:A 是 keyCode 0(美式键盘),Option 是 .maskAlternate
if keyCode == 0 && flags.contains(.maskAlternate) {
print("检测到 Option + A - 调用截图方法")
DispatchQueue.main.async {
StatusBarController.shared.fullScreenshoot()
}
}
return Unmanaged.passUnretained(event)
},
userInfo: nil
)
guard let eventTap = eventTap else {
print("Failed to create event tap.")
return
}
let runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0)
CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, .commonModes)
CGEvent.tapEnable(tap: eventTap, enable: true)
每次键盘的按键被按下时,都会执行callback中的方法。
当使用Option + A组合快捷键时,就会调用StatusBarController.shared.fullScreenshoot方法,截取图片。
注意,首次调用该方法会弹出“按键接收”的提示。
此外,CGEvent.tapCreate(…) 的 callback: 参数是一个 C 函数指针,不允许捕获上下文(比如 self)。这是 Swift 与 C API 交互时的限制。
所以,如果callback中使用self,就会报错:
A C function pointer cannot be formed from a closure that captures context
解决方案为:使用单例模式获取调用的self属性或方法,或者将回调函数改为一个不捕获上下文的全局函数。
移除监听
如果想要控制CGEventTap,可以将eventTap和runLoopSource保存起来:
private var eventTap: CFMachPort? // CGEventTap
private var runLoopSource: CFRunLoopSource? // RunLoop Source
eventTap = CGEvent.tapCreate(...)
runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, eventTap, 0)
保留eventTap的引用便于后续调用 CFMachPortInvalidate(eventTap) 停止监听。
保留runLoopSource为了使用CFRunLoopRemoveSource移除它,如果不保留runLoopSource的引用,后续无法清理运行循环资源。
这两个都属于底层Core Foundation 资源,无法通过普通 ARC 自动管理,必须手动管理它们的生命周期。
移除监听:
func stopListening() {
if let runLoopSource = runLoopSource {
CFRunLoopRemoveSource(CFRunLoopGetCurrent(), runLoopSource, .commonModes)
}
if let eventTap = eventTap {
CFMachPortInvalidate(eventTap)
}
runLoopSource = nil
eventTap = nil
}
这里的stopListening方法用于停止CGEventTap的监听,防止内存泄露、资源占用以及避免继续监听全局事件。
CFRunLoopRemoveSource(…):将CGEventTap从当前运行循环中移除。
CFMachPortInvalidate(…):使事件tap无效,这一步将彻底关闭监听器,否则即使移除Run Loop源,监听器仍然可能存在于内核中。
总结
CGEventTap用于监听或拦截系统中的输入事件,如键盘按键、鼠标点击、滚轮滚动等事件。
在代码层面使用位运算符,这是因为Core Graphics(CG)是一套非常底层的框架,很多 API 都是从早期的 C 和 Objective-C 演变过来的。
在 C 中,用一个整数的不同位表示不同开关(flags)是常见做法。
每一个事件类型对应一个位(bit)的位置,比如 .keyDown = 10,表示第 10 位为 1 就监听 keyDown。
这种方式节省空间、高效、可组合、跨语言兼容。
相关文章
1、macOS模拟事件CGEvent:https://fangjunyu.com/2025/07/22/macos%e6%a8%a1%e6%8b%9f%e4%ba%8b%e4%bb%b6cgevent/