Telegram 技巧档案UIKIT · APPKIT
iOS · UIKit

视觉特效与玻璃

私有 Liquid Lens、CABackdropLayer 与可变模糊。

范围:Telegram-iOS 的私有 Core Animation 视觉特效、Liquid Glass、壁纸透出气泡、粒子遮罩、渐变壁纸与边缘模糊。

结论:该版本把私有 CA 管线封装成可降级能力。 它优先使用 iOS 26 的私有 Liquid Lens。 它在旧系统自绘玻璃并使用私有 backdrop。 它还用 _UIPortalView 让聊天气泡透出同一壁纸。 以下按意外程度和工程价值排序。

1. 直接驱动 iOS 26 的私有 Liquid Lens

#07-01Telegram-iOSUIKit报告精选 07-1源码分析私有 API运行时 hack

做了什么:代码按运行时类名创建 _UILiquidLensView。 它把抬起内容和挖空内容交给私有 Lens。 它把 Lens 放在背景和交互内容之间。

为什么这样做:普通 UIVisualEffectView 没有系统 Lens 的抬起形变。 私有 Lens 能同时处理背景折射和内容挖空。

关键代码摘录

LiquidLensView.swift · L198–L257
if #available(iOS 26.0, *) {
    if let viewClass = NSClassFromString("_UILiquidLensView") as AnyObject as? NSObjectProtocol {
        let allocSelector = NSSelectorFromString("alloc")
        let initSelector = NSSelectorFromString("initWithRestingBackground:")
        let objcAlloc = viewClass.perform(allocSelector).takeUnretainedValue()
        let instance = objcAlloc.perform(initSelector, with: UIView()).takeUnretainedValue()
        self.lensView = instance as? UIView
    }
}

if let lensView = self.lensView {
    lensView.layer.zPosition = 10.0
    self.liftedContainerView.addSubview(self.restingBackgroundView)
    self.containerView.addSubview(self.liftedContainerView)
    self.containerView.addSubview(lensView)
    self.containerView.addSubview(self.contentView)
    lensView.perform(NSSelectorFromString("setLiftedContentView:"), with: self.liftedContainerView)
    lensView.perform(NSSelectorFromString("setOverridePunchoutView:"), with: self.contentView)
}

代价与风险:使用私有类和私有 selector。 审核风险很高。 未来系统可能移除或改变 ABI。

证据链 · #07-01iOS 未做二进制验证
源码分析
已在 Telegram-iOS 固定提交中逐行确认,共 1 处源码位置。
报告摘要
怎么做到:perform 调私有 initWithRestingBackground: 与 setter。
二进制验证
未做。App Store 版 IPA 受 FairPlay 加密,无法合法取得解密的可执行文件。

2. 为私有 Lens 补齐弹跳刷新和位置动画

做了什么:代码先发起 bounds 动画。 它随后清空私有 Lens 的残留动画。 它再写入最终 bounds。 它还补一个随宽度变化的加法 position 动画。

为什么这样做:Lens 的内部动画会覆盖外部状态。 代码注释明确表示需要此位置补偿。 该补偿的根因在源码中标为“未知”。

关键代码摘录

LiquidLensView.swift · L375–L415 等 2 处
let liftedInset: CGFloat = params.isLifted ? params.liftedInset : (-params.inset)
let lensBounds = CGRect(origin: CGPoint(), size: CGSize(
    width: params.baseFrame.width + liftedInset * 2.0,
    height: params.baseFrame.height + liftedInset * 2.0
))
let lensCenter = CGPoint(x: params.baseFrame.midX, y: params.baseFrame.midY)
let previousBounds: CGRect = lensView.bounds
transition.animateView {
    lensView.bounds = lensBounds
}
if let info = transition.userData(TransitionInfo.self), info.disableAnimationWorkarounds {
} else {
    lensView.layer.removeAllAnimations()
    lensView.bounds = lensBounds
}
transition.setPosition(view: lensView, position: lensCenter, completion: { [weak self] flag in
    guard let self, flag else { return }
    if !self.isApplyingLensParams { self.isAnimating = false }
})
transition.animatePosition(
    layer: lensView.layer,
    from: CGPoint(x: (lensBounds.width - previousBounds.width) * 0.5, y: 0.0),
    to: CGPoint(), additive: true
)

