Telegram 技巧档案UIKIT · APPKIT
macOS · AppKit

渲染与应用层

LZFSE 贴纸缓存、共享解码上下文与 Metal 批量提交。

本主题范围:TelegramSwift 的 macOS 渲染、媒体与应用层技巧。结论:该客户端把动画贴纸、GIF、通话视频和特效统一为受限缓冲、按需调度和 GPU 合批的管线,共确认 18 条值得复用的技巧。

1. 用 XOR 帧差分再用 LZFSE 压缩贴纸像素缓存

做了什么:它把连续帧按 64 位字进行 XOR。它再把差分结果写为 LZFSE 压缩帧。读取时,它解压后再次 XOR 前帧,得到原始 BGRA 帧。

为什么这样做:贴纸的相邻帧通常高度相似。直接缓存每帧会放大磁盘和内存压力。差分会产生大量零字节,通常能提升通用压缩效果。

关键代码摘录

LottieBufferCompressor.swift · L308–L350 等 2 处
if !self.data.hasAlreadyFrame(Int(current.1)) {
    current.0.withUnsafeBytes { pointer in
        let ui64Address = pointer.baseAddress!.assumingMemoryBound(to: UInt64.self)
        var length:Int = self.bufferSize
        if let previous = previous, enableDifference {
            let uint64Bs = self.bufferSize / 8
            previous.withUnsafeBytes { pointer in
                // ... `previous` 复制到 `dstDelta` 并重绑定为 `ui64Dst`。
                var i: Int = 0
                while i < uint64Bs {
                    ui64Dst[i] = ui64Dst[i] ^ ui64Address[i]
                    i &+= 1
                }
                length = compression_encode_buffer(dst, self.bufferSize, ui8, self.bufferSize, nil, COMPRESSION_LZFSE)
            }
        } else {
            length = compression_encode_buffer(dst, self.bufferSize, address, self.bufferSize, nil, COMPRESSION_LZFSE)
        }
        _ = self.data.writeFrame(frame: Int(current.1), data: Data(bytes: dst, count: length))
    }
}
// ... 解码后按 UInt64 再次 XOR `previousBytes[i] ^ address[i]`。

代价与风险:随机读取依赖前一帧。任一缓存帧损坏会影响后续还原。bufferSize / 8 也要求像素缓冲大小满足 8 字节对齐。

证据链 · #15-01已发布 · 仍可用
源码分析
已在 TelegramSwift 固定提交中逐行确认,共 2 处源码位置。
报告摘要
怎么做到:写入时 XOR 前帧,读取时再 XOR 还原。
REA 第 15 项
已发布 · 仍可用REA 二进制

13.0 二进制:导入 compression_encode_buffer、compression_decode_buffer

macOS 26.7:未执行

结论:已发布

2. 让相同内联贴纸共享一个解码和播放上下文

#15-02TelegramSwiftAppKit报告精选 15-2源码分析Core Animation性能与内存

做了什么:MultiTargetContextCache 用动画键和唯一值索引上下文。第二个同键图层只注册回调。最后一个订阅者移除时,它才销毁上下文。

为什么这样做:聊天可同时出现相同表情。每个图层独立解析 JSON 或 WebM 会重复解码。这个设计把一份帧输出扇出给多个 CALayer.contents。

关键代码摘录

InlineStickerItemLayer.swift · L208–L250
private static var cache: [Key : MultiTargetAnimationContext] = [:]

static func create(_ animation: LottieAnimation, key: Key,
                   displayFrame: @escaping(CGImage?)->Void,
                   release: @escaping()->Void,
                   updateState: @escaping(LottiePlayerState)->Void) -> Int {
    assertOnMainThread()
    let handlers = MultiTargetAnimationContext.Handlers(
        displayFrame: displayFrame, release: release, updateState: updateState)
    if let context = cache[key] {
        return context.add(handlers)
    }
    var token: Int = 0
    let context = MultiTargetAnimationContext(animation, handlers: handlers, token: &token)
    cache[key] = context
    return token
}

static func remove(_ token: Int, for key: Key) {
    if let context = self.cache[key], context.remove(token) {
        self.cache.removeValue(forKey: key)
    }
}

代价与风险:缓存是静态字典。调用方必须成对执行 remove。错误的 unique 会意外共享或失去共享。

证据链 · #15-02未单独核对
源码分析
已在 TelegramSwift 固定提交中逐行确认,共 1 处源码位置。
报告摘要
怎么做到:静态字典缓存,上下文内维护弱目标。
二进制验证
本条没有单独核对。REA 专题列出了已核对的 20 项。

