肇鑫的技术博客

业精于勤,荒于嬉

苹果表无法解锁macOS问题的一个奇葩的解决办法

我实在无法找到更适当的词,只能用奇葩来形容这个解决办法。

我开始使用macOS 26 beta系列也有一段时间了,目前在使用的最新版本是beta 7。长久以来,我一直遇到一个有些奇怪的问题,就是每次系统睡眠之后唤醒,苹果表的解锁总是失败。但是进入系统之后,在需要苹果表解锁的其它情况下,比如打开密码应用或者钥匙串,双击苹果表侧键解锁的这个功能却总是能成功。

不过苹果表解锁也不总是失败。我有两台Mac mini,一台是M1,一台是M4,M1的解锁就总是成功,而M4这台就总是失败。

尝试解决这个问题

今天我突发奇想,想要在AI的辅助下解决这个问题。我首先怀疑的时候VPN软件。因为更新到最新的macOS 26 beta之后,原来使用Clash X Pro不好用了,我不得不换成了Clash Verge rev。我问AI有没有可能是VPN软件造成的。AI说有可能。让我关了它再试。我试了。还是一样。

之后我又和AI一起开启了macOS控制台应用,想尝试通过读取日志来找到问题的解决方案。最后发现了一个loginwindow的一条故障日志,故障(红色)是比错误(黄色)等级更高的错误。它说在创建main文件夹的某个临时文件夹时出错,可能是沙盒的问题。我当时也信了,因为我的M4是256G的,为了节省磁盘空间,我将home文件夹设置到了外置的SSD上。我想这种比较罕见的设置,可能是苹果没有考虑过。我甚至还差一点儿就去跟苹果反馈这个问题。但是后来我放弃了。因为我觉得每次输入密码也还好,不算很麻烦,就没有反馈。同时我在想,以后我再买新电脑,一定要多花些钱,买个大一些的硬盘。

山重水复疑无路

的确。我没能解决这个问题。我只是放下了它。然后,我开始着手解决我的另外一个开源应用的小问题。

App Helper,应用助手。是我开源在GitHub上的一个macOS的助手应用。它的其中一个功能,是一键切换HDR模式,即在开启和关闭之间切换。不过,我发现,我的显示器,虽然支持HDR,但是使用不同的连接方式它对于HDR的支持不同。比如用USB-C线直接连接,就不支持HDR,用HDMI或者DisplayPort线连接,就支持。

所以,我的目标是通过系统API检测,在不支持HDR的时候,隐藏这个一键切换的按钮。因为我当前是USB-C的连接,不支持HDR,所以我首先完成了这部分的代码。

因为我还需要测试支持HDR下的部分。于是我同时使用HDMI进行连接。macOS有一个问题,就是你用两根不同的线连接同一个显示器,但是在macOS看来你就是在使用双显示器。它的显示器设置中,同时显示出两台显示器,并且不能设置禁用其中的一个。这样就比较麻烦了。因为我虽然可以通过显示器上的信号源菜单来切换到不同的接口,但是存在一个主副窗口的问题。

最终没办法,我只能将Mac mini上的USB-C连接显示器的那根拔了下来。将HDMI的连接作为唯一的连接,这样可以方便我进行调试。

柳暗花明又一村

这次HDR的选项还是不能显示。经过调试我发现,必须显示器先打开HDR模式,苹果的API才能检测出显示器支持HDR,如果没有开启,那就检测不出来。

那也无妨,大不了我就跟原来一样,不检测了。我又测试一键切换的功能。结果这个功能也不好用。这就比较郁闷了。因为苹果本身并没有提供切换的HDR切换的API,我使用的是脚本调用控件的方式,这个方法生效的前提是控件的位置必须固定。我管它叫数格子,脚本的写法类似,找到xx组的xx格子,然后把它上面的开关打开/关闭。现在系统升级了,位置变了。苹果💊。

算了,心累。我打算彻底去掉这个功能。毕竟,苹果动动手指,我就得重新数格子,并且还需要考虑不同版本macOS的兼容性,实在得不偿失。

休息,休息一会儿

休息结束之后,重新唤醒已经睡眠的Mac,手表传来熟悉的解锁声。我居然成功解锁了这台M4。又试了几次,无论是睡眠之后立即解锁,还是睡眠了几小时之后再解锁,都是次次成功。

“这是为什么呢?”(蔡明)如是说。“排除了一切不可能,那么剩下的那个无论多么不合理,就是唯一的可能。”——福尔摩斯。

