Telegram 技巧档案UIKIT · APPKIT
macOS · AppKit

私有系统集成

CAEmitterBehavior、CGEvent tap、Dock 资源叉与系统视图树改造。

本主题范围:TelegramSwift 的 macOS 私有 API、运行时 hack 与系统集成。结论:该版本最激进的做法是私有 Core Animation 行为、系统内部视图树改造、CGEvent tap,以及 Finder 资源叉改图标。

CAPortalLayer 没有活跃用法。它只在 CAPortalLayer.h#L2-L23 声明。CAPortalLayer.m#L1 为空。全仓 rg 只找到该声明和包定义。因此,本笔记不把它计入技巧。

1. 拼接私有类名,并用函数指针调用 CAEmitterBehavior

#14-01TelegramSwiftAppKit源码分析REA 二进制macOS 26.7 实测私有 API运行时 hackCore Animation

做了什么:代码运行时构造私有类 CAEmitterBehavior。代码也构造真实 selector behaviorWithType:。代码把 method(for:) 的 IMP 强转为 C 函数指针。随后,代码通过 emitterBehaviors 的 KVC 键装配吸附和生命周期行为。

为什么这样做:CAEmitterLayer 的普通 Swift 接口不能表达这里的吸附器。代码因而复用系统粒子求解器。它避免自己逐帧计算大量尘粒的轨迹。

关键代码摘录

InvisibleInkDustView.swift · L12–L18 等 2 处
public func createEmitterBehavior(type: String) -> NSObject {
    let selector = ["behaviorWith", "Type:"].joined(separator: "")
    let behaviorClass = NSClassFromString(["CA", "Emitter", "Behavior"].joined(separator: "")) as! NSObject.Type
    let behaviorWithType = behaviorClass.method(for: NSSelectorFromString(selector))!
    let castedBehaviorWithType = unsafeBitCast(
        behaviorWithType,
        to: (@convention(c) (Any?, Selector, Any?) -> NSObject).self
    )
    return castedBehaviorWithType(behaviorClass, NSSelectorFromString(selector), type)
}

let fingerAttractor = createEmitterBehavior(type: "simpleAttractor")
fingerAttractor.setValue("fingerAttractor", forKey: "name")
let alphaBehavior = createEmitterBehavior(type: "valueOverLife")
alphaBehavior.setValue("color.alpha", forKey: "keyPath")
alphaBehavior.setValue([0.0, 0.0, 1.0, 0.0, -1.0], forKey: "values")
alphaBehavior.setValue(true, forKey: "additive")

解混淆确认:['CA', 'Emitter', 'Behavior'].joined() 的结果是 CAEmitterBehavior。['behaviorWith', 'Type:'].joined() 的结果是 behaviorWithType:。这两个结果直接传给 NSClassFromString 和 NSSelectorFromString。

代价与风险:这是私有 API。as!、强制取 IMP 与 unsafeBitCast 都会在类或方法消失时崩溃。emitterBehaviors 及其 KVC 路径也可能被系统改名。App Store 审核风险很高。

证据链 · #14-01已发布 · 仍可用
源码分析
已在 TelegramSwift 固定提交中逐行确认,共 2 处源码位置。
REA 第 1 项
已发布 · 仍可用REA 二进制macOS 26.7 实测

13.0 二进制:"CA"、"Emitter"、"Behavior" 以 Swift 小字符串存于 __DATA.__data 0x5e75208/18/28;"behaviorWith"、"Type:" 存于 0x5e751c0/d0。完整名 CAEmitterBehavior、behaviorWithType 出现 0 次。"simpleAttractor" 编成 5 处、"valueOverLife" 编成 3 处 MOVZ/MOVK 立即数。KVC 路径 emitterBehaviors.fingerAttractor.*、randomAttractor0/1.* 以明文存于 __cstring

macOS 26.7:类存在;+behaviorWithType: 存在;两种类型都能创建

结论:已发布,当前系统仍可用

2. 用监听型 CGEvent tap 实现全局按住说话

#14-02TelegramSwiftAppKit报告精选 14-1源码分析REA 二进制REA 运行时观测文本与输入

做了什么:代码监听 HID 级键盘、修饰键和其他鼠标键。它把 CGEvent 转回 NSEvent。它选择 .listenOnly,所以监听器不拦截原始输入。权限不足时,代码退化为应用内 NSEvent monitor。

为什么这样做:普通窗口事件只能在应用前台工作。这里要支持全局按住说话。监听型 event tap 同时保留其他应用的按键行为。

关键代码摘录

PushToTalk.swift · L119–L169
if hasPermission {
    func callback(proxy: CGEventTapProxy, type: CGEventType,
                  event: CGEvent, refcon: UnsafeMutableRawPointer?) -> Unmanaged<CGEvent>? {
        if let event = NSEvent(cgEvent: event) {
            let processor = Unmanaged<ProcessEvent>.fromOpaque(refcon!).takeUnretainedValue()
            processor.process(event)
        }
        return Unmanaged.passRetained(event)
    }
    let eventMask: Int32 = (1 << CGEventType.keyDown.rawValue)
        | (1 << CGEventType.otherMouseDown.rawValue)
        | (1 << CGEventType.otherMouseUp.rawValue)
        | (1 << CGEventType.keyUp.rawValue)
        | (1 << CGEventType.flagsChanged.rawValue)
    self.eventTap = CGEvent.tapCreate(tap: .cghidEventTap,
                                      place: .headInsertEventTap,
                                      options: .listenOnly,
                                      eventsOfInterest: CGEventMask(eventMask),
                                      callback: callback,
                                      userInfo: UnsafeMutableRawPointer(Unmanaged.passRetained(processEvent).toOpaque()))
}

