Telegram 技巧档案UIKIT · APPKIT
iOS · UIKit

非 UI 内核

双重虚拟映射环形缓冲、共享 SQLite 保护与采样器。

本主题范围:Telegram-iOS 非 UI 核心:并发、存储、扩展、网络与构建。

结论:本次记录 15 条技巧。重点是虚拟内存、跨进程 SQLite、扩展时限和构建规避。审阅基准是 f1dd7a2dbd02cbbf513e75d5695d8d36d1cf5838。未发现本范围内混淆的私有 API 名。

1. 用双重虚拟映射消除环形缓冲区回绕

#11-01Telegram-iOS非 UI报告精选 11-1源码分析REA 二进制

做了什么:它先预留两倍页对齐地址空间。它再把第一段物理页映射到紧邻的第二段。任何跨越尾部的逻辑连续区间都变成连续指针。

为什么这样做:普通环形缓冲区会在回绕处拆成两段。调用方必须分支或复制两次。这里把成本放到初始化,热路径可单次 memcpy。

关键代码摘录

RingBuffer.m · L33–L105
buffer->length = (int32_t)round_page(length);
// Temporarily allocate twice the length, so we have the contiguous address space to
// support a second instance of the buffer directly after
vm_address_t bufferAddress;
kern_return_t result = vm_allocate(mach_task_self(),
                                   &bufferAddress,
                                   buffer->length * 2,
                                   VM_FLAGS_ANYWHERE);
result = vm_deallocate(mach_task_self(),
                       bufferAddress + buffer->length,
                       buffer->length);
// Re-map the buffer to the address space immediately after the buffer
vm_address_t virtualAddress = bufferAddress + buffer->length;
vm_prot_t cur_prot, max_prot;
result = vm_remap(mach_task_self(),
                  &virtualAddress,
                  buffer->length,
                  0,
                  0,
                  mach_task_self(),
                  bufferAddress,
                  0,
                  &cur_prot,
                  &max_prot,
                  VM_INHERIT_DEFAULT);

代价与风险:它依赖 Mach vm_remap 和地址空间连续性。初始化会重试三次。虚拟地址占用为容量的两倍。

证据链 · #11-01共享模块间接证据
源码分析
已在 Telegram-iOS 固定提交中逐行确认,共 1 处源码位置。
报告摘要
怎么做到:vm_allocate→vm_deallocate→vm_remap 构建镜像窗口。
REA 第 20 项
共享模块间接证据REA 二进制

13.0 二进制:导入 vm_allocate、vm_remap;RingBuffer 相关字符串 4 条

macOS 26.7:未执行

结论:已发布(间接)

2. 用“所有权栅栏”和三次阀门保护共享 SQLite

#11-02Telegram-iOS非 UI报告精选 11-2源码分析

做了什么:SqliteValueBox 只让主应用的读写连接删除数据库。它将连续可计数打开失败写入原子标记文件。它达到三次后,才同时删除主库、-shm 和 -wal。

为什么这样做:通知扩展会共享主库。锁屏数据保护、锁竞争和资源耗尽不表示库损坏。立即删库会把暂时打不开变成本地数据丢失。只删主库也会让旧 WAL 索引配对新 WAL。

关键代码摘录

SqliteValueBox.swift · L254–L303
/// Whether this connection may delete the database to recover from an error.
/// Only the owning read-write connection (the main app) may do so.
let removeDatabaseOnError: Bool
static let openFailureWipeThreshold = 3
private static func openFailuresPath(databasePath: String) -> String {
    return databasePath + "-open-failures"
}
static func shouldWipeAfterOpenFailure(
    databasePath: String, systemErrno: Int32
) -> Bool {
    guard shouldCountOpenFailure(systemErrno: systemErrno) else {
        return false
    }
    let failures = self.consecutiveOpenFailures(databasePath: databasePath) + 1
    let _ = try? Data(String(failures).utf8).write(
        to: URL(fileURLWithPath: self.openFailuresPath(databasePath: databasePath)),
        options: .atomic
    )
    return failures >= self.openFailureWipeThreshold
}
static func removeDatabaseFiles(databasePath: String) {
    for suffix in ["", "-shm", "-wal", "-open-failures"] {
        let _ = try? FileManager.default.removeItem(atPath: databasePath + suffix)
    }
}