3. 按运行循环把多个贴纸绘制合入一个 Metal 命令缓冲

#15-03TelegramSwiftAppKit报告精选 15-3源码分析Metal / GPU

做了什么:同一 LottieRunLoop 的待绘制项进入 list。定时器每帧只创建一个 MTLCommandBuffer。各项目编码后统一 commit 和 present。

为什么这样做:逐贴纸创建和提交命令缓冲会增加 CPU 提交和驱动开销。统一提交还能把多个 present 放在同一 GPU 调度批次中。

关键代码摘录

LottiePlayer.swift · L1548–L1584
private final class Loop {
    var list:[RenderLoopItem] = []
    init(_ runLoop: LottieRunLoop, commandQueue: MTLCommandQueue) {
        self.commandQueue = commandQueue
        self.timer = SwiftSignalKit.Timer(
            timeout: 1 / TimeInterval(runLoop.fps), repeat: true,
            completion: { [weak self] in self?.renderItems() }, queue: lottieStateQueue)
        self.timer?.start()
    }
    private func renderItems() {
        let commandBuffer = self.commandQueue.makeCommandBuffer()
        if let commandBuffer = commandBuffer {
            var drawables: [MTLDrawable] = []
            while !self.list.isEmpty {
                let item = self.list.removeLast()
                if let drawable = item.render(commandBuffer) { drawables.append(drawable) }
            }
            if drawables.isEmpty { return }
            commandBuffer.addScheduledHandler { _ in
                for drawable in drawables { drawable.present() }
            }
            commandBuffer.commit()
        }
    }
}

代价与风险:项目以 removeLast() 逆序处理。异常项目可能拖慢该运行循环。代码没有显式上限,峰值期间 list 可能增长。

证据链 · #15-03未单独核对
源码分析
已在 TelegramSwift 固定提交中逐行确认,共 1 处源码位置。
报告摘要
怎么做到:定时器按 fps 触发,收集 drawables,完成后统一 commit。
二进制验证
本条没有单独核对。REA 专题列出了已核对的 20 项。

4. GIF 用 AVSampleBufferDisplayLayer 自行供帧,而非逐帧 NSImage 动画

做了什么:它让 AVAssetReader 只在显示层可接收时取样并 enqueue。它关闭样本复制。它把显示层控制时钟绑定到主机时钟,并在读完后重建 reader。

为什么这样做:逐帧创建 NSImage 会增加解码和主线程提交成本。显示层能按 PTS 调度。背压循环避免缓存超过显示层需要的样本。

关键代码摘录

GIFPlayerView.swift · L245–L301 等 2 处
if let readerValue = reader.with({ $0 }), let outputValue = output.with({ $0 }) {
    let affineTransform = track.with { $0?.preferredTransform.inverted() }
    if let affineTransform = affineTransform {
        layer.setAffineTransform(affineTransform)
    }
    while layer.isReadyForMoreMediaData {
        if !stopRequesting.with({ $0 }) {
            if readerValue.status == .reading,
               let sampleBuffer = outputValue.copyNextSampleBuffer() {
                layer.enqueue(sampleBuffer)
                continue
            }
            _ = stopRequesting.modify { _ in _data.with { $0 } == nil }
            break
        }
    }
    if readerValue.status == .completed || readerValue.status == .cancelled {
        if reset.swap(false) {
            let result = restartReading(/* ... */ layer: layer, _timebase: timebase)
            if result { layer.flush() }
        }
    }
}
// ... `output.alwaysCopiesSampleData = false`。
// ... `CMTimebaseCreateWithSourceClock(... CMClockGetHostTimeClock(), ...)`。

代价与风险:读取器和时间基的重建路径复杂。flush 时可能出现短暂跳帧。它依赖 AVFoundation 的显示层行为。

证据链 · #15-04未单独核对
源码分析
已在 TelegramSwift 固定提交中逐行确认,共 2 处源码位置。
报告摘要
怎么做到:CMTimebaseCreateWithSourceClock + 背压循环。
二进制验证
本条没有单独核对。REA 专题列出了已核对的 20 项。

5. 通话帧池达到五个缓冲就丢帧,优先控制实时延迟

#15-05TelegramSwiftAppKit报告精选 15-5源码分析

做了什么:它按分辨率复用 CVPixelBufferPool。池占用超过五张时,它立即丢弃新通话帧。它随后把 I420 拷入 NV12 缓冲。

为什么这样做:实时通话中,排队旧帧比丢弃新帧更糟。硬阈值防止慢 GPU 或慢合成器使延迟无限累积。

关键代码摘录

