macOS监听/拦截输入事件CGEventTap
macOS监听/拦截输入事件CGEventTap

macOS监听/拦截输入事件CGEventTap

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/

   

如果您认为这篇文章给您带来了帮助,您可以在此通过支付宝或者微信打赏网站开发者。

欢迎加入我们的 微信交流群QQ交流群,交流更多精彩内容!
微信交流群二维码 QQ交流群二维码

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注