代价与风险:这需要“输入监控”授权。用户可能拒绝授权。userInfo 以 passRetained 传入,但该段代码未见对应 release。是否由其他路径释放未验证。

证据链 · #14-02已发布 · 仍可用
源码分析
已在 TelegramSwift 固定提交中逐行确认,共 1 处源码位置。
报告摘要
怎么做到:回调转 NSEvent,用 Unmanaged 传递处理器。
REA 第 10 项
已发布 · 仍可用REA 二进制REA 运行时观测

13.0 二进制:导入 CGEventTapCreate、CGEventTapEnable、IOHIDCheckAccess、IOHIDRequestAccess

macOS 26.7:LLDB 把断点解析到 SkyLight 的 SLEventTapCreate;启动 40 秒内未调用

结论:已发布;默认未启用

3. 用 FinderInfo 位和资源叉永久改写 Dock 图标

#14-03TelegramSwiftAppKit报告精选 14-2源码分析REA 二进制性能与内存

做了什么:代码先让 sips -i 生成图标资源叉。代码把资源叉复制到主 .app 包内名为 Icon\r 的文件。代码设置 com.apple.FinderInfo 第 8 字节的第 2 位。最后,代码删除 Dock 图标缓存,并执行 killall Dock。

为什么这样做:NSApplication.applicationIconImage 只能改变当前进程的图标。它不能保证 Finder 和 Dock 重启后仍使用新图标。资源叉和 FinderInfo 是 Finder 的旧式自定义图标协议。

关键代码摘录

Dock.swift · L64–L93 等 2 处
static func enableCustomIcon(bundle: String) -> Bool {
    var info = readCustomIconAttribute(bundle: bundle)
    if info.isEmpty {
        info = Data(repeating: 0, count: kFinderInfoSize)
    }
    info[8] |= 4
    return writeCustomIconAttribute(bundle: bundle, value: info)
}

static func refreshDock(silence: Bool) -> Bool {
    let _ = launch(command: "/bin/bash", arguments: ["-c", "rm /var/folders/*/*/*/com.apple.dock.iconcache"])
    if !silence {
        let killallResult = launch(command: "/usr/bin/killall", arguments: ["Dock"])
        if killallResult != 0 { return false }
    }
    return true
}

let bundle = bundlePath()
let icon = bundle + "/Icon\r"
guard let rsrc = readResourceFork(path: path), !rsrc.isEmpty else { return nil }
if !writeResourceFork(path: icon, data: rsrc) || !enableCustomIcon(bundle: bundle) {
    return nil
}
return refreshDock(silence: silence) ? digest(rsrc) : nil

代价与风险:代码改写已安装 app bundle。签名、文件权限和沙盒都可能阻止它。删除缓存和杀死 Dock 会干扰用户。系统缓存路径和 Finder 元数据格式也有维护风险。

证据链 · #14-03已发布 · 仍可用
源码分析
已在 TelegramSwift 固定提交中逐行确认,共 2 处源码位置。
报告摘要
怎么做到:POSIX 扩展属性与 killall Dock 刷新缓存。
REA 第 9 项
已发布 · 仍可用REA 二进制

13.0 二进制:com.apple.ResourceFork、com.apple.FinderInfo、/usr/bin/killall、“Failed to run killall Dock”;导入 setxattr、getxattr、removexattr

macOS 26.7:未执行(会改动系统状态)

结论:已发布。非沙盒的 Developer ID 分发让写自身 bundle 成为可能

4. 深挖 AVMovableView,再点击隐藏的全屏按钮

#14-04TelegramSwiftAppKit报告精选 14-3源码分析REA 二进制macOS 26.7 实测REA 运行时观测Core Animation

做了什么:代码按类名找到 AVKit 内部的 AVMovableView。代码假设六层子视图链的末端是 NSButton。代码通过 performClick 触发那个按钮的全屏动作。

为什么这样做:代码保留 AVPlayerView 的系统控制条。它又让外层图库可驱动系统全屏实现。普通公开调用不能表达这条内部控件路径。

关键代码摘录

MGalleryExternalVideoItem.swift · L257–L267
override func toggleFullScreen() {
    if let view = _cachedView {
        let controls = HackUtils.findElements(byClass: "AVMovableView", in: view)?.first as? NSView
        if let controls = controls {
            if let view = controls.subviews.first?.subviews.first?.subviews.first?
                .subviews.last?.subviews.first?.subviews.last {
                if let view = view as? NSButton {
                    view.performClick(self)
                }
            }
        }
    }
}

代价与风险:AVMovableView 和子视图层级都不是稳定契约。任一 macOS 版本调整都会使全屏无效。错误类型或层级只会静默失败。审核风险很高。