MetalVideoRenderView.swift · L286–L334
let pool: PixelBufferPool?
if let current = pixelBufferPoolState.pool,
   current.width == width, current.height == height {
    pool = current
} else {
    pool = PixelBufferPool(width: width, height: height)
    pixelBufferPoolState.pool = pool
}
guard let pool else { return }

let auxAttributes: [String: Any] = [
    kCVPixelBufferPoolAllocationThresholdKey as String: 5 as NSNumber
]
var pixelBuffer: CVPixelBuffer?
let result = CVPixelBufferPoolCreatePixelBufferWithAuxAttributes(
    kCFAllocatorDefault, pool.pool, auxAttributes as CFDictionary, &pixelBuffer)
if result == kCVReturnWouldExceedAllocationThreshold {
    print("kCVReturnWouldExceedAllocationThreshold, dropping frame")
    return
}
guard let pixelBuffer else { return }
if !copyI420BufferToNV12Buffer(buffer: i420Buffer, pixelBuffer: pixelBuffer) { return }

代价与风险:高负载下会明显掉帧。阈值 5 是经验值。它可能不适合离线录制。

证据链 · #15-05未单独核对
源码分析
已在 TelegramSwift 固定提交中逐行确认,共 1 处源码位置。
报告摘要
怎么做到:CVPixelBufferPoolCreatePixelBufferWithAuxAttributes 返回阈值错误即丢弃。
二进制验证
本条没有单独核对。REA 专题列出了已核对的 20 项。

6. 通话背景先统一颜色格式,再降采样做两次计算模糊

#15-06TelegramSwiftAppKit报告精选 15-6源码分析

做了什么:它先把 BGRA、双平面 YUV 或三平面 YUV 统一转成 RGBA。仅当背景层可见时,它降采样并依次执行横向和纵向计算模糊。

为什么这样做:视频来源的颜色布局不同。统一格式简化后续渲染。先降采样和可分离模糊显著降低每帧纹理访问量。

关键代码摘录

PrivateCallVideoLayer.swift · L190–L257
switch videoTextures.textureLayout {
case let .bgra(texture):
    computeEncoder.setComputePipelineState(blurState.computePipelineStateBGRAToRGBA)
    computeEncoder.setTexture(texture.bgra, index: 0)
    computeEncoder.setTexture(rgbaTexture, index: 1)
case let .biPlanar(biPlanar):
    computeEncoder.setComputePipelineState(blurState.computePipelineStateYUVBiPlanarToRGBA)
    computeEncoder.setTexture(biPlanar.y, index: 0)
    computeEncoder.setTexture(biPlanar.uv, index: 1)
    computeEncoder.setTexture(rgbaTexture, index: 2)
case let .triPlanar(triPlanar):
    computeEncoder.setComputePipelineState(blurState.computePipelineStateYUVTriPlanarToRGBA)
    computeEncoder.setTexture(triPlanar.y, index: 0)
    computeEncoder.setTexture(triPlanar.u, index: 1)
    computeEncoder.setTexture(triPlanar.u, index: 2)
    computeEncoder.setTexture(rgbaTexture, index: 3)
}
blurState.downscaleKernel.encode(
    commandBuffer: commandBuffer, sourceTexture: rgbaTexture, destinationTexture: downscaledTexture)
computeEncoder.setComputePipelineState(blurState.computePipelineStateHorizontal)
computeEncoder.setTexture(downscaledTexture, index: 0)
computeEncoder.setTexture(blurredHorizontalTexture, index: 1)
computeEncoder.setComputePipelineState(blurState.computePipelineStateVertical)
computeEncoder.setTexture(blurredHorizontalTexture, index: 0)
computeEncoder.setTexture(blurredVerticalTexture, index: 1)

代价与风险:三平面分支把 triPlanar.u 同时绑定在索引 1 和 2。源码如此实现。其是否应为 v 平面未验证。

证据链 · #15-06未单独核对
源码分析
已在 TelegramSwift 固定提交中逐行确认,共 1 处源码位置。
报告摘要
怎么做到:选择不同 compute pipeline,再二次模糊。
二进制验证
本条没有单独核对。REA 专题列出了已核对的 20 项。

7. Thanos 效果把视图快照变成 GPU 像素粒子,并只在 M2 以上启用

#15-07TelegramSwiftAppKit源码分析Core AnimationMetal / GPU

做了什么:它截取目标视图的像素快照。它把快照和转换后的坐标交给覆盖整个窗口的 DustLayer。粒子结束后,该无交互覆盖视图自动移除。

为什么这样做:直接让原始视图逐块消失难以保持原始外观。快照把任意复杂 UI 变成统一纹理。M2 和 ARM64 门控避免不确定的硬件性能。