代价与风险:这依赖未文档化的动画时序。 removeAllAnimations() 可能中断系统新增动画。 补偿公式需要持续实机回归。

证据链 · #07-02iOS 未做二进制验证
源码分析
已在 Telegram-iOS 固定提交中逐行确认,共 2 处源码位置。
报告摘要
怎么做到:记录前后尺寸,animatePosition 从“宽差/2”回到 0。
二进制验证
未做。App Store 版 IPA 受 FairPlay 加密,无法合法取得解密的可执行文件。

3. 私有 Lens 动画期间强制 120 Hz

#07-03Telegram-iOSUIKit报告精选 07-3源码分析私有 API

做了什么:代码为每个 morph 申请共享显示链路。 它把支持 ProMotion 的屏幕锁定为 120 Hz。 它使用引用计数合并并发 morph。 它还用一秒超时释放未回调的申请。

为什么这样做:源码注释记录了系统 morph 会被降到约 80 Hz。 空 tick 让显示链路持续提出固定帧率请求。 普通动画只设范围下限会允许系统节流。

关键代码摘录

LiquidMorphTransition.swift · L75–L145
private static let maximumDuration: Double = 1.0

static let shared = LiquidMorphFrameRateBoost()
private var displayLink: CADisplayLink?
private var count = 0

func claim(screen: UIScreen?) -> Claim {
    assert(Thread.isMainThread)
    self.count += 1
    if self.displayLink == nil, let screen, screen.maximumFramesPerSecond >= 120 {
        let displayLink = CADisplayLink(target: Target(), selector: #selector(Target.tick))
        displayLink.preferredFrameRateRange = CAFrameRateRange(
            minimum: 120.0, maximum: 120.0, preferred: 120.0
        )
        displayLink.add(to: .main, forMode: .common)
        self.displayLink = displayLink
    }
    let claim = Claim()
    DispatchQueue.main.asyncAfter(deadline: .now() + Self.maximumDuration) {
        claim.end()
    }
    return claim
}

代价与风险:固定 120 Hz 会增加功耗。 超时值来自观测,可能不适配未来系统。 这只在 iOS 26 及高刷屏生效。

证据链 · #07-03iOS 未做二进制验证
源码分析
已在 Telegram-iOS 固定提交中逐行确认,共 1 处源码位置。
报告摘要
怎么做到:.common 模式添加到 RunLoop,并 1 秒超时兜底。
二进制验证
未做。App Store 版 IPA 受 FairPlay 加密,无法合法取得解密的可执行文件。

4. 没有私有 Lens 时,用亮度转 Alpha 模拟挖空玻璃

做了什么:代码把白色容器作为内容 mask。 它给 mask 加私有 luminanceToAlpha 滤镜。 它以可拉伸圆角胶囊图控制挖空区域。

为什么这样做:旧系统没有 _UILiquidLensView。 普通矩形裁剪无法制造透视孔洞。 亮度转 Alpha 可让黑色 blob 直接消去内容。

关键代码摘录

LiquidLensView.swift · L267–L296 等 2 处
let legacySelectionView = GlassBackgroundView.ContentImageView()
self.legacySelectionView = legacySelectionView
if let backgroundView = self.backgroundView {
    backgroundView.contentView.insertSubview(legacySelectionView, at: 0)
} else {
    self.containerView.insertSubview(legacySelectionView, at: 0)
}

let legacyContentMaskView = UIView()
legacyContentMaskView.backgroundColor = .white
self.legacyContentMaskView = legacyContentMaskView
self.contentView.mask = legacyContentMaskView
if let filter = CALayer.luminanceToAlpha() {
    legacyContentMaskView.layer.filters = [filter]
}

let legacyContentMaskBlobView = UIImageView()
self.legacyContentMaskBlobView = legacyContentMaskBlobView
legacyContentMaskView.addSubview(legacyContentMaskBlobView)

代价与风险:CAFilter 是私有 API。 回退效果不具备真实折射。 图层 mask 可能触发离屏渲染。

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

5. 用 _UIPortalView 复制远端视图,而非截图

#07-05Telegram-iOSUIKit源码分析私有 API运行时 hack文本与输入

做了什么:代码运行时解析并实例化 _UIPortalView。 它同步源视图的位置和变换。 它禁用命中测试转发和 portal 命中测试。

为什么这样做:截图或 contentsRect 不会随源壁纸同步更新。 Portal 让同一渲染源在另一个层级显示。 因此不需要每帧手动反向平移内容。

关键代码摘录

UIKitUtils.m · L211–L235
static Class portalViewClass = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    portalViewClass = NSClassFromString([@[@"_", @"UI", @"Portal", @"View"] componentsJoinedByString:@""]);
});
if (!portalViewClass) {
    return nil;
}
UIView<UIKitPortalViewProtocol> *view = [[portalViewClass alloc] init];
if (!view) {
    return nil;
}
if (@available(iOS 14.0, *)) {
    view.forwardsClientHitTestingToSourceView = false;
}
view.matchesPosition = matchPosition;
view.matchesTransform = matchPosition;
view.matchesAlpha = false;
if (@available(iOS 14.0, *)) {
    view.allowsHitTesting = false;
}