证据链 · #14-04版本差异
源码分析
已在 TelegramSwift 固定提交中逐行确认,共 1 处源码位置。
报告摘要
怎么做到:findElements(byClass:) + 子视图链定位。
REA 第 6 项
已发布 · 仍可用REA 二进制macOS 26.7 实测

13.0 二进制:"AVMovableView" 编成 3 处立即数(0x102517a20、0x102518434、0x1025185f8),与源码 3 个调用点一致

macOS 26.7:类存在,并出现在 AVPlayerView 视图树中

结论:已发布;内部按钮路径未验证

REA 第 18 项
版本差异REA 二进制REA 运行时观测

13.0 二进制:HackUtils 出现 0 次;ObjcUtils 类存在,带 findElementsByClass:inView: 和 :array:

macOS 26.7:ObjcUtils 方法因符号已剥离,LLDB 无法下断

结论:版本差异:功能可能并入 ObjcUtils(未验证)

5. 重排 NSTitlebarView 内部子视图,腾出 50 pt 标题栏

#14-05TelegramSwiftAppKit报告精选 14-4源码分析REA 二进制macOS 26.7 实测REA 运行时观测私有 API

做了什么:代码改写 window.titleView 的 frame。代码递归按字符串找到私有 NSTitlebarView。代码遍历前三个内部子视图。代码隐藏它们,并按 Retina 像素取整后重设坐标。

为什么这样做:Passport 窗口使用透明标题栏和自定义导航区。系统的默认交通灯布局不能适配 50 pt 高度。公开 NSWindow API 不提供内部子视图的逐个坐标控制。

关键代码摘录

PassportWindowController.swift · L89–L104
@objc func windowDidNeedSaveState(_ notification: Notification) {
    if let titleView = window.titleView {
        let frame = NSMakeRect(0, window.frame.height - barHeight, titleView.frame.width, barHeight)
        if !NSEqualRects(frame, titleView.frame) {
            titleView.frame = frame
        }
        if let controls = (HackUtils.findElements(byClass: "NSTitlebarView", in: titleView)?.first as? NSView)?.subviews {
            var xs: [CGFloat] = [18, 58, 38]
            for i in 0 ..< min(controls.count, xs.count) {
                let view = controls[i]
                view.isHidden = true
                view.setFrameOrigin(xs[i], floorToScreenPixels(System.backingScale,
                    (barHeight - view.frame.height) / 2))
            }
        }
    }
}

代价与风险:NSTitlebarView、子视图顺序和前三项含义都不是公开契约。该代码不能确认前三项各自对应哪个交通灯。坐标顺序 [18, 58, 38] 也依赖旧层级。系统升级风险和审核风险很高。

证据链 · #14-05版本差异
源码分析
已在 TelegramSwift 固定提交中逐行确认,共 1 处源码位置。
报告摘要
怎么做到:HackUtils.findElements 定位并像素对齐。
REA 第 7 项
已发布 · 仍可用REA 二进制macOS 26.7 实测

13.0 二进制:"NSTitlebarView" 编成 1 处立即数(0x1036c54f8);NSTitlebarContainerView 在 __cstring

macOS 26.7:两个类都在标题栏视图树中

结论:已发布,结构仍在

REA 第 18 项
版本差异REA 二进制REA 运行时观测

13.0 二进制:HackUtils 出现 0 次;ObjcUtils 类存在,带 findElementsByClass:inView: 和 :array:

macOS 26.7:ObjcUtils 方法因符号已剥离,LLDB 无法下断

结论:版本差异:功能可能并入 ObjcUtils(未验证)

6. 用私有文本菜单 selector 删除系统子菜单

#14-06TelegramSwiftAppKit报告精选 14-5源码分析REA 二进制macOS 26.7 实测私有 API文本与输入

做了什么:代码检查 AppKit 生成的文本右键菜单。代码通过 _shareServiceSelected: 识别系统分享服务项。代码发现分享、字体或替换项时,删除整个父菜单。代码再插入 Telegram 的文本转换项。

为什么这样做:NSMenuItem 没有稳定的公开分类可区分这些系统项目。检查 action 让代码无需匹配本地化标题。它也能保持自定义转换项的位置。

关键代码摘录

ChatInputTextView.swift · L987–L1013
public override func menu(for event: NSEvent) -> NSMenu? {
    let menu = super.menu(for: event) as? NSMenu
    var removeItems = [NSMenuItem]()
    var addedTransformations = false
    menu?.items.enumerated().forEach { (idx, item) in
        if item.action == NSSelectorFromString("submenuAction:") {
            item.submenu?.items.enumerated().forEach { (subIdx, subItem) in
                if subItem.action == NSSelectorFromString("_shareServiceSelected:")
                    || subItem.action == NSSelectorFromString("orderFrontFontPanel:")
                    || subItem.action == NSSelectorFromString("orderFrontSubstitutionsPanel:")
                    || subItem.action == NSSelectorFromString("changeLayoutOrientation:") {
                    removeItems.append(item)
                } else if subItem.action == #selector(capitalizeWord(_:)) {
                    addedTransformations = true
                    if self.selectedRange.length > 0 {
                        if self.customDelegate?.inputTextCanTransform() == true {
                            self.transformItems.enumerated().forEach { (transformIdx, obj) in
                                item.submenu?.insertItem(obj, at: transformIdx)
                            }
                        }
                    }
                }
            }
        }
    }