代价与风险:主应用连续三次可计数失败时仍会重建空库。该策略优先避免误删,可能延迟真正损坏的恢复。

证据链 · #11-02iOS 未做二进制验证
源码分析
已在 Telegram-iOS 固定提交中逐行确认,共 1 处源码位置。
报告摘要
怎么做到:-open-failures 标记文件原子写入与阈值判断。
二进制验证
未做。App Store 版 IPA 受 FairPlay 加密,无法合法取得解密的可执行文件。

3. 采样 CPU 时手工解 PAC 并限制栈遍历

#11-03Telegram-iOS非 UI报告精选 11-3源码分析

做了什么:CpuProfiler 直接读取挂起线程的 arm64 寄存器和帧指针链。它先去掉 arm64e 返回地址的 PAC。它再用栈上下界、8 字节对齐和单调帧指针终止遍历。

为什么这样做:符号化工具不适合进程内高频采样。PAC 位未去除会使地址无法解析。未校验的帧链会读出栈外垃圾并崩溃。

关键代码摘录

CpuProfiler.mm · L42–L99
/// arm64e signs return addresses stored on the stack.
inline uintptr_t strippedPointer(uintptr_t value) {
#if __has_feature(ptrauth_calls)
    return (uintptr_t)ptrauth_strip((void *)value, ptrauth_key_return_address);
#else
    return value & 0x000000ffffffffffULL;
#endif
}
int walkStack(thread_t thread, uintptr_t stackLow, uintptr_t stackHigh,
              uintptr_t *frames) {
    arm_thread_state64_t state;
    mach_msg_type_number_t stateCount = ARM_THREAD_STATE64_COUNT;
    if (thread_get_state(thread, ARM_THREAD_STATE64,
                         (thread_state_t)&state, &stateCount) != KERN_SUCCESS) {
        return 0;
    }
    const uintptr_t pc = (uintptr_t)arm_thread_state64_get_pc(state);
    const uintptr_t lr = strippedPointer((uintptr_t)arm_thread_state64_get_lr(state));
    uintptr_t framePointer = (uintptr_t)arm_thread_state64_get_fp(state);
    if (framePointer < stackLow ||
        framePointer + 2 * sizeof(uintptr_t) > stackHigh) {
        break;
    }
    const uintptr_t returnAddress = strippedPointer(
        *(uintptr_t *)(framePointer + sizeof(uintptr_t)));

代价与风险:它使用 Mach 线程 API。采样必须短暂挂起其他线程。非 arm64 平台不采集调用栈。

证据链 · #11-03iOS 未做二进制验证
源码分析
已在 Telegram-iOS 固定提交中逐行确认,共 1 处源码位置。
报告摘要
怎么做到:ptrauth_strip,校验 fp 在界内且 8 字节对齐。
二进制验证
未做。App Store 版 IPA 受 FairPlay 加密,无法合法取得解密的可执行文件。

4. 通知扩展选择崩溃,而不是带着半事务继续

做了什么:通知扩展在启动处理器时开启 setValueBoxStrictErrorHandling(true)。SQLite 的 BEGIN、COMMIT 或 step 失败会使扩展进程失败。系统随后展示未修改的推送。

为什么这样做:该扩展与主应用跨进程共享账户数据库。静默失败会留下非原子状态。代码明确指出,这会让差异轮询永久活锁。

关键代码摘录

NotificationService.swift · L766–L778
private let notificationKeyDisposable = MetaDisposable()
private let pollDisposable = MetaDisposable()
// Handler creation is, for practical purposes, when the system's ~30 s extension
// budget started ticking; the connection deadline on the poll path counts from here.
private let startTimestamp = CFAbsoluteTimeGetCurrent()

init?(queue: Queue, episode: String, updateCurrentContent: @escaping (NotificationContent) -> Void,
      completed: @escaping () -> Void, requestIdentifier: String,
      payload: [AnyHashable: Any]) {
    self.queue = queue

    // In the extension, database BEGIN/COMMIT/step failures must crash (the system then
    // falls back to displaying the unmodified push) instead of failing silently: silent
    // failures under cross-process lock contention produce non-atomic writes and pin the
    // account state, turning the difference polling loop into an infinite livelock.
    setValueBoxStrictErrorHandling(true)

代价与风险:这是故意降低可用性的故障策略。用户可能看到原始通知。它避免的是更久的 CPU 占用和账户状态卡死。

证据链 · #11-04iOS 未做二进制验证
源码分析
已在 Telegram-iOS 固定提交中逐行确认,共 1 处源码位置。
报告摘要
怎么做到:初始化处理器即切换严格模式,失败即结束进程。
二进制验证
未做。App Store 版 IPA 受 FairPlay 加密,无法合法取得解密的可执行文件。

5. 将硬链接的多个路径合并为一个缓存淘汰单元

#11-05Telegram-iOS非 UI源码分析性能与内存

做了什么:清理器用 (st_dev, st_ino) 识别相同物理文件。第一个路径记录大小。额外硬链接只附加到同一临时数据库行。淘汰时会一起取消链接全部路径。

为什么这样做:一个下载完成文件可能有 <id> 和 <id>_partial 两个目录项。按路径累加会重复计算容量。只删一个路径也不会释放 inode 数据块。

关键代码摘录

TimeBasedCleanup.swift · L255–L284
var identity: FileIdentity?
var isAdditionalLink = false
if value.st_nlink > 1 {
    let fileIdentity = FileIdentity(
        device: UInt64(UInt32(bitPattern: value.st_dev)),
        inode: UInt64(value.st_ino)
    )
    identity = fileIdentity
    isAdditionalLink = !seenLinkedInodes.insert(fileIdentity).inserted
}

if isAdditionalLink {
    if performSizeMapping, let identity = identity {
        tempDatabase.addLink(pathBuffer: pathBuffer,
                             pathSize: strnlen(pathBuffer, 1024),
                             identity: identity)
    }
} else {
    result.totalSize += UInt64(value.st_size)
    if performSizeMapping {
        tempDatabase.add(pathBuffer: pathBuffer,
                         pathSize: strnlen(pathBuffer, 1024),
                         size: Int64(value.st_size),
                         timestamp: scanTimestamp(value.st_mtimespec.tv_sec),
                         identity: identity)

代价与风险:扫描需维护 Set<FileIdentity>。它只在 st_nlink > 1 时进入此路径。异常文件系统语义未在代码中验证。

证据链 · #11-05iOS 未做二进制验证
源码分析
已在 Telegram-iOS 固定提交中逐行确认,共 1 处源码位置。
二进制验证
未做。App Store 版 IPA 受 FairPlay 加密,无法合法取得解密的可执行文件。

6. 用无行 ID 表和持久语句实现紧凑 ValueBox

#11-06Telegram-iOS非 UI源码分析性能与内存

做了什么:ValueBox 按逻辑表 ID 动态创建 t<ID>。二进制主键表可使用 WITHOUT ROWID。它按表缓存查询语句,并以 SQLITE_PREPARE_PERSISTENT 编译。

为什么这样做:小型键值表不需要重复存储 SQLite 隐式行 ID。频繁读取也不应反复解析相同 SQL。一个对象复用语句前会 reset() 再绑定新键。

关键代码摘录

SqliteValueBox.swift · L898–L952
private func createTable(database: Database, table: ValueBoxTable) {
    switch table.keyType {
    case .binary:
        var createStatement =
            "CREATE TABLE IF NOT EXISTS t\(table.id) (key BLOB PRIMARY KEY, value BLOB)"
        if table.compactValuesOnCreation {
            createStatement += " WITHOUT ROWID"
        }
        let resultCode = database.execute(createStatement)
        assert(resultCode)
    }
}
if let statement = self.getStatements[table.id] {
    resultStatement = statement
} else {
    var statement: OpaquePointer? = nil
    let status = sqlite3_prepare_v3(self.database.handle,
        "SELECT value FROM t\(table.id) WHERE key=?", -1,
        SQLITE_PREPARE_PERSISTENT, &statement, nil)
    precondition(status == SQLITE_OK)
    let preparedStatement = SqlitePreparedStatement(statement: statement)
    self.getStatements[table.id] = preparedStatement
    resultStatement = preparedStatement
}
resultStatement.reset()

代价与风险:每个访问过的表会常驻预编译语句。动态 SQL 用表 ID 拼接。该 ID 来自内部 ValueBoxTable,而非外部字符串。

证据链 · #11-06iOS 未做二进制验证
源码分析
已在 Telegram-iOS 固定提交中逐行确认,共 1 处源码位置。
二进制验证
未做。App Store 版 IPA 受 FairPlay 加密,无法合法取得解密的可执行文件。

7. 显式关闭 SQLite mmap,并分拆持久化取舍

#11-07Telegram-iOS非 UI源码分析性能与内存

做了什么:禁用缓存的连接把 SQLite 页缓存限制为 32。它显式关闭 SQLite mmap。它组合 WAL、synchronous=NORMAL 和内存临时表。检查点仍要求 fullfsync。

为什么这样做:这组参数把运行时延迟、内存和持久化拆开取舍。useCaches 为假时,32 页限制尤其适合临时或扩展连接。代码未说明关闭 mmap_size 的具体系统缺陷,原因未验证。

关键代码摘录

SqliteValueBox.swift · L590–L628
if !self.useCaches {
    resultCode = database.execute("PRAGMA cache_size=32")
    assert(resultCode)
}
resultCode = database.execute("PRAGMA mmap_size=0")
assert(resultCode)
resultCode = database.execute("PRAGMA synchronous=NORMAL")
assert(resultCode)
resultCode = database.execute("PRAGMA temp_store=MEMORY")
assert(resultCode)
resultCode = database.execute("PRAGMA journal_mode=WAL")
assert(resultCode)
resultCode = database.execute("PRAGMA cipher_memory_security = OFF")
assert(resultCode)

self.pageSize = Int(self.runPragma(database, "page_size"))
// ...
let _ = self.runPragma(database, "checkpoint_fullfsync = 1")
assert(self.runPragma(database, "checkpoint_fullfsync") == "1")

代价与风险:synchronous=NORMAL 的单次事务耐久性低于 FULL。cipher_memory_security = OFF 也放宽了 SQLCipher 的内存擦除保护。参数兼容性依赖捆绑的 SQLite/SQLCipher。

证据链 · #11-07iOS 未做二进制验证
源码分析
已在 Telegram-iOS 固定提交中逐行确认,共 1 处源码位置。
二进制验证
未做。App Store 版 IPA 受 FairPlay 加密,无法合法取得解密的可执行文件。

8. MTProxy 与 Web 代理只探测一个地址

做了什么:对 MTProxy 或 Web 代理,发现逻辑选择单个地址。它优先选择 IPv4。只要配置了代理,它也禁用 80 和 5222 的备用端口探测。

为什么这样做:这两种代理会忽略或重解释目标地址。地址数乘端口数的探测不会增加信息量。不可达代理下,该扇出会放大通知扩展的重试风暴。

关键代码摘录

MTDiscoverConnectionSignals.m · L194–L225
if (proxySettings != nil &&
    (proxySettings.secret != nil || proxySettings.webProxy)) {
    // An MTProxy chooses the datacenter from the obfuscated header and a WEB relay
    // ignores the address it is handed, so every probe through either lands in the
    // same place: N of them carry exactly the information of one.
    MTDatacenterAddress *chosen = nil;
    for (MTDatacenterAddress *address in bestAddressList) {
        if (![self isIpv6:address.ip]) {
            chosen = address;
            break;
        }
    }
    if (chosen == nil) {
        chosen = bestAddressList.firstObject;
    }
    return chosen == nil ? @[] : @[chosen];
}
+ (NSArray<NSNumber *> *)alternatePortsForProxySettings:
    (MTSocksProxySettings *)proxySettings {
    if (proxySettings != nil) {
        return @[];
    }
    return @[@80, @5222];
}

代价与风险:单地址策略降低冗余。首选 IPv4 是代码策略,并不保证当前网络的最佳链路。

证据链 · #11-08iOS 未做二进制验证
源码分析
已在 Telegram-iOS 固定提交中逐行确认,共 1 处源码位置。
二进制验证
未做。App Store 版 IPA 受 FairPlay 加密,无法合法取得解密的可执行文件。

9. 用可取消的延迟递归实现指数退避

#11-09Telegram-iOS非 UI报告精选 11-5源码分析

做了什么:每轮完成后才从计时器回调发起下一轮。间隔按两倍增长,并封顶。MTMetaDisposable 覆盖当前轮。外部取消会设置原子标记并处置当前订阅。

为什么这样做:同步递归会增长调用栈。固定短间隔会在断网时持续制造连接。这个实现不保留一串旧轮次,并让取消立即切断当前工作。

关键代码摘录

MTDiscoverConnectionSignals.m · L228–L257
+ (void)_startBackoffRoundOf:(MTSignal *)signal
                       delay:(NSTimeInterval)delay
                    maxDelay:(NSTimeInterval)maxDelay
                       queue:(MTQueue *)queue
                  subscriber:(MTSubscriber *)subscriber
           currentDisposable:(MTMetaDisposable *)currentDisposable
                  isDisposed:(MTAtomic *)isDisposed {
    if ([[isDisposed value] boolValue]) {
        return;
    }
    NSTimeInterval nextDelay = MIN(delay * 2.0, maxDelay);
    MTSignal *round = [signal then:[[MTSignal complete] delay:delay onQueue:queue]];
    // The recursion happens from the delay timer's completion, so it never grows the stack.
    [currentDisposable setDisposable:[round startWithNext:^(id next) {
        [subscriber putNext:next];
    } error:^(id error) {
        [subscriber putError:error];
    } completed:^{
        [self _startBackoffRoundOf:signal delay:nextDelay maxDelay:maxDelay
            queue:queue subscriber:subscriber
            currentDisposable:currentDisposable isDisposed:isDisposed];
    }]];
}

代价与风险:它不含抖动。多个客户端仍可能在相同退避节拍重试。

证据链 · #11-09iOS 未做二进制验证
源码分析
已在 Telegram-iOS 固定提交中逐行确认,共 1 处源码位置。
报告摘要
怎么做到:then(delay:) 串联,MTMetaDisposable 覆盖当前订阅。
二进制验证
未做。App Store 版 IPA 受 FairPlay 加密,无法合法取得解密的可执行文件。

10. 到期时靠释放队列本地对象取消整个通知管线

做了什么:serviceExtensionTimeWillExpire() 不逐项停止网络任务。它将 QueueLocalObject 置空。处理器析构时会释放 MetaDisposable,从而处置轮询并断开保活连接。它再提交最新内容或原始内容。

为什么这样做:iOS 收回扩展时间不等于立即杀进程。若只调用内容回调,差异同步仍可能消耗 CPU。对象所有权成为整条异步管线的取消开关。

关键代码摘录

NotificationService.swift · L2748–L2765
override func serviceExtensionTimeWillExpire() {
    // Releasing impl deallocates the handler on its queue, which disposes the in-flight
    // poll (getDifference/state replay) and drops the keep-alive network connection.
    // Without this, that work keeps running after the system takes back control and
    // continues to consume CPU for as long as the extension process stays alive.
    self.impl = nil

    if let contentHandler = self.contentHandler {
        self.contentHandler = nil

        Logger.shared.log("NotificationService \(self.episode ?? \"???\")",
                          "Completing due to serviceExtensionTimeWillExpire")

        if let content = self.content.with({ $0 }) {
            contentHandler(content.generate())
        } else if let initialContent = self.initialContent {
            contentHandler(initialContent)
        }
    }
}

代价与风险:该策略依赖析构链正确释放所有订阅。若新增任务未受处理器持有,它不会自动取消。

证据链 · #11-10iOS 未做二进制验证
源码分析
已在 Telegram-iOS 固定提交中逐行确认,共 1 处源码位置。
二进制验证
未做。App Store 版 IPA 受 FairPlay 加密,无法合法取得解密的可执行文件。

11. 将名为 async 的队列调用改为同队列内联

#11-11Telegram-iOS非 UI源码分析性能与内存

做了什么:Queue 为串行队列设置 DispatchSpecificKey。在目标队列内,它让 async 和 sync 都直接执行闭包。主队列则用 Thread.isMainThread 识别。

为什么这样做:同队列 sync 会死锁。无条件 async 也会增加一次排队延迟。这个封装统一保持队列亲和性,并移除不必要跳转。

关键代码摘录

Queue.swift · L42–L81
public init(name: String? = nil, qos: DispatchQoS = .default) {
    self.nativeQueue = DispatchQueue(label: name ?? "", qos: qos)
    self.specialIsMainQueue = false
    self.nativeQueue.setSpecific(key: QueueSpecificKey, value: self.specific)
}

public func isCurrent() -> Bool {
    if DispatchQueue.getSpecific(key: QueueSpecificKey) === self.specific {
        return true
    } else if self.specialIsMainQueue && Thread.isMainThread {
        return true
    } else {
        return false
    }
}

public func async(_ f: @escaping () -> Void) {
    if self.isCurrent() {
        f()
    } else {
        self.nativeQueue.async(execute: f)
    }
}

代价与风险:async 不再保证异步边界。调用者若依赖延后执行或重入隔离,会得到反直觉行为。

证据链 · #11-11iOS 未做二进制验证
源码分析
已在 Telegram-iOS 固定提交中逐行确认,共 1 处源码位置。
二进制验证
未做。App Store 版 IPA 受 FairPlay 加密,无法合法取得解密的可执行文件。

12. 用启动时间加运行时长实现不可回拨的自动锁

#11-12Telegram-iOS非 UI源码分析运行时 hack文本与输入

做了什么:锁状态保存启动时间和开机运行秒数。若设备已重启,它立即锁定。未重启时,它使用 uptime 比较超时,不使用可被用户调整的墙上时间。

为什么这样做:普通 Date 差值会受手动改时和时区变化影响。单独保存 uptime 又无法跨重启比较。这个二元时间戳同时处理两种情况。

关键代码摘录

AppLock.swift · L18–L39
private func isLocked(passcodeSettings: PresentationPasscodeSettings,
                      state: LockState) -> Bool {
    if state.isManuallyLocked {
        return true
    } else if let autolockTimeout = passcodeSettings.autolockTimeout {
        var bootTimestamp: Int32 = 0
        let uptime = getDeviceUptimeSeconds(&bootTimestamp)
        let timestamp = MonotonicTimestamp(bootTimestamp: bootTimestamp,
                                           uptime: uptime)

        let applicationActivityTimestamp = state.applicationActivityTimestamp

        if let applicationActivityTimestamp = applicationActivityTimestamp {
            if timestamp.bootTimestamp != applicationActivityTimestamp.bootTimestamp {
                return true
            }
            if timestamp.uptime >= applicationActivityTimestamp.uptime + autolockTimeout {
                return true
            }
        } else {
            return true
        }
    }
    return false
}

代价与风险:getDeviceUptimeSeconds 通过 KERN_BOOTTIME 和 time() 计算。代码未验证系统时钟异常时的边界表现。

证据链 · #11-12iOS 未做二进制验证
源码分析
已在 Telegram-iOS 固定提交中逐行确认,共 1 处源码位置。
二进制验证
未做。App Store 版 IPA 受 FairPlay 加密,无法合法取得解密的可执行文件。

13. 先累积消息索引,再批量更新未读状态

#11-13Telegram-iOS非 UI源码分析性能与内存

做了什么:每个 peer 的操作循环复用一个键、写缓冲和编码器。它将新增入站消息索引放进 Set。它只在遇到次序敏感操作或循环末尾时批量调用 addIncomingMessages。

为什么这样做:连续插入若逐条改未读状态,会重复更新同一派生结构。集合还会消除重复索引。批量提交保留后续删除和更新时间戳操作所需的顺序边界。

关键代码摘录

MessageHistoryTable.swift · L234–L260
let sharedKey = self.key(MessageIndex(
    id: MessageId(peerId: PeerId(0), namespace: 0, id: 0), timestamp: 0
))
let sharedBuffer = WriteBuffer()
let sharedEncoder = PostboxEncoder()
var outputOperations: [MessageHistoryOperation] = []
var accumulatedRemoveIndices: [(MessageIndex)] = []
var accumulatedAddedIncomingMessageIndices = Set<MessageIndex>()
var updatedCombinedState: CombinedPeerReadState?
var invalidateReadState = false

let commitAccumulatedAddedIndices: () -> Void = {
    if !accumulatedAddedIncomingMessageIndices.isEmpty {
        let (combinedState, invalidate) = self.readStateTable.addIncomingMessages(
            peerId, indices: accumulatedAddedIncomingMessageIndices
        )
        if let combinedState = combinedState {
            updatedCombinedState = combinedState
        }
        if invalidate {
            invalidateReadState = true
        }
        accumulatedAddedIncomingMessageIndices.removeAll()
    }
}

代价与风险:这段状态机很依赖每个 switch 分支的刷新时机。新增操作类型时容易漏掉 commitAccumulatedAddedIndices()。

证据链 · #11-13iOS 未做二进制验证
源码分析
已在 Telegram-iOS 固定提交中逐行确认,共 1 处源码位置。
二进制验证
未做。App Store 版 IPA 受 FairPlay 加密,无法合法取得解密的可执行文件。

14. 在新启动 VM 中提前挂载 Metal 工具链

#11-14Telegram-iOS非 UI源码分析Metal / GPU

做了什么:Tart 构建脚本在 Bazel 前串行挂载 Xcode 26 的 Metal cryptex。它检测 xcrun metal 失败后,用软链接覆盖 Xcode 默认工具链内的同名 stub。

为什么这样做:懒挂载与约 17 个并发 MetalCompile 动作竞争。并且 xcrun 会找到错误的 metal stub。脚本把两个失败条件都前置处理。

关键代码摘录

TartBuild.py · L968–L998
# Since Xcode 26 the Metal toolchain is an optional download that only works while
# its cryptex DMG is mounted at /Volumes/MetalToolchainCryptex.
# Xcode remounts it lazily, which races when bazel starts ~17 MetalCompile actions.
# Mounting it once, serially, before bazel runs removes the race.
guest_build_sh = '''
    set -x
    set -e

    if [ ! -d /Volumes/MetalToolchainCryptex ]; then
        METAL_DMG="$(ls -t /System/Library/AssetsV2/com_apple_MobileAsset_MetalToolchain/*.asset/AssetData/Restore/*.dmg 2>/dev/null | head -n 1)"
        if [ -n "$METAL_DMG" ]; then
            hdiutil attach "$METAL_DMG" -mountpoint /Volumes/MetalToolchainCryptex -nobrowse -quiet || true
        fi
    fi
    METAL_REAL=/Volumes/MetalToolchainCryptex/Metal.xctoolchain/usr/bin/metal
    if [ -x "$METAL_REAL" ] && ! xcrun metal --version >/dev/null 2>&1; then
        sudo ln -sf "$METAL_REAL" "$(xcode-select -p)/Toolchains/XcodeDefault.xctoolchain/usr/bin/metal" || true
    fi
    xcrun metal --version || true
    xcrun -f metallib || true
'''

代价与风险:它修改 VM 内 Xcode 工具链路径。脚本以 || true 容错,可能掩盖真正的工具链安装问题。该技巧依赖 Xcode 26 的目录布局。

证据链 · #11-14iOS 未做二进制验证
源码分析
已在 Telegram-iOS 固定提交中逐行确认,共 1 处源码位置。
二进制验证
未做。App Store 版 IPA 受 FairPlay 加密,无法合法取得解密的可执行文件。

15. 从 Bazel 图生成 SwiftPM 的影子源码树

#11-15Telegram-iOS非 UI源码分析

做了什么:脚本读取 Bazel 输出的模块清单。它在 spm-files 建立模块目录,但普通源码和 bazel-out 生成源码都只创建相对软链接。它拒绝不在声明模块目录中的输入。

为什么这样做:SwiftPM 能直接索引实际源文件,不必复制海量源码。相对链接也让生成树整体可移动。路径校验防止模块清单意外把无关文件带入包。

关键代码摘录

generate_spm.py · L333–L378
for source in module["sources"] + module.get("hdrs", []) + module.get("textual_hdrs", []):
    # Process all sources (both regular and generated) with symlinks
    if source.startswith("bazel-out/"):
        if module["path"] in source:
            source_file_name = source[source.index(module["path"]) + len(module["path"]) + 1:]
        else:
            print("Source {} is not inside module path {}".format(source, module["path"]))
            sys.exit(1)
    else:
        if not source.startswith(module["path"]):
            print("Source {} is not inside module path {}".format(source, module["path"]))
            sys.exit(1)
        source_file_name = source[len(module["path"]) + 1:]

    symlink_location = os.path.join(module_directory, source_file_name)
    symlink_parent = os.path.dirname(symlink_location)
    create_spm_directory(symlink_parent)

    num_parent_dirs = symlink_location.count(os.path.sep)
    relative_prefix = "".join(["../"] * num_parent_dirs)
    symlink_target = relative_prefix + source
    link_spm_file(symlink_target, symlink_location)

代价与风险:IDE 与工具必须正确跟随软链接。生成目录与 Bazel 图不同步时需要重新运行脚本。该脚本对路径层级的计数实现有维护成本。

证据链 · #11-15iOS 未做二进制验证
源码分析
已在 Telegram-iOS 固定提交中逐行确认,共 1 处源码位置。
二进制验证
未做。App Store 版 IPA 受 FairPlay 加密,无法合法取得解密的可执行文件。

本页目录