代价与风险:_UIPortalView 是私有 UIKit 类。 类名通过数组拼接隐藏。 解混淆结果是 _UIPortalView。 拼接文本可直接确认该名字。 审核风险很高。

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

6. Portal 重绑后原位置重插,迫使渲染刷新

#07-06Telegram-iOSUIKit源码分析私有 APICore Animation

做了什么:代码先重新设置 sourceView。 它再把 portal 重插回原来的下标。 它在 UIView 和纯 CALayer 两种宿主中都处理。

为什么这样做:仅改私有 sourceView 可能不触发 portal 重建。 原位重插不改变 z-order。 它却能迫使 UIKit 或 CA 重新连接源。

关键代码摘录

PortalView.swift · L15–L28
func reloadPortal(sourceView: PortalSourceView) {
    self.view.sourceView = sourceView
    self.sourceView = sourceView

    if let portalSuperview = self.view.superview,
       let index = portalSuperview.subviews.firstIndex(of: self.view) {
        portalSuperview.insertSubview(self.view, at: index)
    } else if let portalSuperlayer = self.view.layer.superlayer,
              let index = portalSuperlayer.sublayers?.firstIndex(of: self.view.layer) {
        portalSuperlayer.insertSublayer(self.view.layer, at: UInt32(index))
    }
}

func disablePortal() {
    self.view.sourceView = nil
    self.sourceView = nil
}

代价与风险:依赖私有 portal 的失效语义。 重插可能与宿主的层级动画冲突。

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

7. 聊天气泡真正“透出”同一张动态壁纸

做了什么:每种气泡类型都有各自的壁纸 portal 源。 气泡节点只显示相同源的 PortalView。 它不在每个气泡内重新绘制壁纸。

为什么这样做:旧 contentsRect 路径需管理滚动反向位移。 源码注释说明该路径会把整张壁纸压入气泡。 Portal 让滚动、动画和壁纸坐标天然一致。

关键代码摘录

WallpaperBackgroundNode.swift · L1988–L2023
let sourceView: PortalSourceView?
switch type {
case .free:
    sourceView = self.freeBackgroundPortalSourceView
case .incoming:
    sourceView = self.incomingBackgroundPortalSourceView
case .outgoing:
    sourceView = self.outgoingBackgroundPortalSourceView
}

guard let sourceView, let portalView = PortalView(matchPosition: true) else {
    return nil
}
sourceView.addPortal(view: portalView)
return WallpaperBackgroundNodeImpl.BubbleBackgroundPortalNodeImpl(
    portalView: portalView
)

代价与风险:这依赖 _UIPortalView。 私有类不存在时,代码退化为平面主题气泡。

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

8. 拆解 UIVisualEffectView 内部滤镜,获得可控导航模糊

#07-08Telegram-iOSUIKit源码分析私有 APICore Animation