代价与风险:_shareServiceSelected: 是私有 selector。submenuAction: 和其他系统菜单结构也会变化。某些系统版本可能删错菜单,或无法删掉目标菜单。审核风险存在。

证据链 · #14-06系统结构已变化
源码分析
已在 TelegramSwift 固定提交中逐行确认,共 1 处源码位置。
报告摘要
怎么做到:遍历子菜单,按 selector 过滤。
REA 第 5 项
系统结构已变化REA 二进制macOS 26.7 实测

13.0 二进制:该 selector 同时在 __cstring 和 __objc_methname

macOS 26.7:NSTextView 默认菜单中没有它,分享项改用 _performStandardShareMenuItem:。13.0 二进制中没有新 selector

结论:分享项的过滤条件在 26.7 上可能失效(推断,未在 Telegram 内实测)

7. 直接调用未公开的对角缩放光标 selector

#14-07TelegramSwiftAppKit报告精选 14-6源码分析REA 二进制macOS 26.7 实测私有 API

做了什么:代码向 NSCursor 类发送两个下划线 selector。它取得两种对角窗口缩放光标。调用点把它们用于图像编辑窗口和通话窗口的角落拖拽。

为什么这样做:系统内部光标可与 Finder 式窗口缩放保持一致。代码不用打包和维护自绘光标资源。

关键代码摘录

ObjcUtils.m · L30–L43
@implementation ObjcUtils

+(NSCursor * __nullable)windowResizeNorthWestSouthEastCursor {
    NSCursor *cursor = [[NSCursor class]
        performSelector:@selector(_windowResizeNorthWestSouthEastCursor)];
    return cursor;
}

+(NSCursor * __nullable)windowResizeNorthEastSouthWestCursor {
    NSCursor *cursor = [[NSCursor class]
        performSelector:@selector(_windowResizeNorthEastSouthWestCursor)];
    return cursor;
}

+ (NSData *)dataFromHexString:(NSString *)string

代价与风险:_windowResizeNorthWestSouthEastCursor 与 _windowResizeNorthEastSouthWestCursor 都是私有 API。代码没有先检查 respondsToSelector:。selector 消失会触发未识别 selector 崩溃。审核风险很高。

证据链 · #14-07已发布 · 仍可用
源码分析
已在 TelegramSwift 固定提交中逐行确认,共 1 处源码位置。
报告摘要
怎么做到:performSelector: 调用类方法。
REA 第 4 项
已发布 · 仍可用REA 二进制macOS 26.7 实测

13.0 二进制:两个 selector 都在 __objc_methname(0x1050992e1、0x105099307)

macOS 26.7:LLDB 在 AppKit 中解析到 +[NSCursor _windowResizeNorthWestSouthEastCursor] 等两个方法

结论:已发布,仍可用

8. 按私有 MKZoomSegmentedControl 类名移动地图缩放控件

#14-08TelegramSwiftAppKit报告精选 14-7源码分析REA 二进制macOS 26.7 实测REA 运行时观测私有 API

做了什么:代码在每次 layout 后扫描 MKMapView 的内部树。代码找到 MKZoomSegmentedControl。代码把它固定到 (20, 20)。代码在调整 map frame 时暂时置空 delegate。

为什么这样做:自定义地图面板会改变地图可见区域。系统控件的默认位置会与头部和列表冲突。公开 MapKit API 不提供此缩放控件的布局对象。

关键代码摘录

LocationModalController.swift · L231–L246
let delegate = mapView.delegate
mapView.delegate = nil
mapView.frame = NSMakeRect(0, mapY, frame.width, mapHeight)
mapView.delegate = delegate

let pinPoint = mapView.focus(NSMakeSize(4, 4))
locationPinView.centerX(y: pinPoint.midY - locationPinView.frame.height)

expandButton.center()
loadingView.center()

let zoomControls = HackUtils.findElements(byClass: "MKZoomSegmentedControl", in: mapView).first as? NSView
if let zoomControls = zoomControls {
    zoomControls.setFrameOrigin(20, 20)
}

代价与风险:MKZoomSegmentedControl 是内部类名。控件可能不存在,或在新版 MapKit 改名。短暂清空 delegate 还可能丢失重设 frame 期间的回调。审核风险存在。

证据链 · #14-08系统结构已变化
源码分析
已在 TelegramSwift 固定提交中逐行确认,共 1 处源码位置。
报告摘要
怎么做到:findElements(byClass:) 定位控件后 setFrameOrigin。
REA 第 8 项
系统结构已变化REA 二进制macOS 26.7 实测

13.0 二进制:__cstring 0x104c3bd50

macOS 26.7:类存在,但设置 showsZoomControls = true 后不在 MKMapView 视图树中

结论:代码用 if let 保护,会静默跳过(本次测试条件下)

REA 第 18 项
版本差异REA 二进制REA 运行时观测

13.0 二进制:HackUtils 出现 0 次;ObjcUtils 类存在,带 findElementsByClass:inView: 和 :array:

macOS 26.7:ObjcUtils 方法因符号已剥离,LLDB 无法下断

结论:版本差异:功能可能并入 ObjcUtils(未验证)

9. 用 CATransaction 包裹系统全屏切换,并延迟发布状态

