如何使用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
相关产品推荐
相关产品推荐