做了什么:代码从公开 UIVisualEffectView 取得系统内部层。 它隐藏内部视觉子视图。 它只保留 gaussianBlur 和可选 colorSaturate。

为什么这样做:标准效果包含系统定义的叠色和材质层。 导航栏只需要背景采样、模糊和饱和度。 这个做法还能保持无障碍“降低透明度”降级。

关键代码摘录

NavigationBackgroundView.swift · L62–L108
if self.enableBlur && !sharedIsReduceTransparencyEnabled
    && ((self._color.alpha > .ulpOfOne && self._color.alpha < 0.95) || forceKeepBlur) {
    if self.effectView == nil {
        let effectView = UIVisualEffectView(effect: UIBlurEffect(style: .light))
        for subview in effectView.subviews {
            if subview.description.contains("VisualEffectSubview") {
                subview.isHidden = true
            }
        }
        if let sublayer = effectView.layer.sublayers?[0], let filters = sublayer.filters {
            sublayer.backgroundColor = nil
            sublayer.isOpaque = false
            var allowedKeys: [String] = ["gaussianBlur"]
            if self.enableSaturation { allowedKeys.append("colorSaturate") }
            sublayer.filters = filters.filter { filter in
                let filterName = String(describing: filter)
                return allowedKeys.contains(filterName)
            }
        }
    }
}

代价与风险:依赖内部子视图描述和 layer 顺序。 filters 是私有 CA 属性。 未来系统修改效果结构会破坏该实现。

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

9. 用 CABackdropLayer 和颜色矩阵手搓旧版玻璃

做了什么:代码创建私有 CABackdropLayer。 它先应用 backdrop-aware 颜色矩阵。 它再应用小半径高斯模糊。

为什么这样做:CABackdropLayer 直接采样其后的已合成画面。 颜色矩阵会产生比公开 blur 更强的玻璃染色。 它是原生 Liquid Glass 不可用时的传统回退。

关键代码摘录

LegacyGlassView.swift · L7–L13 等 2 处
private let backdropLayerClass: NSObject? = {
    let name = ("CA" as NSString).appendingFormat("BackdropLayer")
    if let cls = NSClassFromString(name as String) as AnyObject as? NSObject {
        return cls
    }
    return nil
}()

if let blurFilter = CALayer.blur(), let colorMatrixFilter = CALayer.colorMatrix() {
    blurFilter.setValue(2.0 as NSNumber, forKey: "inputRadius")
    var matrix: [Float32] = [
        2.6705, -1.1087999, -0.1117, 0.0, 0.049999997,
        -0.3295, 1.8914, -0.111899994, 0.0, 0.049999997,
        -0.3297, -1.1084, 2.8881, 0.0, 0.049999997,
        0.0, 0.0, 0.0, 1.0, 0.0
    ]
    colorMatrixFilter.setValue(NSValue(bytes: &matrix,
        objCType: "{CAColorMatrix=ffffffffffffffffffff}"), forKey: "inputColorMatrix")
    colorMatrixFilter.setValue(true as NSNumber, forKey: "inputBackdropAware")
    backdropLayer.filters = [colorMatrixFilter, blurFilter]
}

代价与风险:CABackdropLayer、CAFilter 和 KVC 均为私有用法。 类名被字符串拼接隐藏。 解混淆结果是 CABackdropLayer。 "CA" + "BackdropLayer" 可直接确认该名字。 审核风险很高。

证据链 · #07-09iOS 未做二进制验证
源码分析
已在 Telegram-iOS 固定提交中逐行确认,共 2 处源码位置。
报告摘要
怎么做到:NSClassFromString("CABackdropLayer") 与 CAFilter 链。
二进制验证
未做。App Store 版 IPA 受 FairPlay 加密,无法合法取得解密的可执行文件。

10. 私有 CAMeshTransform 让 backdrop 边缘像玻璃一样折射

做了什么:代码为圆角矩形生成位移网格。 它把网格写到 backdrop 的私有 meshTransform 键。 尺寸变化时,它显式插值旧网格和新网格。