#14-09TelegramSwiftAppKit源码分析Core Animation性能与内存

做了什么:Window 在调用 super.toggleFullScreen 前后显式开关 Core Animation 事务。它允许 processFullScreen 先介入。它把保存状态写在实际切换后。无介入器时,它下一轮主队列才发布信号。

为什么这样做:直接调用 super 无法让应用插入额外的全屏流程。这个封装也把“请求切换”和“状态流更新”分开。调用方可在系统动画前安排自己的界面状态。

关键代码摘录

Window.swift · L936–L958
public var processFullScreen: ((Bool, @escaping (Bool) -> Void) -> Void)?

private func invokeFullScreen(_ sender: Any?) {
    CATransaction.begin()
    super.toggleFullScreen(sender)
    CATransaction.commit()
    saver?.isFullScreen = isFullScreen
}

open override func toggleFullScreen(_ sender: Any?) {
    let newValue = !isFullScreen
    self.onToggleFullScreen?(newValue)
    if let process = processFullScreen {
        process(newValue, { [weak self] value in
            self?.invokeFullScreen(nil)
            self?.onToggleFullScreen?(newValue)
        })
    } else {
        invokeFullScreen(nil)
        DispatchQueue.main.async {
            self.isFullScreenValue.set(newValue)
        }
    }
}

代价与风险:未验证 CATransaction 是否针对特定 macOS bug。回调闭包忽略参数 value。若 processFullScreen 不回调,窗口不会切换。异步状态还可能与系统拒绝的切换短暂不一致。

证据链 · #14-09未单独核对
源码分析
已在 TelegramSwift 固定提交中逐行确认,共 1 处源码位置。
二进制验证
本条没有单独核对。REA 专题列出了已核对的 20 项。

10. 混用 IOPS 摘要和 AppleSmartBattery 注册表私有键

做了什么:代码先用公开 IOPS 获取充满或耗尽剩余时间。代码再连接 IOKit 的 AppleSmartBattery 服务。代码直接读取容量、循环次数、温度和设计循环数等键。它把电流与电压换算为带正负号的瓦数。

为什么这样做:IOPS 的公开摘要没有这些健康指标。注册表键使 Telegram 能显示或使用更详细的内部电池信息。代码每分钟新建 InternalFinder 后读取一次。

关键代码摘录

InternalFinder.swift · L32–L53 等 2 处
fileprivate func open() {
    self.serviceInternal = IOServiceGetMatchingService(
        kIOMasterPortDefault, IOServiceMatching("AppleSmartBattery"))
}

public func getInternalBattery() -> InternalBattery? {
    self.open()
    if self.serviceInternal == 0 {
        return nil
    }
    let battery = self.getBatteryData()
    self.close()
    return battery
}

battery.currentCapacity = self.getIntValue("CurrentCapacity" as CFString)
battery.maxCapacity = self.getIntValue("MaxCapacity" as CFString)
battery.designCapacity = self.getIntValue("DesignCapacity" as CFString)
battery.cycleCount = self.getIntValue("CycleCount" as CFString)
battery.designCycleCount = self.getIntValue("DesignCycleCount9C" as CFString)
battery.acPowered = self.getBoolValue("ExternalConnected" as CFString)
battery.isCharging = self.getBoolValue("IsCharging" as CFString)
battery.temperature = self.getTemperature()

代价与风险:AppleSmartBattery 及 DesignCycleCount9C 等键没有稳定公开契约。不同机型可能缺键或返回不同类型。getTemperature 和制造日期处的强制转换也可能崩溃。维护风险高。

证据链 · #14-10已发布 · 仍可用
源码分析
已在 TelegramSwift 固定提交中逐行确认,共 2 处源码位置。
REA 第 11 项
已发布 · 仍可用REA 二进制REA 运行时观测

13.0 二进制:IOHIDSystem、HIDIdleTime、AppleSmartBattery;导入 IOServiceGetMatchingService、IORegistryEntryCreateCFProperty、IOPSCopyPowerSourcesInfo

macOS 26.7:启动时 IOServiceGetMatchingService 被调用 6 次,调用栈中没有 Telegram 帧

结论:已发布

11. 用 Carbon 键盘布局翻译物理键,并重试非 Unicode 布局

#14-11TelegramSwiftAppKit源码分析REA 二进制文本与输入

做了什么:代码先映射功能键和修饰键。普通物理键则从当前 ASCII 键盘布局取得 UCKeyboardLayout。代码把 Cocoa 修饰键转成 Carbon 标志。代码用 UCKeyTranslate 得到用户当前布局下的快捷键文案。

为什么这样做:直接显示 key code 会让非 US 键盘难以理解。普通 NSEvent 的字符也不适合配置快捷键的物理键表达。代码还明确尝试规避中文和日文非 Unicode 布局崩溃。

关键代码摘录

