You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使用Java FFM API在macOS上调用NSEvent.addGlobalMonitorForEventsMatchingMask方法

如何使用Java FFM API在macOS上调用NSEvent.addGlobalMonitorForEventsMatchingMask方法

嘿,我刚好研究过用Java的FFM API调用macOS上的Cocoa原生方法,这就给你一步步拆解怎么实现全局事件监听:

首先得明确,FFM API是Java 21及以上版本才有的正式特性,用来和本地C/Objective-C代码交互,所以得确保你的JDK版本达标。

核心实现思路

要调用Objective-C的NSEvent.addGlobalMonitorForEventsMatchingMask:handler:,我们需要和Objective-C运行时打交道,核心步骤包括:

  • 加载Objective-C运行时的关键函数(比如objc_getClass、sel_registerName、objc_msgSend)
  • 获取NSEvent类的指针
  • 拿到目标方法的选择子(selector)
  • 把Java方法包装成Objective-C能识别的block作为事件回调
  • 调用方法注册全局事件监听

完整可运行代码

import java.lang.foreign.*;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;

import static java.lang.foreign.ValueLayout.ADDRESS;
import static java.lang.foreign.ValueLayout.JAVA_LONG;

public class CocoaTest {
    // 获取objc_getClass的MethodHandle
    private static Optional<MethodHandle> objcGetClass() {
        try {
            SymbolLookup objcLookup = SymbolLookup.libraryLookup("/usr/lib/libobjc.A.dylib", Arena.global());
            MethodHandle mh = MethodHandles.lookup().findStatic(
                    objcLookup.find("objc_getClass").orElseThrow(),
                    "invoke",
                    MethodType.methodType(long.class, MemorySegment.class)
            );
            return Optional.of(mh);
        } catch (Exception e) {
            e.printStackTrace();
            return Optional.empty();
        }
    }

    // 获取sel_registerName的MethodHandle
    private static Optional<MethodHandle> selRegisterName() {
        try {
            SymbolLookup objcLookup = SymbolLookup.libraryLookup("/usr/lib/libobjc.A.dylib", Arena.global());
            MethodHandle mh = MethodHandles.lookup().findStatic(
                    objcLookup.find("sel_registerName").orElseThrow(),
                    "invoke",
                    MethodType.methodType(MemorySegment.class, MemorySegment.class)
            );
            return Optional.of(mh);
        } catch (Exception e) {
            e.printStackTrace();
            return Optional.empty();
        }
    }

    // 获取objc_msgSend的MethodHandle,注意签名要匹配调用的方法
    private static Optional<MethodHandle> objcMsgSend() {
        try {
            SymbolLookup objcLookup = SymbolLookup.libraryLookup("/usr/lib/libobjc.A.dylib", Arena.global());
            MethodHandle mh = MethodHandles.lookup().findStatic(
                    objcLookup.find("objc_msgSend").orElseThrow(),
                    "invoke",
                    MethodType.methodType(MemorySegment.class, long.class, MemorySegment.class, long.class, MemorySegment.class)
            );
            return Optional.of(mh);
        } catch (Exception e) {
            e.printStackTrace();
            return Optional.empty();
        }
    }

    // 自定义的事件处理回调方法,参数是NSEvent的指针
    public static void eventHandler(MemorySegment event) {
        // 这里可以扩展处理逻辑,比如获取事件类型、位置等
        System.out.println("捕获到全局事件,事件地址: " + event.address());
    }

    public static void main(String[] args) {
        try (Arena arena = Arena.ofConfined()) {
            // 1. 获取NSEvent类的指针
            long nsEventClass = (long) objcGetClass().get().invoke(arena.allocateFrom("NSEvent"));

            // 2. 获取目标方法的selector
            MemorySegment monitorSelector = (MemorySegment) selRegisterName().get()
                    .invoke(arena.allocateFrom("addGlobalMonitorForEventsMatchingMask:handler:"));

            // 3. 把Java的eventHandler包装成Objective-C block
            MethodType blockMethodType = MethodType.methodType(void.class, MemorySegment.class);
            MemorySegment eventHandlerBlock = Linker.nativeLinker().upcallStub(
                    MethodHandles.lookup().findStatic(CocoaTest.class, "eventHandler", blockMethodType),
                    FunctionDescriptor.ofVoid(ADDRESS),
                    arena
            );

            // 4. 设置要监听的事件掩码,这里以鼠标移动事件为例(对应NSMouseMovedMask)
            long eventMask = 1L << 6;

            // 5. 调用方法注册全局监听
            objcMsgSend().get().invoke(nsEventClass, monitorSelector, eventMask, eventHandlerBlock);

            // 保持程序运行,否则会直接退出
            System.out.println("全局事件监听已启动,按回车键退出...");
            System.in.read();

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

关键细节提醒

  • 事件掩码:你可以根据需求修改eventMask的值,比如键盘事件用1L << 14(NSKeyDownMask),点击事件用1L << 1(NSLeftMouseDownMask),多个事件可以用位或(|)组合
  • 权限问题:macOS上全局事件监听需要权限,首次运行系统会弹出权限请求,要在「系统设置-隐私与安全性-输入监控」里给Java程序授权,否则会监听失败
  • 内存管理:用Arena.ofConfined()来管理本地内存,确保资源能自动释放,避免内存泄漏

备注:内容来源于stack exchange,提问作者Ysxz Y

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.15 12:42:59