关键代码摘录

DustLayer.swift · L369–L418
public func ApplyDustAnimation(for currentView: NSView) {
    guard let window = currentView.window, isChipM2OrHigher() else {
        return
    }
    #if arch(arm64)
    window.contentView?.layer?.addSublayer(MetalEngine.shared.rootLayer)
    let metalLayer = DustLayer()
    metalLayer.isInHierarchy = true
    metalLayer.frame = window.contentView!.bounds
    let view = View(frame: metalLayer.frame.size.bounds)
    view.isEventLess = true
    view.layer?.addSublayer(metalLayer)
    window.contentView?.addSubview(view)
    metalLayer.becameEmpty = { [weak view] in
        view?.removeFromSuperview()
    }
    #endif
}

for subview in views {
    view.metalLayer.addItem(
        frame: CGRect(origin: subview.convert(.zero, to: view), size: subview.frame.size),
        image: subview.snapshot)
}

代价与风险:只有 Apple Silicon M2 及更高设备可见该效果。大视图快照和逐像素粒子会占用大量 GPU 资源。

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

8. Thanos 粒子只在有项目且可见时订阅全局显示链接

做了什么:层仅在有粒子且已进入层级时订阅共享显示链接。每个项目只初始化一次粒子状态。每帧计算着色器更新状态,再按 particleCount 实例化绘制。

为什么这样做:单独计时器会浪费唤醒次数。CPU 逐粒子更新和绘制会成为瓶颈。实例绘制让顶点着色器根据粒子缓冲取状态。

关键代码摘录

DustLayer.swift · L170–L229 等 2 处
private func updateNeedsAnimation() {
    if !self.items.isEmpty && self.isInHierarchy {
        if self.updateLink == nil {
            self.updateLink = SharedDisplayLinkDriver.shared.add(
                framesPerSecond: .max, { [weak self] deltaTime in
                    guard let self else { return }
                    self.updateItems(deltaTime: deltaTime)
                    self.setNeedsUpdate()
                })
            self.setNeedsUpdate()
        }
    } else if self.updateLink != nil {
        self.updateLink = nil
    }
}

if !item.particleBufferIsInitialized {
    item.particleBufferIsInitialized = true
    computeEncoder.setComputePipelineState(state.computePipelineStateInitializeParticle)
    computeEncoder.dispatchThreadgroups(threadgroupCount, threadsPerThreadgroup: threadgroupSize)
}
computeEncoder.setComputePipelineState(state.computePipelineStateUpdateParticle)
encoder.setVertexBuffer(particleBuffer.buffer, offset: 0, index: 3)
encoder.setFragmentTexture(item.texture, index: 0)
encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6, instanceCount: particleCount)

代价与风险:particleCount 是快照宽乘高。大尺寸快照会产生非常多实例。全局显示链接会以最高帧率驱动它。

证据链 · #15-08未单独核对
源码分析
已在 TelegramSwift 固定提交中逐行确认,共 2 处源码位置。
报告摘要
怎么做到:SharedDisplayLinkDriver 订阅回调更新状态与重绘。
二进制验证
本条没有单独核对。REA 专题列出了已核对的 20 项。

9. CPU 模糊把三个颜色通道塞入 64 位累积值,并用滑动二阶差分

#15-09TelegramSwiftAppKit源码分析性能与内存

做了什么:实现用固定半径 7 做横向和纵向两遍模糊。它把 RGB 通道打包在 uint64_t。它通过二阶差分递推滑动窗口,并以首尾像素延拓边缘。

为什么这样做:朴素盒模糊每个输出像素需遍历半径。递推使每步只做常数次读取和算术。打包通道也减少了循环和索引操作。

关键代码摘录

FastBlur.m · L120–L215
const int radius = 7;
const int r1 = radius + 1;
const int div = radius * 2 + 1;
if (radius > 15 || div >= w || div >= h) { return; }

uint64_t *rgb = malloc(imageStride * imageHeight * sizeof(uint64_t));
uint64_t cur = get_colors(&pix[yw]);
uint64_t rgballsum = -radius * cur;
uint64_t rgbsum = cur * ((r1 * (r1 + 1)) >> 1);

#define update(start, middle, end)                         \
rgb[y * w + x] = (rgbsum >> 6) & 0x00FF00FF00FF00FF;      \
rgballsum += get_colors(&pix[yw + (start) * 4]) -          \
    2 * get_colors(&pix[yw + (middle) * 4]) +              \
    get_colors(&pix[yw + (end) * 4]);                       \
rgbsum += rgballsum; x++;

