视觉特效与玻璃
私有 Liquid Lens、CABackdropLayer 与可变模糊。
范围:Telegram-iOS 的私有 Core Animation 视觉特效、Liquid Glass、壁纸透出气泡、粒子遮罩、渐变壁纸与边缘模糊。
结论:该版本把私有 CA 管线封装成可降级能力。
它优先使用 iOS 26 的私有 Liquid Lens。
它在旧系统自绘玻璃并使用私有 backdrop。
它还用 _UIPortalView 让聊天气泡透出同一壁纸。
以下按意外程度和工程价值排序。
1. 直接驱动 iOS 26 的私有 Liquid Lens
做了什么:代码按运行时类名创建 _UILiquidLensView。
它把抬起内容和挖空内容交给私有 Lens。
它把 Lens 放在背景和交互内容之间。
为什么这样做:普通 UIVisualEffectView 没有系统 Lens 的抬起形变。
私有 Lens 能同时处理背景折射和内容挖空。
关键代码摘录
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。
- 源码分析
- 已在 Telegram-iOS 固定提交中逐行确认,共 1 处源码位置。
- 报告摘要
- 怎么做到:
perform调私有initWithRestingBackground:与 setter。 - 二进制验证
- 未做。App Store 版 IPA 受 FairPlay 加密,无法合法取得解密的可执行文件。
2. 为私有 Lens 补齐弹跳刷新和位置动画
做了什么:代码先发起 bounds 动画。 它随后清空私有 Lens 的残留动画。 它再写入最终 bounds。 它还补一个随宽度变化的加法 position 动画。
为什么这样做:Lens 的内部动画会覆盖外部状态。 代码注释明确表示需要此位置补偿。 该补偿的根因在源码中标为“未知”。
关键代码摘录
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() 可能中断系统新增动画。
补偿公式需要持续实机回归。
- 源码分析
- 已在 Telegram-iOS 固定提交中逐行确认,共 2 处源码位置。
- 报告摘要
- 怎么做到:记录前后尺寸,
animatePosition从“宽差/2”回到 0。 - 二进制验证
- 未做。App Store 版 IPA 受 FairPlay 加密,无法合法取得解密的可执行文件。
3. 私有 Lens 动画期间强制 120 Hz
做了什么:代码为每个 morph 申请共享显示链路。 它把支持 ProMotion 的屏幕锁定为 120 Hz。 它使用引用计数合并并发 morph。 它还用一秒超时释放未回调的申请。
为什么这样做:源码注释记录了系统 morph 会被降到约 80 Hz。 空 tick 让显示链路持续提出固定帧率请求。 普通动画只设范围下限会允许系统节流。
关键代码摘录
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 及高刷屏生效。
- 源码分析
- 已在 Telegram-iOS 固定提交中逐行确认,共 1 处源码位置。
- 报告摘要
- 怎么做到:
.common模式添加到 RunLoop,并 1 秒超时兜底。 - 二进制验证
- 未做。App Store 版 IPA 受 FairPlay 加密,无法合法取得解密的可执行文件。
4. 没有私有 Lens 时,用亮度转 Alpha 模拟挖空玻璃
做了什么:代码把白色容器作为内容 mask。
它给 mask 加私有 luminanceToAlpha 滤镜。
它以可拉伸圆角胶囊图控制挖空区域。
为什么这样做:旧系统没有 _UILiquidLensView。
普通矩形裁剪无法制造透视孔洞。
亮度转 Alpha 可让黑色 blob 直接消去内容。
关键代码摘录
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 可能触发离屏渲染。
- 源码分析
- 已在 Telegram-iOS 固定提交中逐行确认,共 2 处源码位置。
- 二进制验证
- 未做。App Store 版 IPA 受 FairPlay 加密,无法合法取得解密的可执行文件。
5. 用 _UIPortalView 复制远端视图,而非截图
做了什么:代码运行时解析并实例化 _UIPortalView。
它同步源视图的位置和变换。
它禁用命中测试转发和 portal 命中测试。
为什么这样做:截图或 contentsRect 不会随源壁纸同步更新。
Portal 让同一渲染源在另一个层级显示。
因此不需要每帧手动反向平移内容。
关键代码摘录
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。
拼接文本可直接确认该名字。
审核风险很高。
- 源码分析
- 已在 Telegram-iOS 固定提交中逐行确认,共 1 处源码位置。
- 二进制验证
- 未做。App Store 版 IPA 受 FairPlay 加密,无法合法取得解密的可执行文件。
6. Portal 重绑后原位置重插,迫使渲染刷新
做了什么:代码先重新设置 sourceView。
它再把 portal 重插回原来的下标。
它在 UIView 和纯 CALayer 两种宿主中都处理。
为什么这样做:仅改私有 sourceView 可能不触发 portal 重建。
原位重插不改变 z-order。
它却能迫使 UIKit 或 CA 重新连接源。
关键代码摘录
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 的失效语义。 重插可能与宿主的层级动画冲突。
- 源码分析
- 已在 Telegram-iOS 固定提交中逐行确认,共 1 处源码位置。
- 二进制验证
- 未做。App Store 版 IPA 受 FairPlay 加密,无法合法取得解密的可执行文件。
7. 聊天气泡真正“透出”同一张动态壁纸
做了什么:每种气泡类型都有各自的壁纸 portal 源。
气泡节点只显示相同源的 PortalView。
它不在每个气泡内重新绘制壁纸。
为什么这样做:旧 contentsRect 路径需管理滚动反向位移。
源码注释说明该路径会把整张壁纸压入气泡。
Portal 让滚动、动画和壁纸坐标天然一致。
关键代码摘录
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。
私有类不存在时,代码退化为平面主题气泡。
- 源码分析
- 已在 Telegram-iOS 固定提交中逐行确认,共 1 处源码位置。
- 二进制验证
- 未做。App Store 版 IPA 受 FairPlay 加密,无法合法取得解密的可执行文件。
8. 拆解 UIVisualEffectView 内部滤镜,获得可控导航模糊
做了什么:代码从公开 UIVisualEffectView 取得系统内部层。
它隐藏内部视觉子视图。
它只保留 gaussianBlur 和可选 colorSaturate。
为什么这样做:标准效果包含系统定义的叠色和材质层。 导航栏只需要背景采样、模糊和饱和度。 这个做法还能保持无障碍“降低透明度”降级。
关键代码摘录
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 属性。
未来系统修改效果结构会破坏该实现。
- 源码分析
- 已在 Telegram-iOS 固定提交中逐行确认,共 1 处源码位置。
- 二进制验证
- 未做。App Store 版 IPA 受 FairPlay 加密,无法合法取得解密的可执行文件。
9. 用 CABackdropLayer 和颜色矩阵手搓旧版玻璃
做了什么:代码创建私有 CABackdropLayer。
它先应用 backdrop-aware 颜色矩阵。
它再应用小半径高斯模糊。
为什么这样做:CABackdropLayer 直接采样其后的已合成画面。
颜色矩阵会产生比公开 blur 更强的玻璃染色。
它是原生 Liquid Glass 不可用时的传统回退。
关键代码摘录
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" 可直接确认该名字。
审核风险很高。
- 源码分析
- 已在 Telegram-iOS 固定提交中逐行确认,共 2 处源码位置。
- 报告摘要
- 怎么做到:
NSClassFromString("CABackdropLayer")与CAFilter链。 - 二进制验证
- 未做。App Store 版 IPA 受 FairPlay 加密,无法合法取得解密的可执行文件。
10. 私有 CAMeshTransform 让 backdrop 边缘像玻璃一样折射
做了什么:代码为圆角矩形生成位移网格。
它把网格写到 backdrop 的私有 meshTransform 键。
尺寸变化时,它显式插值旧网格和新网格。
为什么这样做:普通圆角裁剪不会移动背景采样点。 网格能在边缘偏移 backdrop,模拟折射。 显式网格动画避免形状突变。
关键代码摘录
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。
网格拓扑变化会使插值失效。
- 源码分析
- 已在 Telegram-iOS 固定提交中逐行确认,共 2 处源码位置。
- 报告摘要
- 怎么做到:构造位移网格
roundedRect,动画前后网格。 - 二进制验证
- 未做。App Store 版 IPA 受 FairPlay 加密,无法合法取得解密的可执行文件。
11. 把可变模糊半径编码进一张一像素宽的 mask
做了什么:代码生成一像素宽的黑白渐变图。
它拉伸该图覆盖任意宽度。
它把图作为私有 variableBlur 的 inputMaskImage。
为什么这样做:可变模糊只沿垂直方向变化。
一像素宽的模板避免生成整张大位图。
公开 UIBlurEffect 不能用 mask 指定局部半径。
关键代码摘录
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。
渐变方向和亮度语义必须与系统滤镜一致。
该层还可能增加离屏合成。
- 源码分析
- 已在 Telegram-iOS 固定提交中逐行确认,共 2 处源码位置。
- 二进制验证
- 未做。App Store 版 IPA 受 FairPlay 加密,无法合法取得解密的可执行文件。
12. 波纹优先用位移贴图,缺失时回退网格变形
做了什么:代码优先生成低分辨率的位移贴图。 它根据性能自适应贴图分辨率。 没有位移层时,它回退私有 mesh 变形。
为什么这样做:位移贴图的采样成本较低。 网格能兼容没有位移滤镜的路径。 两条路径都让底下内容形成径向波纹。
关键代码摘录
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。
两条渲染路径需保持视觉一致。
- 源码分析
- 已在 Telegram-iOS 固定提交中逐行确认,共 1 处源码位置。
- 二进制验证
- 未做。App Store 版 IPA 受 FairPlay 加密,无法合法取得解密的可执行文件。
13. 自绘 Liquid Glass 用 Metal 输出私有位移图的 RG 编码
做了什么:Shader 在每个形状边缘计算向内采样偏移。
它将 X 和 Y 偏移编码进红绿通道。
中性值 0.5 表示不位移。
为什么这样做:玻璃边缘应显示更靠内的背景内容。 把折射场预渲成位移图可交给后续滤镜采样。 这避免在内容 shader 中重复计算形状距离。
关键代码摘录
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 必须严格匹配。
三形状循环是固定上限。
- 源码分析
- 已在 Telegram-iOS 固定提交中逐行确认,共 1 处源码位置。
- 二进制验证
- 未做。App Store 版 IPA 受 FairPlay 加密,无法合法取得解密的可执行文件。
14. 每帧自绘玻璃,但离屏后及时释放昂贵 Surface
做了什么:代码为内容、遮罩和位移图分别渲染专属 Surface。 源码注释说明录音 blob 的 3x Surface 约为 5.5 MB。 离开窗口时,策略延迟到当前 run loop 末尾检查。
为什么这样做:专属 Surface 适合每帧都会重绘的层。 立刻释放会误伤跨窗口移动的短暂空窗。 延迟一个 run loop 可避免无意义的重分配。
关键代码摘录
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 风险。
- 源码分析
- 已在 Telegram-iOS 固定提交中逐行确认,共 2 处源码位置。
- 二进制验证
- 未做。App Store 版 IPA 受 FairPlay 加密,无法合法取得解密的可执行文件。
15. Spoiler 用私有 CAEmitterBehavior 实现闪烁和吸引力
做了什么:代码通过 IMP 调用私有 emitter 工厂。
调用方可以取得闪烁和吸引器行为对象。
公开 CAEmitterLayer 仍负责粒子发射。
为什么这样做:公共 emitter API 没有 twinkle 或 attractor。 系统行为让尘埃不只是匀速随机点。 它能围绕手指或随机点形成活跃的 spoiler 表面。
关键代码摘录
/// 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:。
两者由数组拼接文本直接确认。
- 源码分析
- 已在 Telegram-iOS 固定提交中逐行确认,共 1 处源码位置。
- 二进制验证
- 未做。App Store 版 IPA 受 FairPlay 加密,无法合法取得解密的可执行文件。
16. 点按揭示 spoiler 时,反向径向 mask 让粒子从触点爆开
做了什么:代码生成中心透明、外部不透明的 mask。 半径同时受 10 pt 和文本区域尺寸限制。 该 mask 作为粒子揭示区域的触点孔洞。
为什么这样做:直接淡出只会均匀消失。 反向 mask 保持触点附近先被清空。 周边粒子仍可继续发射和闪烁。
关键代码摘录
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 和内存成本。
代码中省略了大尺寸降采样路径。
该限制对短文本行成立,但对大区域未验证。
- 源码分析
- 已在 Telegram-iOS 固定提交中逐行确认,共 1 处源码位置。
- 二进制验证
- 未做。App Store 版 IPA 受 FairPlay 加密,无法合法取得解密的可执行文件。
17. 动态渐变不是每帧算 shader,而是预烘 8 个阶段并动画 contents
做了什么:代码先生成一串渐变位图。
它用 CAKeyframeAnimation 动画 layer 的 contents。
它把同一关键帧同步给原色和降饱和 clone。
为什么这样做:软件渐变生成较贵。 把生成移到阶段切换时,不需每帧 CPU 重绘。 clone 使用同一节奏,所以壁纸与 dimmed 副本不漂移。
关键代码摘录
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")
}
}
}代价与风险:位图关键帧占用内存。 快速连续触发会重复生成位图。 该方案的色彩连续性取决于阶段数量。
- 源码分析
- 已在 Telegram-iOS 固定提交中逐行确认,共 2 处源码位置。
- 二进制验证
- 未做。App Store 版 IPA 受 FairPlay 加密,无法合法取得解密的可执行文件。
18. 多个动态壁纸用共享阶段保持相位一致
做了什么:代码把阶段循环限制在 0...7。
它把发起者之外的背景同步到同一 phase。
它用管道广播而不是各自用时间戳计算。
为什么这样做:同一聊天中可能同时有主壁纸和 clone。 独立阶段会使两个渐变颜色错相。 显式相位同步能保持“同一张壁纸”的观感。
关键代码摘录
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 风险。
- 源码分析
- 已在 Telegram-iOS 固定提交中逐行确认,共 1 处源码位置。
- 二进制验证
- 未做。App Store 版 IPA 受 FairPlay 加密,无法合法取得解密的可执行文件。
19. Shimmer 用超宽 mask 和加法 position 无限滚动
做了什么:mask 比实际内容宽两段完整行程。
动画只对 position.x 做加法位移。
布局变化时,它先更新静态基准位置再重启动画。
为什么这样做:超宽 mask 可让亮带从内容外进入和离开。 加法动画不需要把 model layer 写到终点。 重置基准位置可避免尺寸改变后的闪现。
关键代码摘录
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 风险。
- 源码分析
- 已在 Telegram-iOS 固定提交中逐行确认,共 2 处源码位置。
- 二进制验证
- 未做。App Store 版 IPA 受 FairPlay 加密,无法合法取得解密的可执行文件。
20. 气泡撕裂边缘用“过冲一像素”消除平铺接缝
做了什么:黑色撕裂带超出气泡边界。 白色边缘图却避开 overhang 和尾巴预留列。 上边图刻意少一个屏幕像素高。
为什么这样做:黑带过冲能消除气泡轮廓的残留细线。 白边若全宽会把壁纸绘到气泡外。 少一个像素避免平铺图回卷成半像素白缝。
关键代码摘录
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 风险。
- 源码分析
- 已在 Telegram-iOS 固定提交中逐行确认,共 1 处源码位置。
- 二进制验证
- 未做。App Store 版 IPA 受 FairPlay 加密,无法合法取得解密的可执行文件。