HotKeyUtilities.m · L89–L129
NSString *mapped = [characterMap objectForKey:@(keyCode)];
if (mapped != nil) {
    [final appendString:mapped];
} else {
    TISInputSourceRef currentKeyboard = TISCopyCurrentASCIICapableKeyboardLayoutInputSource();
    CFDataRef uchr = (CFDataRef)TISGetInputSourceProperty(
        currentKeyboard, kTISPropertyUnicodeKeyLayoutData);
    if (!uchr) {
        CFRelease(currentKeyboard);
        currentKeyboard = TISCopyCurrentASCIICapableKeyboardLayoutInputSource();
        uchr = (CFDataRef)TISGetInputSourceProperty(
            currentKeyboard, kTISPropertyUnicodeKeyLayoutData);
    }
    const UCKeyboardLayout *keyboardLayout =
        (const UCKeyboardLayout *)CFDataGetBytePtr(uchr);
    if (keyboardLayout) {
        UInt32 keyModifiers = CarbonModifierFlagsFromCocoaModifiers(modifiers);
        OSStatus status = UCKeyTranslate(keyboardLayout, keyCode,
            kUCKeyActionDown, keyModifiers, LMGetKbdType(), 0,
            &deadKeyState, maxStringLength, &actualStringLength, unicodeString);

代价与风险:Carbon 是旧接口。第二次调用仍使用同一个获取函数。若 uchr 仍为空,代码仍传入 CFDataGetBytePtr。该保护是否足够未验证。

证据链 · #14-11已发布 · 仍可用
源码分析
已在 TelegramSwift 固定提交中逐行确认,共 1 处源码位置。
REA 第 13 项
已发布 · 仍可用REA 二进制

13.0 二进制:导入 TISCopyCurrentKeyboardInputSource、TISCopyCurrentASCIICapableKeyboardLayoutInputSource、UCKeyTranslate

macOS 26.7:未执行

结论:已发布

12. 把可能阻塞的输入监控授权移出主线程

#14-12TelegramSwiftAppKit源码分析性能与内存

做了什么:无提示检查同步返回授权状态。需要提示时,代码在用户发起队列调用 IOHIDRequestAccess。代码立即返回 NO。10.15 以前,它回退到辅助功能授权。另一方法还拼接系统设置深链。

为什么这样做:源码注释说明带提示的调用会阻塞。把它移出主线程可避免按下“启用按住说话”时卡住界面。普通同步提示会直接阻塞交互线程。

关键代码摘录

HotKeyUtilities.m · L150–L176
+ (BOOL)checkInputMonitoringWithPrompt:(BOOL)prompt {
    if (@available(macOS 10.15, *)) {
        static const IOHIDRequestType accessType = kIOHIDRequestTypeListenEvent;
        if (prompt) {
            dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
                IOHIDRequestAccess(accessType);
            });
            return NO;
        } else {
            return kIOHIDAccessTypeGranted == IOHIDCheckAccess(accessType);
        }
    } else {
        return [PermissionsManager checkAccessibilityWithPrompt:prompt];
    }
}

+ (NSURL *)securitySettingsUrlForKey:(NSString *)key {
    return [NSURL URLWithString:[NSString stringWithFormat:
        @"x-apple.systempreferences:com.apple.preference.security?%@", key]];
}

代价与风险:提示开始与返回值不同步。调用方必须再次检查状态。x-apple.systempreferences 参数不是稳定公开 URL 契约。不同 macOS 设置页可能失效。

证据链 · #14-12未单独核对
源码分析
已在 TelegramSwift 固定提交中逐行确认,共 1 处源码位置。
二进制验证
本条没有单独核对。REA 专题列出了已核对的 20 项。

13. 直接用 POSIX 扩展属性存取 Finder 元数据

#14-13TelegramSwiftAppKit源码分析性能与内存

做了什么:代码先以空缓冲区查询属性长度。代码再分配精确长度的缓冲区并读取原始字节。它把 POSIX errno 包装成 Swift 错误。Dock 包用它读写 com.apple.FinderInfo 和 com.apple.ResourceFork。

为什么这样做:UserDefaults 和普通文件内容不能修改 Finder 的隐藏元数据。这个小封装让 Swift 代码能直接操作 macOS 文件系统属性。

关键代码摘录

XAttr.swift · L4–L29
public struct XAttr {
    public struct Error: Swift.Error {
        public let localizedDescription = String(utf8String: strerror(errno))
    }

    public static func set(named name: String, data: Data, atPath path: String) throws {
        if setxattr(path, name, (data as NSData).bytes, data.count, 0, 0) == -1 {
            throw Error()
        }
    }

    public static func remove(named name: String, atPath path: String) throws {
        if removexattr(path, name, 0) == -1 {
            throw Error()
        }
    }

    public static func get(named name: String, atPath path: String) throws -> Data {
        let bufLength = getxattr(path, name, nil, 0, 0, 0)
        guard bufLength != -1, let buf = malloc(bufLength),
              getxattr(path, name, buf, bufLength, 0, 0) != -1 else { throw Error() }
        return Data(bytes: buf, count: bufLength)
    }
}

代价与风险:该函数中没有 free(buf)。Data(bytes:count:) 是否复制该缓冲区需结合 SDK 实现确认。若它采用复制语义,这里会每次读取泄漏一块内存,未验证。扩展属性还会因卷格式、权限和沙盒而失败。

证据链 · #14-13未单独核对
源码分析
已在 TelegramSwift 固定提交中逐行确认,共 1 处源码位置。
二进制验证
本条没有单独核对。REA 专题列出了已核对的 20 项。

14. 从 IOHIDSystem 读取原始空闲纳秒数