while (x < r1) { update(0, x, x + r1); }
while (x < we) { update(x - r1, x, x + r1); }
while (x < w) { update(x - r1, x, w - 1); }

代价与风险:半径固定。大半径模糊需多次调用或其他算法。临时缓冲按 imageStride * imageHeight * sizeof(uint64_t) 分配,内存占用较高。

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

10. 软件渐变先渲染很小的位图,再动画 contents

#15-10TelegramSwiftAppKit源码分析Core Animation性能与内存

做了什么:代码按 16×16 量级的适配尺寸生成软件渐变帧。它在 CPU 插值颜色锚点。它把预制 CGImage 列表交给 Core Animation 的 contents 关键帧动画。

为什么这样做:每次屏幕刷新计算全尺寸渐变很昂贵。渐变可被双线性放大。Core Animation 能独立提交预计算帧,不必每帧执行 Swift 绘制代码。

关键代码摘录

SoftwareGradientBackground.swift · L287–L372
let imageSize = size.fitted(CGSize(width: 16, height: 16)).integralFloor
let positions = gatherPositions(
    shiftArray(array: AnimatedGradientBackgroundView.basePositions, offset: self.phase % 8))

let fps: Double = extendAnimation ? 60 : 30
let maxFrame = Int(duration * fps)
for frameIndex in 0 ..< maxFrame {
    let t = curve.solve(at: CGFloat(frameIndex) / CGFloat(maxFrame - 1))
    let globalStep = Int(t * CGFloat(maxFrame))
    let stepIndex = min(stepCount - 1, globalStep / framesPerAnyStep)
    let stepT = CGFloat(stepFrameIndex) / CGFloat(stepFrames - 1)
    var morphedPositions: [CGPoint] = []
    for i in 0 ..< steps[0].count {
        morphedPositions.append(interpolatePoints(
            steps[stepIndex][i], steps[stepIndex + 1][i], at: stepT))
    }
    images.append(generateGradient(size: imageSize, colors: self.colors, positions: morphedPositions))
}
self.contentView.image = images.last
let animation = CAKeyframeAnimation(keyPath: "contents")
animation.values = images.map { $0 }
animation.duration = duration
self.contentView.layer!.add(animation, forKey: "contents")

代价与风险:一次动画会保留全部帧图像。长时长或 60 fps 会增加瞬时内存。低分辨率会牺牲细节。

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

11. Shimmer 将压缩路径缩略图解码为异步位图掩码

做了什么:它把自定义紧凑字节编码还原成路径字符串。它在并发队列将路径栅格化为 CGImage。主线程只把结果设给 mask 或前景层。

为什么这样做:占位 shimmer 若使用矩形会暴露布局。直接在主线程解析和绘制复杂贴纸轮廓会卡顿。位图掩码可准确贴合轮廓。

关键代码摘录