我做的最大改动只有一点,使用HDMI连接显示器,并且拿掉了USB-C的连线。所以,这个问题的原因就是使用USB-C连接显示器,会导致苹果表无法解锁macOS。这谁能想到啊,你说是不是奇葩的解决办法?

特别说明,这个USB-C连接显示器,就是两边都是USB-C接口。而HDMI连接,则是用Mac mini独有的HDMI接口连接显示器的HDMI接口。

理论上,这个USB-C除了传递数据,还能同时获得显示器传来最大90瓦的电量。不过由于Mac mini的USB-C不支持反向供电,所以没啥用。如果比MacBook,是可以同时供电的。但是不知道是不是这一点影响了苹果表的解锁。我不是硬件工程师,不敢妄言。但是觉得这个值得提一下。

macOS 26 Beta版至今发现的兼容性问题及解决方案

我是从beta 4开始当成主力机使用的,因此从beta 4开始记录。之后会不定期更新。

Beta 4

命令system_profiler的参数发生改变

之前要查看USB设备,需要使用

system_profiler SPUSBDataType

但是新系统中,参数改变了,变成了SPUSBHostDataType。并且输出的结果中的属性也有了变化,需要进行对应的修改。

应用伴随系统启动的方式,不同的方法在设置中显示不同

如果要应用伴随系统启动,现在有两种方式:

  1. 使用Login Item应用,注册一个Login Item辅助程序,系统启动它之后,由它来调用主应用。
  2. 直接在主应用中调用SMAppService.mainApp.register()。

方法1是很早就有的方式。方法2是后来新增的方式。不过在新系统中,如果你使用的是方法1,那么在设置的启动项中,只有后台有主应用的名字,系统自动其中中不会有。如果是使用的方法2,那么启动项和后台中都会有主应用的名字。

两种方式都可以成功伴随系统启动。

系统常驻菜单栏图标消失问题的解决(下方2025年7月29日有更新)

新系统中,我发现有的应用的菜单栏常驻图标会消失。进一步调查我发现,如果你的菜单栏常驻图标中使用attributedTitle,并且重复设置了相同的值,常驻图标就会消失。

临时解决方案是,每次更改之前比较值是否发生了改变,变了再重新设置。

Beta 4 2025年7月29日更新

系统常驻菜单栏图标消失问题的解决(更新版)

上面的临时解决方案虽然在系统运行时可以解决问题,但是一旦系统睡眠,唤醒之后还是可能出现同样的问题。经过进一步研究,我发现了更好的解决方案:

假设你的代码是类似这种

class AppDelegate: NSObject, NSApplicationDelegate {
  private var statusItem: NSStatusItem?

  private func setupMenubarTray() {
    let statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
    self.statusItem = statusItem

    guard let button = statusItem.button else {
      fatalError()
    }
    
    // 其他代码
  }
}

那么将代码改成

class AppDelegate: NSObject, NSApplicationDelegate {
  private var statusItem: NSStatusItem?

  private func setupMenubarTray() {
    if self.statusItem == nil {
      self.statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
    }

    guard let button = self.statusItem?.button else {
      fatalError()
    }
    
     // 其他代码
  }
}

我想问题应该是出在NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)这个函数。它可能没有正确的释放。导致同时存在多个实例了。

Beta 5 2025年8月7日更新

Beta 5修正了MacCatalyst下.fullScreenCover的显示问题。上一版,会透出后面视图的内容。

不要使用NSDecimalNumber.intValue

今天遇到了一件怪事。函数返回值始终是0,但是中间计算过程又没啥问题。

最终判定,就是在计算百分比时将Decimal计算的百分比,通过(decimal as NSDecimalNumber).intValue进行输出时出错的。

深入分析具体的原因,是因为Int的精度不足以转换NSDecimalNumber,所以就自动设置为0了。这个对于普通人来说,可能很难理解。毕竟结果也才46,怎么说Int的精度不足呢?但是事实就是如此,你可以简单地认为就是46后面的小数点之后的位数过多了。

解决的办法有有两个,一种是直接使用.doubleValue,然后取整。

let intValue = Int(nsDecimalNumber.doubleValue)

另一种则比较复杂,使用了专门的rounding函数。

let rounded = nsDecimalNumber.rounding(accordingToBehavior: NSDecimalNumberHandler(
    roundingMode: .plain,
    scale: 0,
    raiseOnExactness: false,
    raiseOnOverflow: false,
    raiseOnUnderflow: false,
    raiseOnDivideByZero: true
))
let intValue = rounded.intValue