MacPro GPU Switch:用 JavaFX 做一个 macOS 显卡切换工具

#Java#JavaFX#macOS#开源#工具

MacBook Pro 15 寸是台双显卡的机器:一块 Intel 核显,一块 Radeon 独显。

用久了会撞上一个很具体的烦恼 —— 你没法决定现在用哪块。

macOS 的系统设置里,关于显卡只有「自动切换图形卡模式」这一个勾选框,它管的是“要不要自动切”,管不了“现在锁在哪一块”。真正能决定这件事的,是一条电源管理命令:

sudo pmset -a GPUSwitch 0   # 整合(只用核显)
sudo pmset -a GPUSwitch 1   # 高性能(只用独显)
sudo pmset -a GPUSwitch 2   # 自动

能用,但每次都得开终端、敲一遍、输一次密码。为了两秒钟的切换走完这一套,太长了。

MacPro GPU Switch 就是把这条命令包进一个窗口:选核显就一直用核显,选独显就一直用独显,选自动就交回给系统。

它长这样

界面很直白:左边大字写着当前处于哪个模式,颜色也跟着变(整合是青色,高性能是橙色,自动是蓝色);右边填 Root 密码,下面三个单选框对应三种模式;底部是「关于」「应用」「关闭」。

勾上「记住密码」,下次打开就不用再输。

它做的事就四步:读当前状态 → 你选一个新模式 → 点「应用」→ 它替你跑那条 sudo 命令,再把新状态读回来显示。

三种模式

对应 GPUSwitch 的三个值:

  1. 整合(0)—— 只用核显,最省电最凉快,重活干不了;
  2. 高性能(1)—— 只用独显,性能拉满,代价是发热和续航;
  3. 自动(2)—— 交给系统判断,也就是 macOS 默认的那套逻辑。

实现上其实只有三件事

代码量很小,核心就是三次 Shell 调用。

一、读懂当前状态

pmset -g 会打印一大串电源管理配置,显卡那一行长这样:

gpuswitch             0

所以读状态的逻辑就是:跑一遍 pmset -g,逐行找以 gpuswitch 开头的那行,把数字抠出来。

String[] command = {"/bin/sh", "-c", "pmset -g"};

for (String process : processList) {
    if (process.startsWith("gpuswitch")) {
        i = Integer.parseInt(process.replaceAll("gpuswitch| ", ""));
    }
}

拿到 0 / 1 / 2,再去点亮界面上对应的那个单选框。

二、切换

String[] command = {"/bin/bash", "-c",
    "echo \"" + password + "\" | sudo -S pmset -a GPUSwitch " + status};

关键在 sudo -S 这个参数。它让 sudo 从标准输入读密码 —— 不加它,sudo 会去找终端要密码,而一个图形界面程序没有终端,命令会直接卡在那里。把密码 echo 进去、管道喂给 sudo,就能在没有终端的环境里把这条命令跑完。

三、验证密码

切显卡之前得先知道密码对不对,否则用户点「应用」只会撞上一个莫名其妙的失败。

String[] command = {"/bin/bash", "-c",
    "echo \"" + password + "\" | sudo -S whoami"};

跑一次 whoami,输出的第一行是 root,就说明密码正确。

whoami 而不是 sudo -v,好处是它验证权限的方式和后面那条 pmset 完全一致,同时又没有任何副作用 —— 密码错了也不会误伤配置。

写正式代码之前,我先在 src/test 下扔了两个带 main 的小类,把命令单独跑通再搬进工具类。目录里留下的 CheckPasswordRunPmset 就是当时的验证代码。

界面部分

JavaFX + FXML,没什么花活。三个 RadioButtonuserData 里分别挂着 0 / 1 / 2,用 ToggleGroup 保证互斥;窗口初始化时先读一次当前状态,用 fire() 把对应的那个点亮。为了避免用户还没选模式就点「应用」,applyApp() 里也做了一次空判断。

「记住密码」是怎么存的

勾上「记住密码」之后,密码不会明文落盘,而是先加密再写进程序目录下的 conf.properties

  1. 算法AES/ECB/PKCS5Padding,密文再转成 URL-safe 的 Base64 保存;
  2. 密钥不是写死的常量,而是每台机器独有的一个标识 —— 跑一遍 system_profiler SPHardwareDataType,抓 Hardware UUID: 那一行,去掉空格和连字符,正好 32 位十六进制,直接拿来当 AES-256 的密钥。

配置文件里只存两样东西:一个「是否记住密码」的开关,和密文本身。

取 Hardware UUID 而不是写死一个字符串,好处是凭据跟机器绑在一起 —— 把 conf.properties 拷到别的机器上,密文就解不开了。反过来,换机器或者重装系统之后,之前记住的密码会失效,重新输一次就行。

支持范围,和一句要紧的警告

  • 实际验证过的只有一台:MacBook Pro(15-inch,2018)+ macOS Mojave 10.14.3
  • 理论上所有能用 sudo pmset -a GPUSwitch 切换显卡的型号都能用,欢迎大家补充自己的型号;
  • ⚠️ 只有核显的机器切勿尝试。 这是 README 里唯一用感叹号写的一句,我原样搬过来 —— 没有独显的机器上执行这条命令没有意义,也没人验证过会发生什么;
  • 它只在 macOS 上有意义,依赖 pmsetsystem_profiler 这两个系统命令。

跑起来

开发环境是当年的那一套:Apache Maven 3.6、JDK 8、IntelliJ IDEA 2018.3。

当年 Oracle JDK 8 自带 JavaFX,不用另外下 SDK —— 这也是我选 JavaFX 而不是拉一堆第三方 UI 库的原因:对一个这么小的工具来说,零额外依赖比什么都重要。

跑起来很简单:clone 下来用 IDEA 打开,运行 cn.jxh.switcher.Main 就行。想打包成 macOS 的原生应用,pom 里已经配好了 JavaFX 的 Maven 插件(zenjava 8.8.3,应用名 MacProGPUSwitch)。

版本号那处设计顺带提一句:app.properties 里放的不是写死的数字,而是 ${project.version},打包时由 Maven 的 resource filtering 替换成真实版本。改 pom 里的 <version> 一处,界面「关于」里显示的版本号就跟着走。

版本

  • 1.0.0(2019-02-10):第一个版本,能读状态、能切换;
  • 1.0.1(2019-02-12):加上 Root 密码验证,以及「记住密码」。

写完这篇的时候,正好是 1.0.1。

最后

仓库在这里,Apache License 2.0:

这是个很小的工具,解决的问题也很窄。但如果你也是 2018 款 MacBook Pro 的用户,被那块随时会醒过来的独显烦过,它可能正好是你要的那个开关。