ShimmerLayer.swift · L158–L175 等 2 处
private func decodeStickerThumbnailData(_ data: Data) -> String {
    var string = "M"
    data.forEach { byte in
        if byte >= 128 + 64 {
            string.append(decodingMap[Int(byte) - 128 - 64])
        } else {
            // ... 写入逗号或负号。
            string.append("\(byte & 63)")
        }
    }
    string.append("z")
    return string
}
let signal: Signal<CGImage?, NoError> = Signal { subscriber in
    let image = generateImage(size, rotatedContext: { size, context in
        var path = decodeStickerThumbnailData(data)
        if !path.hasPrefix("z") { path = "\(path)z" }
        let segments = PathDataReader(input: path).read()
        let scale = max(size.width, size.height) / max(imageSize.width, imageSize.height)
        context.scaleBy(x: scale, y: scale)
        renderPath(segments, context: context)
    })
    subscriber.putNext(image)
    subscriber.putCompletion()
    // ... `return EmptyDisposable` 后转入并发队列。

代价与风险:路径协议是私有数据格式,但不是 Apple 私有 API。hasPrefix("z") 的判断看起来反直觉。这里保持源码行为,原因未验证。

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

12. 声音波纹把原始音量做指数平滑,再生成闭合贝塞尔曲线

做了什么:视图将输入音量归一化,并给三层 Blob 同时调速。显示链接用 0.9/0.1 的指数低通生成展示音量。路径以相邻点导出的控制点闭合为贝塞尔曲线。

为什么这样做:麦克风幅度抖动很大。直接映射会产生尖刺和闪烁。该滤波与连续曲线让三层随机形状仍保持稳定柔和。

关键代码摘录

VoiceBlobView.swift · L78–L112 等 2 处
displayLinkAnimator = ConstantDisplayLinkAnimator(update: { [weak self] in
    guard let strongSelf = self, let window = self?.window, window.isVisible else { return }
    strongSelf.presentationAudioLevel =
        strongSelf.presentationAudioLevel * 0.9 + strongSelf.audioLevel * 0.1
    strongSelf.smallBlob.level = strongSelf.presentationAudioLevel
    strongSelf.mediumBlob.level = strongSelf.presentationAudioLevel
    strongSelf.bigBlob.level = strongSelf.presentationAudioLevel
})

public func updateLevel(_ level: CGFloat) {
    let normalizedLevel = min(1, max(level / maxLevel, 0))
    smallBlob.updateSpeedLevel(to: normalizedLevel)
    mediumBlob.updateSpeedLevel(to: normalizedLevel)
    bigBlob.updateSpeedLevel(to: normalizedLevel)
    audioLevel = normalizedLevel
}

let currSmoothOut = curr.smoothOut()
let nextSmoothIn = next.smoothIn()
resultPath.addCurve(to: next.point, control1: currSmoothOut, control2: nextSmoothIn)
resultPath.closeSubpath()

代价与风险:滤波会引入视觉滞后。显示链接只判断窗口可见,不会自动停止 Blob 自身的其他动画。

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

13. 聊天贴纸按尺寸降低帧率,并用缩略图无闪接力

#15-13TelegramSwiftAppKit源码分析Core Animation性能与内存

做了什么:它用映射读取载入完整动画数据。小于 200 pt 的非表情贴纸限定 30 fps。缩略图一直显示到播放器真正进入 .playing,再切换图层可见性。

为什么这样做:聊天列表的小贴纸无需 60 fps。内存映射避免整份文件复制。状态驱动的双层切换避免网络和解码期间出现空白框。

关键代码摘录

ChatMediaAnimatedStickerView.swift · L188–L246
if resourceData.complete,
   let data = try? Data(contentsOf: URL(fileURLWithPath: resourceData.path),
                        options: [.mappedIfSafe]) {
    let playPolicy: LottiePlayPolicy = file.isEmojiAnimatedSticker ? .once : .loop
    let maximumFps: Int = size.width < 200 && !file.isEmojiAnimatedSticker ? 30 : 60
    let fitzModifier = file.animatedEmojiFitzModifier
    self?.sticker = LottieAnimation(
        compressed: data,
        key: LottieAnimationEntryKey(key: .media(file.id), size: size, fitzModifier: fitzModifier),
        cachePurpose: size.width < 200 ? .temporaryLZ4(.thumb) :
            self?.parent != nil ? .temporaryLZ4(.chat) : .none,
        playPolicy: playPolicy, maximumFps: maximumFps)
}

self.thumbView.setSignal(signal: cachedMedia(media: file, arguments: arguments,
                                             scale: backingScaleFactor), clearInstantly: updated)
if !self.thumbView.isFullyLoaded { /* 继续请求贴纸缩略图 */ }
stateDisposable.set((self.playerView.state |> deliverOnMainQueue).start(next: { state in
    self.playerView.isHidden = state != .playing
    self.thumbView.isHidden = state == .playing
}))

代价与风险:size.width 是唯一的分级维度。窄而高的贴纸会按小图处理。缩略图和播放器会在短时间内同时驻留。

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

14. 内联贴纸延迟冷启动,却让缓存命中立即开始

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

做了什么:新上下文冷启动时等待 100 ms。命中共享缓存、强制播放或指定无延迟时立即启动。回调发现图层离开窗口后会异步撤销自身。

为什么这样做:滚动时大量内联表情可能一闪而过。延迟可过滤这类短生命周期工作。已存在的共享上下文不应再引入启动等待。

关键代码摘录

InlineStickerItemLayer.swift · L576–L627
private var contextToken: (Int, MultiTargetContextCache.Key)?
private func set(_ animation: LottieAnimation?, force: Bool = false) {
    self.animation = animation
    if let animation = animation, let isPlayable = self.isPlayable, isPlayable, !stopped {
        weak var layer: InlineStickerItemLayer? = self
        let key = MultiTargetContextCache.Key(key: animation.key, unique: unique)
        delayDisposable.set(delaySignal(
            MultiTargetContextCache.exists(key) || force || noDelayBeforeplay ? 0 : 0.1
        ).start(completed: { [weak self] in
            self?.contextToken = (MultiTargetContextCache.create(animation, key: key,
                displayFrame: { image in
                    layer?.contents = image
                    if layer?.isPreviousPreview == true {
                        layer?.animateContents()
                        layer?.isPreviousPreview = false
                    }
                    if self?.superview?.window == nil {
                        DispatchQueue.main.async { self?.set(nil, force: true) }
                    }
                }, release: {}, updateState: { [weak self] state in
                    self?.updateState(state)
                }), key)
        }))
    }
}

代价与风险:首次出现的表情可能晚 100 ms 开始。异步撤销依赖生命周期竞态处理。weak var layer 避免循环引用,但也可能丢首帧。

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

15. 内联贴纸仅在窗口活跃且滚动区域可见时播放

#15-15TelegramSwiftAppKit源码分析手势与滚动

做了什么:播放条件同时要求可播放、窗口活跃和 visibleRect 非零。它监听窗口焦点、滚动裁剪区边界和文档视图 frame 变化,立即重新计算条件。

为什么这样做:仅监听 viewDidMoveToWindow 无法识别滚出屏幕的文本附件。仅暂停非活跃窗口也不足以节省聊天长列表中的离屏动画工作。

关键代码摘录

InlineStickerItemLayer.swift · L280–L322
@objc func updateAnimatableContent() -> Void {
    if controlContent {
        var isKeyWindow = false
        if let window = window {
            if !window.canBecomeKey {
                isKeyWindow = true
            } else {
                isKeyWindow = window.isKeyWindow
            }
        }
        animateLayer.isPlayable = isKeyWindow && isPlayable && self.visibleRect != .zero
    }
}
center.addObserver(self, selector: #selector(updateAnimatableContent),
                   name: NSWindow.didBecomeKeyNotification, object: window)
center.addObserver(self, selector: #selector(updateAnimatableContent),
                   name: NSView.boundsDidChangeNotification,
                   object: self.enclosingScrollView?.contentView)
center.addObserver(self, selector: #selector(updateAnimatableContent),
                   name: NSView.frameDidChangeNotification,
                   object: self.enclosingScrollView?.documentView)

代价与风险:通知注册较多。window.canBecomeKey == false 被特殊视为活跃。这是对非激活面板的兼容,具体动机未验证。

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

16. PiP 不依赖系统 PiP 控制器,而用跨 Space 的非激活面板模拟

做了什么:它用 NSPanel 自建浮窗。面板不抢键盘焦点,并加入所有 Space 和全屏辅助窗口集合。它保留一个移到 (3000, 3000) 的 Window,以让 PictureInpictureView.window 返回该窗口。

为什么这样做:系统 AVPictureInPictureController 只适合特定播放路径。自定义面板可承载任意 PictureInPictureControl.view,并复用 Telegram 自己的手势和图库跳转。

关键代码摘录

PIPVideoWindow.swift · L69–L150 等 2 处
fileprivate class ModernPictureInPictureVideoWindow: NSPanel {
    init(_ control: PictureInPictureControl, item: MGalleryItem,
         viewer: GalleryViewer, origin: NSPoint, type: GalleryAppearType) {
        let minSize = control.view.frame.size.aspectFilled(NSMakeSize(120, 120))
        let size = item.notFittedSize.aspectFilled(NSMakeSize(250, 250)).aspectFilled(minSize)
        let newRect = NSMakeRect(origin.x, origin.y, size.width, size.height)
        __window = Window(contentRect: newRect, styleMask: [.resizable],
                          backing: .buffered, defer: true)
        __window.setFrame(NSMakeRect(3000, 3000, saver.rect.width, saver.rect.height), display: true)
        super.init(contentRect: newRect, styleMask: [.resizable, .nonactivatingPanel],
                   backing: .buffered, defer: true)
        self.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
        self.contentView = PictureInpictureView(frame: bounds, window: __window)
        view.forceMouseDownCanMoveWindow = true
        self.contentView?.layer?.cornerRadius = 4
        control.view.frame = NSMakeRect(0, 0, newRect.width, newRect.height)
        contentView?.addSubview(control.view)
    }
}
// ... 将越界窗口框吸附回可见区域。

代价与风险:这不是系统级 PiP。它需要自行处理位置、尺寸、焦点、暂停和多显示器边界。未发现私有 API 调用。

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

17. GIF 转码器修正极短延时,并直接绘入 CVPixelBuffer

做了什么:它优先使用 GIF 的未钳制延时。低于 11 ms 的延时统一修正为 100 ms。它从像素池取 ARGB 缓冲,并让 Core Graphics 直接写入该缓冲。

为什么这样做:部分 GIF 报告不现实的极短延时。若照播会导致高 CPU 或错误帧率。直接绘入编码器可使用的 CVPixelBuffer 避免额外图像中转。

关键代码摘录

TGGifConverter.m · L185–L255
float frameDuration = 0.1f;
NSDictionary *gifProperties = frameProperties[(NSString*)kCGImagePropertyGIFDictionary];
NSNumber *delayTimeUnclampedProp = gifProperties[(NSString*)kCGImagePropertyGIFUnclampedDelayTime];
if (delayTimeUnclampedProp) {
    frameDuration = [delayTimeUnclampedProp floatValue];
} else {
    NSNumber *delayTimeProp = gifProperties[(NSString*)kCGImagePropertyGIFDelayTime];
    if (delayTimeProp) { frameDuration = [delayTimeProp floatValue]; }
}
if (frameDuration < 0.011f) frameDuration = 0.100f;

if (pixelBufferPool)
    status = CVPixelBufferPoolCreatePixelBuffer(kCFAllocatorDefault, pixelBufferPool, &pxBuffer);
else
    status = CVPixelBufferCreate(kCFAllocatorDefault, width, height,
        kCVPixelFormatType_32ARGB, (__bridge CFDictionaryRef)attributes, &pxBuffer);
CVPixelBufferLockBaseAddress(pxBuffer, 0);
void *pxData = CVPixelBufferGetBaseAddress(pxBuffer);
CGContextRef context = CGBitmapContextCreate(pxData, width, height, bpc,
    bytesPerRow, colorSpace, kCGImageAlphaNoneSkipFirst);
CGContextDrawImage(context, CGRectMake(0, 0, width, height), frame);
CVPixelBufferUnlockBaseAddress(pxBuffer, 0);

代价与风险:11 ms 以下的合法高速 GIF 会被放慢。该策略优先兼容性,不保证原始时序。

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

18. 软件视频预览锁定 CVPixelBuffer 后用 vImage 一次缩放

#15-18TelegramSwiftAppKit源码分析REA 二进制性能与内存

做了什么:预览路径直接锁定解码帧的像素内存。它用 vImageScale_ARGB8888 缩放到目标 backing scale。最后才包装为 CGImage。

为什么这样做:该路径避免创建中间 NSImage 和多层绘制上下文。vImage 能正确处理源和目标 stride。它适合生成单帧静态预览。

关键代码摘录

SoftwareVideoSource.swift · L26–L50
let s = (w: Int(size.width) * backingScale, h: Int(size.height) * backingScale)
let destBytesPerRow = DeviceGraphicsContextSettings.shared.bytesPerRow(forWidth: s.w)
let bufferSize = s.h * DeviceGraphicsContextSettings.shared.bytesPerRow(forWidth: s.w)
let memoryData = malloc(bufferSize)!
let bytes = memoryData.assumingMemoryBound(to: UInt8.self)

let imageBuffer = CMSampleBufferGetImageBuffer(frame.sampleBuffer)
CVPixelBufferLockBaseAddress(imageBuffer!, CVPixelBufferLockFlags(rawValue: 0))
let bytesPerRow = CVPixelBufferGetBytesPerRow(imageBuffer!)
let width = CVPixelBufferGetWidth(imageBuffer!)
let height = CVPixelBufferGetHeight(imageBuffer!)
let srcData = CVPixelBufferGetBaseAddress(imageBuffer!)
var sourceBuffer = vImage_Buffer(data: srcData, height: vImagePixelCount(height),
    width: vImagePixelCount(width), rowBytes: bytesPerRow)
var destBuffer = vImage_Buffer(data: bytes, height: vImagePixelCount(s.h),
    width: vImagePixelCount(s.w), rowBytes: destBytesPerRow)
_ = vImageScale_ARGB8888(&sourceBuffer, &destBuffer, nil, vImage_Flags(kvImageDoNotTile))
CVPixelBufferUnlockBaseAddress(imageBuffer!, CVPixelBufferLockFlags(rawValue: 0))
return generateImagePixel(size, scale: CGFloat(backingScale), pixelGenerator: {
    _, pixelData, _ in memcpy(pixelData, bytes, bufferSize)
})

代价与风险:代码使用了 malloc,但该片段没有显式 free(memoryData)。generateImagePixel 是否在后续复制后释放该块未验证。若未释放,预览高频调用会泄漏内存。

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

13.0 二进制:导入 20 个 vImage* 函数

macOS 26.7:未执行

结论:已发布

附注

私有 API 与混淆核对

本次范围内未发现通过字符串拼接、Base64、哈希或运行时 selector 调用 Apple 私有 API 的证据。PIPVideoWindow.swift 导入 AVKit,但实际 PiP 使用公开的 NSPanel、NSWindow 和 NSView API。此结论只覆盖本笔记读取的文件和 rg 扫描命中,不代表整个仓库不存在私有 API。

本页目录