为什么这样做:普通圆角裁剪不会移动背景采样点。 网格能在边缘偏移 backdrop,模拟折射。 显式网格动画避免形状突变。

关键代码摘录

LegacyGlassView.swift · L198–L230 等 2 处
let meshTransform = MeshTransform.roundedRect(
    width: size.width,
    height: size.height,
    cornerRadius: cornerRadius,
    displacementMagnitudeU: displacementMagnitudeU,
    displacementMagnitudeV: displacementMagnitudeV,
    outerEdgeDistance: outerEdgeDistance
).mesh.makeValue()

if let meshTransform {
    if !transition.animation.isImmediate,
       let previousTransform = backdropLayer.value(forKey: "meshTransform") as? NSObject {
        backdropLayer.removeAnimation(forKey: "meshTransform")
        backdropLayer.setValue(meshTransform, forKey: "meshTransform")
        transition.animateMeshTransform(
            layer: backdropLayer, from: previousTransform, to: meshTransform
        )
    } else {
        backdropLayer.setValue(meshTransform, forKey: "meshTransform")
    }
}

代价与风险:meshTransform 是私有属性。 实现依赖私有 CAMeshTransform。 网格拓扑变化会使插值失效。

证据链 · #07-10iOS 未做二进制验证
源码分析
已在 Telegram-iOS 固定提交中逐行确认,共 2 处源码位置。
报告摘要
怎么做到:构造位移网格 roundedRect,动画前后网格。
二进制验证
未做。App Store 版 IPA 受 FairPlay 加密,无法合法取得解密的可执行文件。

11. 把可变模糊半径编码进一张一像素宽的 mask

做了什么:代码生成一像素宽的黑白渐变图。 它拉伸该图覆盖任意宽度。 它把图作为私有 variableBlur 的 inputMaskImage。

为什么这样做:可变模糊只沿垂直方向变化。 一像素宽的模板避免生成整张大位图。 公开 UIBlurEffect 不能用 mask 指定局部半径。

关键代码摘录

EdgeEffect.swift · L693–L698 等 2 处
if let cgImage = image?.cgImage {
    variableBlur.setValue(cgImage, forKey: "inputMaskImage")
}
self.layer.filters = [variableBlur]

let baseHeight = max(1.0, params.gradient.height + inwardsExtension)
let resizingInverted = params.placement.position != .bottom
self.gradientImage = generateImage(CGSize(width: 1.0, height: baseHeight), opaque: false,
    rotatedContext: { size, context in
        let gradientColors = [
            UIColor.white.withAlphaComponent(1.0).cgColor,
            UIColor.white.withAlphaComponent(0.0).cgColor
        ] as CFArray
        let gradient = CGGradient(colorsSpace: CGColorSpaceCreateDeviceRGB(),
            colors: gradientColors, locations: [0.0, 1.0])!
        context.drawLinearGradient(gradient, start: .zero,
            end: CGPoint(x: 0.0, y: size.height), options: [])
    }
)?.resizableImage(withCapInsets: UIEdgeInsets(bottom: baseHeight), resizingMode: .stretch)

代价与风险:variableBlur 和 inputMaskImage 是私有 API。 渐变方向和亮度语义必须与系统滤镜一致。 该层还可能增加离屏合成。

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

12. 波纹优先用位移贴图,缺失时回退网格变形

#07-12Telegram-iOSUIKit源码分析私有 API性能与内存

做了什么:代码优先生成低分辨率的位移贴图。 它根据性能自适应贴图分辨率。 没有位移层时,它回退私有 mesh 变形。

为什么这样做:位移贴图的采样成本较低。 网格能兼容没有位移滤镜的路径。 两条路径都让底下内容形成径向波纹。

关键代码摘录

SpaceWarpView.swift · L888–L912
if self.backdropLayer != nil {
    if let displacementMapLayer = self.displacementMapLayer {
        let baseMapResolution = (
            x: max(2, resolution.x / 2),
            y: max(2, resolution.y / 2)
        )
        let mapResolution = self.adaptiveDisplacementMapResolution(
            from: baseMapResolution
        )
        displacementMapLayer.contents = self.makeDisplacementMapImage(
            size: size, resolution: mapResolution, params: params
        )
        self.transformContainerLayer.setValue(nil, forKey: "meshTransform")
    } else {
        if let meshTransform = self.makeRippleMeshTransform(
            size: size, resolution: resolution, params: params
        ) {
            self.transformContainerLayer.setValue(meshTransform, forKey: "meshTransform")
        }
    }
}

