非 UI 内核
双重虚拟映射环形缓冲、共享 SQLite 保护与采样器。
本主题范围:Telegram-iOS 非 UI 核心:并发、存储、扩展、网络与构建。
结论:本次记录 15 条技巧。重点是虚拟内存、跨进程 SQLite、扩展时限和构建规避。审阅基准是 f1dd7a2dbd02cbbf513e75d5695d8d36d1cf5838。未发现本范围内混淆的私有 API 名。
1. 用双重虚拟映射消除环形缓冲区回绕
做了什么:它先预留两倍页对齐地址空间。它再把第一段物理页映射到紧邻的第二段。任何跨越尾部的逻辑连续区间都变成连续指针。
为什么这样做:普通环形缓冲区会在回绕处拆成两段。调用方必须分支或复制两次。这里把成本放到初始化,热路径可单次 memcpy。
关键代码摘录
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 和地址空间连续性。初始化会重试三次。虚拟地址占用为容量的两倍。
- 源码分析
- 已在 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
做了什么:SqliteValueBox 只让主应用的读写连接删除数据库。它将连续可计数打开失败写入原子标记文件。它达到三次后,才同时删除主库、-shm 和 -wal。
为什么这样做:通知扩展会共享主库。锁屏数据保护、锁竞争和资源耗尽不表示库损坏。立即删库会把暂时打不开变成本地数据丢失。只删主库也会让旧 WAL 索引配对新 WAL。
关键代码摘录
/// 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)
}
}代价与风险:主应用连续三次可计数失败时仍会重建空库。该策略优先避免误删,可能延迟真正损坏的恢复。
- 源码分析
- 已在 Telegram-iOS 固定提交中逐行确认,共 1 处源码位置。
- 报告摘要
- 怎么做到:
-open-failures标记文件原子写入与阈值判断。 - 二进制验证
- 未做。App Store 版 IPA 受 FairPlay 加密,无法合法取得解密的可执行文件。
3. 采样 CPU 时手工解 PAC 并限制栈遍历
做了什么:CpuProfiler 直接读取挂起线程的 arm64 寄存器和帧指针链。它先去掉 arm64e 返回地址的 PAC。它再用栈上下界、8 字节对齐和单调帧指针终止遍历。
为什么这样做:符号化工具不适合进程内高频采样。PAC 位未去除会使地址无法解析。未校验的帧链会读出栈外垃圾并崩溃。
关键代码摘录
/// 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 平台不采集调用栈。
- 源码分析
- 已在 Telegram-iOS 固定提交中逐行确认,共 1 处源码位置。
- 报告摘要
- 怎么做到:
ptrauth_strip,校验fp在界内且 8 字节对齐。 - 二进制验证
- 未做。App Store 版 IPA 受 FairPlay 加密,无法合法取得解密的可执行文件。
4. 通知扩展选择崩溃,而不是带着半事务继续
做了什么:通知扩展在启动处理器时开启 setValueBoxStrictErrorHandling(true)。SQLite 的 BEGIN、COMMIT 或 step 失败会使扩展进程失败。系统随后展示未修改的推送。
为什么这样做:该扩展与主应用跨进程共享账户数据库。静默失败会留下非原子状态。代码明确指出,这会让差异轮询永久活锁。
关键代码摘录
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 占用和账户状态卡死。
- 源码分析
- 已在 Telegram-iOS 固定提交中逐行确认,共 1 处源码位置。
- 报告摘要
- 怎么做到:初始化处理器即切换严格模式,失败即结束进程。
- 二进制验证
- 未做。App Store 版 IPA 受 FairPlay 加密,无法合法取得解密的可执行文件。
5. 将硬链接的多个路径合并为一个缓存淘汰单元
做了什么:清理器用 (st_dev, st_ino) 识别相同物理文件。第一个路径记录大小。额外硬链接只附加到同一临时数据库行。淘汰时会一起取消链接全部路径。
为什么这样做:一个下载完成文件可能有 <id> 和 <id>_partial 两个目录项。按路径累加会重复计算容量。只删一个路径也不会释放 inode 数据块。
关键代码摘录
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 时进入此路径。异常文件系统语义未在代码中验证。
- 源码分析
- 已在 Telegram-iOS 固定提交中逐行确认,共 1 处源码位置。
- 二进制验证
- 未做。App Store 版 IPA 受 FairPlay 加密,无法合法取得解密的可执行文件。
6. 用无行 ID 表和持久语句实现紧凑 ValueBox
做了什么:ValueBox 按逻辑表 ID 动态创建 t<ID>。二进制主键表可使用 WITHOUT ROWID。它按表缓存查询语句,并以 SQLITE_PREPARE_PERSISTENT 编译。
为什么这样做:小型键值表不需要重复存储 SQLite 隐式行 ID。频繁读取也不应反复解析相同 SQL。一个对象复用语句前会 reset() 再绑定新键。
关键代码摘录
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,而非外部字符串。
- 源码分析
- 已在 Telegram-iOS 固定提交中逐行确认,共 1 处源码位置。
- 二进制验证
- 未做。App Store 版 IPA 受 FairPlay 加密,无法合法取得解密的可执行文件。
7. 显式关闭 SQLite mmap,并分拆持久化取舍
做了什么:禁用缓存的连接把 SQLite 页缓存限制为 32。它显式关闭 SQLite mmap。它组合 WAL、synchronous=NORMAL 和内存临时表。检查点仍要求 fullfsync。
为什么这样做:这组参数把运行时延迟、内存和持久化拆开取舍。useCaches 为假时,32 页限制尤其适合临时或扩展连接。代码未说明关闭 mmap_size 的具体系统缺陷,原因未验证。
关键代码摘录
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。
- 源码分析
- 已在 Telegram-iOS 固定提交中逐行确认,共 1 处源码位置。
- 二进制验证
- 未做。App Store 版 IPA 受 FairPlay 加密,无法合法取得解密的可执行文件。
8. MTProxy 与 Web 代理只探测一个地址
做了什么:对 MTProxy 或 Web 代理,发现逻辑选择单个地址。它优先选择 IPv4。只要配置了代理,它也禁用 80 和 5222 的备用端口探测。
为什么这样做:这两种代理会忽略或重解释目标地址。地址数乘端口数的探测不会增加信息量。不可达代理下,该扇出会放大通知扩展的重试风暴。
关键代码摘录
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 是代码策略,并不保证当前网络的最佳链路。
- 源码分析
- 已在 Telegram-iOS 固定提交中逐行确认,共 1 处源码位置。
- 二进制验证
- 未做。App Store 版 IPA 受 FairPlay 加密,无法合法取得解密的可执行文件。
9. 用可取消的延迟递归实现指数退避
做了什么:每轮完成后才从计时器回调发起下一轮。间隔按两倍增长,并封顶。MTMetaDisposable 覆盖当前轮。外部取消会设置原子标记并处置当前订阅。
为什么这样做:同步递归会增长调用栈。固定短间隔会在断网时持续制造连接。这个实现不保留一串旧轮次,并让取消立即切断当前工作。
关键代码摘录
+ (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];
}]];
}代价与风险:它不含抖动。多个客户端仍可能在相同退避节拍重试。
- 源码分析
- 已在 Telegram-iOS 固定提交中逐行确认,共 1 处源码位置。
- 报告摘要
- 怎么做到:
then(delay:)串联,MTMetaDisposable覆盖当前订阅。 - 二进制验证
- 未做。App Store 版 IPA 受 FairPlay 加密,无法合法取得解密的可执行文件。
10. 到期时靠释放队列本地对象取消整个通知管线
做了什么:serviceExtensionTimeWillExpire() 不逐项停止网络任务。它将 QueueLocalObject 置空。处理器析构时会释放 MetaDisposable,从而处置轮询并断开保活连接。它再提交最新内容或原始内容。
为什么这样做:iOS 收回扩展时间不等于立即杀进程。若只调用内容回调,差异同步仍可能消耗 CPU。对象所有权成为整条异步管线的取消开关。
关键代码摘录
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)
}
}
}代价与风险:该策略依赖析构链正确释放所有订阅。若新增任务未受处理器持有,它不会自动取消。
- 源码分析
- 已在 Telegram-iOS 固定提交中逐行确认,共 1 处源码位置。
- 二进制验证
- 未做。App Store 版 IPA 受 FairPlay 加密,无法合法取得解密的可执行文件。
11. 将名为 async 的队列调用改为同队列内联
做了什么:Queue 为串行队列设置 DispatchSpecificKey。在目标队列内,它让 async 和 sync 都直接执行闭包。主队列则用 Thread.isMainThread 识别。
为什么这样做:同队列 sync 会死锁。无条件 async 也会增加一次排队延迟。这个封装统一保持队列亲和性,并移除不必要跳转。
关键代码摘录
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 不再保证异步边界。调用者若依赖延后执行或重入隔离,会得到反直觉行为。
- 源码分析
- 已在 Telegram-iOS 固定提交中逐行确认,共 1 处源码位置。
- 二进制验证
- 未做。App Store 版 IPA 受 FairPlay 加密,无法合法取得解密的可执行文件。
12. 用启动时间加运行时长实现不可回拨的自动锁
做了什么:锁状态保存启动时间和开机运行秒数。若设备已重启,它立即锁定。未重启时,它使用 uptime 比较超时,不使用可被用户调整的墙上时间。
为什么这样做:普通 Date 差值会受手动改时和时区变化影响。单独保存 uptime 又无法跨重启比较。这个二元时间戳同时处理两种情况。
关键代码摘录
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() 计算。代码未验证系统时钟异常时的边界表现。
- 源码分析
- 已在 Telegram-iOS 固定提交中逐行确认,共 1 处源码位置。
- 二进制验证
- 未做。App Store 版 IPA 受 FairPlay 加密,无法合法取得解密的可执行文件。
13. 先累积消息索引,再批量更新未读状态
做了什么:每个 peer 的操作循环复用一个键、写缓冲和编码器。它将新增入站消息索引放进 Set。它只在遇到次序敏感操作或循环末尾时批量调用 addIncomingMessages。
为什么这样做:连续插入若逐条改未读状态,会重复更新同一派生结构。集合还会消除重复索引。批量提交保留后续删除和更新时间戳操作所需的顺序边界。
关键代码摘录
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()。
- 源码分析
- 已在 Telegram-iOS 固定提交中逐行确认,共 1 处源码位置。
- 二进制验证
- 未做。App Store 版 IPA 受 FairPlay 加密,无法合法取得解密的可执行文件。
14. 在新启动 VM 中提前挂载 Metal 工具链
做了什么:Tart 构建脚本在 Bazel 前串行挂载 Xcode 26 的 Metal cryptex。它检测 xcrun metal 失败后,用软链接覆盖 Xcode 默认工具链内的同名 stub。
为什么这样做:懒挂载与约 17 个并发 MetalCompile 动作竞争。并且 xcrun 会找到错误的 metal stub。脚本把两个失败条件都前置处理。
关键代码摘录
# 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 的目录布局。
- 源码分析
- 已在 Telegram-iOS 固定提交中逐行确认,共 1 处源码位置。
- 二进制验证
- 未做。App Store 版 IPA 受 FairPlay 加密,无法合法取得解密的可执行文件。
15. 从 Bazel 图生成 SwiftPM 的影子源码树
做了什么:脚本读取 Bazel 输出的模块清单。它在 spm-files 建立模块目录,但普通源码和 bazel-out 生成源码都只创建相对软链接。它拒绝不在声明模块目录中的输入。
为什么这样做:SwiftPM 能直接索引实际源文件,不必复制海量源码。相对链接也让生成树整体可移动。路径校验防止模块清单意外把无关文件带入包。
关键代码摘录
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 图不同步时需要重新运行脚本。该脚本对路径层级的计数实现有维护成本。
- 源码分析
- 已在 Telegram-iOS 固定提交中逐行确认,共 1 处源码位置。
- 二进制验证
- 未做。App Store 版 IPA 受 FairPlay 加密,无法合法取得解密的可执行文件。