#14-14TelegramSwiftAppKit源码分析REA 二进制REA 运行时观测文本与输入

做了什么:代码遍历 IOHIDSystem。代码读取注册表键 HIDIdleTime。代码把读出的数右移 30 位。通知锁定逻辑用该值与超时比较。在线状态还要求该值小于 30 秒。

为什么这样做:NSApp.isActive 不能说明用户是否长期未触碰输入设备。HID 空闲时间允许应用锁和在线状态使用同一个系统级活动信号。

关键代码摘录

ObjcUtils.m · L1235–L1257
int64_t SystemIdleTime(void) {
    int64_t idlesecs = -1;
    io_iterator_t iter = 0;
    if (IOServiceGetMatchingServices(kIOMasterPortDefault,
            IOServiceMatching("IOHIDSystem"), &iter) == KERN_SUCCESS) {
        io_registry_entry_t entry = IOIteratorNext(iter);
        if (entry) {
            CFMutableDictionaryRef dict = NULL;
            if (IORegistryEntryCreateCFProperties(entry, &dict,
                    kCFAllocatorDefault, 0) == KERN_SUCCESS) {
                CFNumberRef obj = CFDictionaryGetValue(dict, CFSTR("HIDIdleTime"));
                if (obj) {
                    int64_t nanoseconds = 0;
                    if (CFNumberGetValue(obj, kCFNumberSInt64Type, &nanoseconds)) {
                        idlesecs = (nanoseconds >> 30);
                    }
                }
                CFRelease(dict);
            }
            IOObjectRelease(entry);
        }
        IOObjectRelease(iter);
    }
    return idlesecs;
}

代价与风险:HIDIdleTime 是注册表实现细节。源码注释称右移 30 位等于除以 10^9,但两者并不精确相等。超时边界会有约 7% 的偏差。不同硬件或系统可能缺少该键。

证据链 · #14-14已发布 · 仍可用
源码分析
已在 TelegramSwift 固定提交中逐行确认,共 1 处源码位置。
REA 第 11 项
已发布 · 仍可用REA 二进制REA 运行时观测

13.0 二进制:IOHIDSystem、HIDIdleTime、AppleSmartBattery;导入 IOServiceGetMatchingService、IORegistryEntryCreateCFProperty、IOPSCopyPowerSourcesInfo

macOS 26.7:启动时 IOServiceGetMatchingService 被调用 6 次,调用栈中没有 Telegram 帧

结论:已发布

15. 为 M2 后的泛化 hw.model 启动 system_profiler

#14-15TelegramSwiftAppKit源码分析REA 二进制

做了什么:代码不只读 sysctlbyname("hw.model")。它启动 Apple 的 system_profiler。代码请求 JSON 硬件摘要。代码从 machine_name 和 chip_type 合成展示用型号。外部命令失败后才回退到 sysctl。

为什么这样做:源码明确说明 M2 起的 hw.model 可能只给 Mac数字,数字。网络初始化需要更可读的设备名。普通 sysctl 回退不能解决这个型号退化。

关键代码摘录

TelegramSystem.swift · L4–L52
func deviceFromSystemProfiler() -> String? {
    // Starting with MacBook M2 the hw.model returns simply Mac[digits],[digits].
    let process = Process()
    if #available(macOS 10.13, *) {
        process.executableURL = URL(fileURLWithPath: "/usr/sbin/system_profiler")
    } else {
        process.launchPath = "/usr/sbin/system_profiler"
    }
    process.arguments = ["-json", "SPHardwareDataType", "-detailLevel", "mini"]
    let pipe = Pipe()
    process.standardOutput = pipe
    do {
        if #available(macOS 10.13, *) { try process.run() }
        else { process.launch() }
    } catch {
        return nil
    }
    process.waitUntilExit()
    let data = pipe.fileHandleForReading.readDataToEndOfFile()
    let document = try? JSONSerialization.jsonObject(with: data, options: [])
    guard let fields = (document as? [String: Any])?["SPHardwareDataType"] as? [[String: Any]],
          !fields.isEmpty else { return nil }

代价与风险:waitUntilExit() 会同步等待外部进程。system_profiler 可能很慢。JSON 键名和命令可用性也会变化。失败时会降级为空字符串。

证据链 · #14-15已发布 · 仍可用
源码分析
已在 TelegramSwift 固定提交中逐行确认,共 1 处源码位置。
REA 第 12 项
已发布 · 仍可用REA 二进制

13.0 二进制:/usr/sbin/system_profiler

macOS 26.7:未执行

结论:已发布

16. 用双时间戳文件做无信号处理器的崩溃哨兵

做了什么:启动时,代码检查名为 crashhandler 的文件。代码每次中间保存更新时间戳。下次启动时,文件只含一个时间戳,或两个时间戳相差小于 10 秒,都会被视为异常退出。应用代理启动后立即写入一次,并在终止时删除文件。

为什么这样做:这避开了崩溃 signal handler 的不安全上下文。它也不依赖外部崩溃上报服务。普通“是否正常退出”布尔值无法提供启动后的存活时间。

关键代码摘录