代价与风险:贴图会有分辨率伪影。 回退使用私有 meshTransform。 两条渲染路径需保持视觉一致。

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

13. 自绘 Liquid Glass 用 Metal 输出私有位移图的 RG 编码

#07-13Telegram-iOSUIKit源码分析私有 APIMetal / GPU

做了什么:Shader 在每个形状边缘计算向内采样偏移。 它将 X 和 Y 偏移编码进红绿通道。 中性值 0.5 表示不位移。

为什么这样做:玻璃边缘应显示更靠内的背景内容。 把折射场预渲成位移图可交给后续滤镜采样。 这避免在内容 shader 中重复计算形状距离。

关键代码摘录

LiquidGlassShapes.metal · L497–L524
float2 sampleOffset = float2(0.0);
for (int i = 0; i < 3; i++) {
    if (uniforms.layerRimGlass[i].y == 0.0) {
        continue;
    }
    LiquidGlassShapeSample shape = liquidGlassShape(samples, i, point, uniforms);
    float coverage = liquidGlassCoverage(shape.distance, pixelsPerPoint);
    float falloff = 1.0 - saturate(max(shape.distance, 0.0) / uniforms.glassBand);
    sampleOffset += shape.inwardNormal * (
        uniforms.glassShift * falloff * falloff * coverage
    );
}

float2 encoded = saturate(
    0.5 - 0.5 * sampleOffset / uniforms.glassAmount
);
return half4(half(encoded.x), half(encoded.y), 1.0, 1.0);

代价与风险:依赖私有 displacementMap 滤镜时仍有审核风险。 位移量与滤镜 amount 必须严格匹配。 三形状循环是固定上限。

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

14. 每帧自绘玻璃,但离屏后及时释放昂贵 Surface

做了什么:代码为内容、遮罩和位移图分别渲染专属 Surface。 源码注释说明录音 blob 的 3x Surface 约为 5.5 MB。 离开窗口时,策略延迟到当前 run loop 末尾检查。

为什么这样做:专属 Surface 适合每帧都会重绘的层。 立刻释放会误伤跨窗口移动的短暂空窗。 延迟一个 run loop 可避免无意义的重分配。

关键代码摘录

LiquidGlassShapesLayer.swift · L358–L389 等 2 处
let spec = RenderLayerSpec(
    size: renderSize,
    edgeInset: Constants.edgeInset,
    prefersDedicatedSurface: true
)
context.renderToLayer(
    spec: spec,
    state: LiquidGlassShapesRenderState<Pass, Kind>.self,
    layer: layer,
    inputs: samples,
    commands: { encoder, placement, samples in
        guard let samples else { return }
        encoder.setFragmentBytes(&uniforms,
            length: MemoryLayout<LiquidGlassShapesUniforms>.stride, index: 0)
        encoder.setFragmentBuffer(samples, offset: 0, index: 1)
        encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 6)
    }
)

代价与风险:多 Surface 会提升峰值显存。 资源释放的时序需要和窗口生命周期保持一致。 无明显私有 API 风险。

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

15. Spoiler 用私有 CAEmitterBehavior 实现闪烁和吸引力

#07-15Telegram-iOSUIKit源码分析私有 API运行时 hackCore Animation文本与输入

做了什么:代码通过 IMP 调用私有 emitter 工厂。 调用方可以取得闪烁和吸引器行为对象。 公开 CAEmitterLayer 仍负责粒子发射。

为什么这样做:公共 emitter API 没有 twinkle 或 attractor。 系统行为让尘埃不只是匀速随机点。 它能围绕手指或随机点形成活跃的 spoiler 表面。

关键代码摘录

SpoilerDustView.swift · L239–L250
/// Private `CAEmitterBehavior` bridge (no public API exists for the twinkle / attractor).
@available(iOS 13.0, *)
extension CAEmitterCell {
    static func createEmitterBehavior(type: String) -> NSObject {
        let selector = ["behaviorWith", "Type:"].joined()
        let behaviorClass = NSClassFromString(["CA", "Emitter", "Behavior"].joined())
            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
        )
    }
}

代价与风险:私有类、selector 和 unsafeBitCast 风险很高。 解混淆类名为 CAEmitterBehavior。 解混淆 selector 为 behaviorWithType:。 两者由数组拼接文本直接确认。

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

16. 点按揭示 spoiler 时,反向径向 mask 让粒子从触点爆开

#07-16Telegram-iOSUIKit源码分析文本与输入性能与内存

做了什么:代码生成中心透明、外部不透明的 mask。 半径同时受 10 pt 和文本区域尺寸限制。 该 mask 作为粒子揭示区域的触点孔洞。

为什么这样做:直接淡出只会均匀消失。 反向 mask 保持触点附近先被清空。 周边粒子仍可继续发射和闪烁。

关键代码摘录

SpoilerDustView.swift · L183–L199
private static func emitterMaskImage(size: CGSize, position: CGPoint) -> UIImage? {
    guard size.width > 0, size.height > 0 else { return nil }
    let renderer = UIGraphicsImageRenderer(size: size)
    return renderer.image { ctx in
        let c = ctx.cgContext
        c.clear(CGRect(origin: .zero, size: size))
        let locations: [CGFloat] = [0.0, 0.7, 0.95, 1.0]
        let colors = [
            UIColor(white: 1, alpha: 0).cgColor,
            UIColor(white: 1, alpha: 0).cgColor,
            UIColor(white: 1, alpha: 1).cgColor,
            UIColor(white: 1, alpha: 1).cgColor
        ] as CFArray
        let endRadius = min(10.0, min(size.width, size.height) * 0.4)
        c.drawRadialGradient(gradient, startCenter: position, startRadius: 0,
            endCenter: position, endRadius: endRadius, options: .drawsAfterEndLocation)
    }
}

代价与风险:每次生成 UIImage 有 CPU 和内存成本。 代码中省略了大尺寸降采样路径。 该限制对短文本行成立,但对大区域未验证。

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

17. 动态渐变不是每帧算 shader,而是预烘 8 个阶段并动画 contents

做了什么:代码先生成一串渐变位图。 它用 CAKeyframeAnimation 动画 layer 的 contents。 它把同一关键帧同步给原色和降饱和 clone。

为什么这样做:软件渐变生成较贵。 把生成移到阶段切换时,不需每帧 CPU 重绘。 clone 使用同一节奏,所以壁纸与 dimmed 副本不漂移。

关键代码摘录

SoftwareGradientBackground.swift · L637–L675 等 2 处
self.contentView.layer.removeAnimation(forKey: "contents")
self.contentView.layer.add(animation, forKey: "contents")

if !self.rawCloneNodes.isEmpty {
    let cloneAnimation = CAKeyframeAnimation(keyPath: "contents")
    cloneAnimation.values = images.map { $0.0.cgImage! }
    cloneAnimation.duration = animation.duration
    cloneAnimation.calculationMode = animation.calculationMode
    cloneAnimation.isRemovedOnCompletion = animation.isRemovedOnCompletion
    cloneAnimation.fillMode = animation.fillMode
    cloneAnimation.beginTime = animation.beginTime
    for cloneNode in self.rawCloneNodes {
        if let value = cloneNode.value {
            value.image = images.last?.0
            value.layer.removeAnimation(forKey: "contents")
            value.layer.add(cloneAnimation, forKey: "contents")
        }
    }
}

代价与风险:位图关键帧占用内存。 快速连续触发会重复生成位图。 该方案的色彩连续性取决于阶段数量。

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

18. 多个动态壁纸用共享阶段保持相位一致

做了什么:代码把阶段循环限制在 0...7。 它把发起者之外的背景同步到同一 phase。 它用管道广播而不是各自用时间戳计算。