CrashHandler.swift · L11–L50
public func isCrashedLastTime(_ folder: String) -> Bool {
    let url = folder + "/" + "crashhandler"
    if let dateString = try? String(contentsOf: URL(fileURLWithPath: url)) {
        let components = dateString.components(separatedBy: " ")
        if components.count == 2 {
            let initedDate = Int32(components[0]) ?? 0
            let lastSavedDate = Int32(components[1]) ?? 0
            return lastSavedDate - initedDate < 10
        } else {
            return true
        }
    }
    return FileManager.default.fileExists(atPath: url)
}

public func crashIntermediateDate(_ folder: String) {
    let url = folder + "/" + "crashhandler"
    if let dateString = try? String(contentsOf: URL(fileURLWithPath: url)) {
        let time = "\(Int32(Date().timeIntervalSince1970))"
        var components = dateString.components(separatedBy: " ")
        if components.count == 2 { components[1] = time }
        else if components.count == 1 { components.append(time) }
        try? FileManager.default.removeItem(atPath: url)
        FileManager.default.createFile(atPath: url,
            contents: components.joined(separator: " ").data(using: .utf8), attributes: nil)

代价与风险:这是启发式算法,不是崩溃捕获。启动后 10 秒内正常退出会误报。文件写入失败被 try? 忽略。秒级 Int32 还存在 2038 年溢出风险。

证据链 · #14-16未单独核对
源码分析
已在 TelegramSwift 固定提交中逐行确认,共 1 处源码位置。
二进制验证
本条没有单独核对。REA 专题列出了已核对的 20 项。

17. 对 Spotlight 做稳定 ID 差分,而非全量重建索引

做了什么:每个 Spotlight 条目 ID 带帐户 ID 和 peer ID。代码维护前一份条目列表。代码计算删除、插入和更新集合。它只把插入和更新交给 indexSearchableItems。它只删除消失的标识符。应用收到 CSSearchableItemActionType 后,再按帐户路由至聊天。

为什么这样做:全量重建会制造不必要的系统索引 I/O。稳定 ID 还能安全地区分多个帐户的同一 peer。普通全量写入会放大频繁搜索历史变化的代价。

关键代码摘录

Spotlight.swift · L104–L157
let signal = combineLatest(queue: .mainQueue(), accountPeer, peers)
disposable.set(signal.start(next: { [weak self] accountPeer, peers in
    guard let `self` = self else { return }
    // `items` is rebuilt from the current peers.
    let (delete, insert, update) = mergeListsStableWithUpdates(
        leftList: self.previousItems, rightList: items)
    if !insert.isEmpty || !update.isEmpty {
        CSSearchableIndex.default().indexSearchableItems(
            insert.map { $0.1.item }, completionHandler: nil)
        CSSearchableIndex.default().indexSearchableItems(
            update.map { $0.1.item }, completionHandler: nil)
    }
    var deleted: [SpotlightItem] = []
    for index in delete.reversed() {
        deleted.append(self.previousItems.remove(at: index))
    }
    self.previousItems = items
    if !deleted.isEmpty {
        CSSearchableIndex.default().deleteSearchableItems(
            withIdentifiers: deleted.map { $0.stableId.stringValue }, completionHandler: nil)
    }
}))

代价与风险:两处上游 distinctUntilChanged 只比较 peer 数量。相同数量但内容变化时,信号可能被抑制。Spotlight 条目因此可能陈旧。该风险在同文件 #L104-L121 可见。

证据链 · #14-17未单独核对
源码分析
已在 TelegramSwift 固定提交中逐行确认,共 1 处源码位置。
二进制验证
本条没有单独核对。REA 专题列出了已核对的 20 项。

18. 把 Dock 徽标字符串当成状态栏图标的输入

做了什么:未读数先写入 NSDockTile.badgeLabel。状态栏图标更新函数随后再读取这个字符串。代码在资源队列生成图像。代码回主队列替换状态栏图片。它也按 macOS 版本和外观选择黑白颜色。

为什么这样做:这让 Dock 和状态栏从同一格式化后的字符串派生数值。它减少两套未读数格式化逻辑。后台生成图像也避免把绘制放在主线程。

关键代码摘录

GlobalBadgeNode.swift · L199–L201 等 2 处
if dockTile {
    NSApplication.shared.dockTile.badgeLabel = result.dockText
    forceUpdateStatusBarIconByDockTile(sharedContext: sharedContext)
}

func forceUpdateStatusBarIconByDockTile(sharedContext: SharedAccountContext) {
    if let count = Int(NSApplication.shared.dockTile.badgeLabel ?? "0") {
        var color: NSColor = .black
        if #available(OSX 10.14, *) {
            if systemAppearance.name != .aqua {
                color = .white
            }
        }
        if #available(OSX 11.0, *) {
            color = .white
        }
        resourcesQueue.async {
            let icon = generateStatusBarIcon(count, color: color)
            Queue.mainQueue().async {
                sharedContext.updateStatusBarImage(icon)
            }
        }
    }
}

代价与风险:badgeLabel 是展示属性,不是强类型状态存储。空字符串或非数字会使状态栏更新被跳过。Dock 被其他代码改写时,状态栏会跟随错误值。无明显私有 API 风险。

证据链 · #14-18未单独核对
源码分析
已在 TelegramSwift 固定提交中逐行确认,共 2 处源码位置。
二进制验证
本条没有单独核对。REA 专题列出了已核对的 20 项。

本页目录