为什么这样做:同一聊天中可能同时有主壁纸和 clone。 独立阶段会使两个渐变颜色错相。 显式相位同步能保持“同一张壁纸”的观感。

关键代码摘录

SoftwareGradientBackground.swift · L770–L794
if extendAnimation || backwards {
    self.invalidated = true
} else {
    if self.phase == 0 {
        self.phase = 7
    } else {
        self.phase = self.phase - 1
    }
}
if self.useSharedAnimationPhase {
    GradientBackgroundNode.sharedPhase = self.phase
    GradientBackgroundNode.sharedAnimationSyncPipe.putNext(
        SharedAnimationUpdate(phase: self.phase, sender: self)
    )
}
if let size = self.validLayout {
    self.updateLayout(size: size, transition: transition,
        extendAnimation: extendAnimation, backwards: backwards, completion: completion)
} else {
    completion()
}

代价与风险:管道订阅者的生命周期必须正确清理。 8 个离散阶段可能在极慢动画中显出跳变。 无明显私有 API 风险。

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

19. Shimmer 用超宽 mask 和加法 position 无限滚动

做了什么:mask 比实际内容宽两段完整行程。 动画只对 position.x 做加法位移。 布局变化时,它先更新静态基准位置再重启动画。

为什么这样做:超宽 mask 可让亮带从内容外进入和离开。 加法动画不需要把 model layer 写到终点。 重置基准位置可避免尺寸改变后的闪现。

关键代码摘录

ShimmeringMaskView.swift · L61–L81 等 2 处
let travelDelta = params.containerWidth + params.gradientWidth
let animation = self.maskLayer.makeAnimation(
    from: 0.0 as NSNumber,
    to: travelDelta as NSNumber,
    keyPath: "position.x",
    timingFunction: CAMediaTimingFunctionName.easeOut.rawValue,
    duration: self.duration,
    delay: 0.0,
    mediaTimingFunction: nil,
    removeOnCompletion: true,
    additive: true
)
animation.repeatCount = Float.infinity
self.maskLayer.add(animation, forKey: "shimmer")

let travelDistance = containerWidth + gradientWidth
let maskWidth = size.width + 2.0 * travelDistance
let staticPositionX = -gradientWidth * 0.5 - offsetX
let maskPosition = CGPoint(x: staticPositionX, y: size.height * 0.5)
transition.setBounds(layer: self.maskLayer, bounds: maskBounds)
transition.setPosition(layer: self.maskLayer, position: maskPosition)

代价与风险:永久 CA 动画会占用合成资源。 层级进入后才启动,依赖 HierarchyTrackingLayer 时序。 无明显私有 API 风险。

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

20. 气泡撕裂边缘用“过冲一像素”消除平铺接缝

做了什么:黑色撕裂带超出气泡边界。 白色边缘图却避开 overhang 和尾巴预留列。 上边图刻意少一个屏幕像素高。

为什么这样做:黑带过冲能消除气泡轮廓的残留细线。 白边若全宽会把壁纸绘到气泡外。 少一个像素避免平铺图回卷成半像素白缝。

关键代码摘录

ChatMessageBackgroundTear.swift · L289–L314
let graphicsInset = bubbleTearHorizontalOverhang + bubbleTearGraphicsHorizontalInset
let graphicsX = graphicsInset + self.tailInsets.left
let graphicsWidth = max(0.0, size.width - graphicsInset * 2.0
    - self.tailInsets.left - self.tailInsets.right)

if let topEdgeView {
    topEdgeView.frame = CGRect(
        x: graphicsX,
        y: bubbleTearEdgeInset,
        width: graphicsWidth,
        height: bubbleTearEdgeHeight - UIScreenPixel
    )
}
if let bottomEdgeView {
    bottomEdgeView.frame = CGRect(
        x: graphicsX,
        y: size.height - bubbleTearEdgeHeight - bubbleTearEdgeInset,
        width: graphicsWidth,
        height: bubbleTearEdgeHeight
    )
}

代价与风险:这依赖当前资源尺寸和像素栅格。 改动气泡尾巴或图块高度后必须重测。 无明显私有 API 风险。

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

本页目录