Telegram 技巧档案UIKIT · APPKIT
iOS · UIKit

运行时与私有 API

方法交换、私有类名混淆、XOR 字节串与精确 ABI 调用。

范围:Telegram-iOS(f1dd7a2dbd02cbbf513e75d5695d8d36d1cf5838)的 UIKit 运行时 hack、方法交换与私有 API 混淆。

1. 借安全文本输入层给任意图层打上截图排除标记

#01-01Telegram-iOSUIKit报告精选 01-1源码分析私有 APICore Animation文本与输入

做了什么:代码找到 UITextField 的内部 TextLayoutCanvasView。代码临时用 KVC 把目标 CALayer 替换成该视图的 layer。代码切换 secureTextEntry 后恢复原图层。秘密聊天的历史节点会调用此函数。

为什么这样做:公开 UIKit 没有“排除指定图层截图”的 API。安全文本输入由系统处理截图保护。该做法把系统标记转移给任意显示图层。

关键代码摘录

UIKitUtils.m · L299–L328
    dispatch_once(&onceToken, ^{
        textField = [[UITextField alloc] init];
        for (UIView *subview in textField.subviews) {
            if ([NSStringFromClass([subview class]) containsString:@"TextLayoutCanvasView"]) {
                secureView = subview;
                break;
            }
        }
    });
    if (secureView == nil) {
        return;
    }
    CALayer *previousLayer = secureView.layer;
    [secureView setValue:layer forKey:@"layer"];
    if (disableScreenshots) {
        textField.secureTextEntry = false;
        textField.secureTextEntry = true;
    } else {
        textField.secureTextEntry = true;
        textField.secureTextEntry = false;
    }
    [secureView setValue:previousLayer forKey:@"layer"];
    [layer setAssociatedObject:@(disableScreenshots) forKey:layerDisableScreenshotsKey
             associationPolicy:NSObjectAssociationPolicyRetain];
}

代价与风险:它依赖内部子视图类名和可 KVC 写入的 layer。系统层级或实现改变会失效。它使用未公开行为,存在审核和兼容性风险。

证据链 · #01-01iOS 未做二进制验证
源码分析
已在 Telegram-iOS 固定提交中逐行确认,共 1 处源码位置。
报告摘要
怎么做到:dispatch_once 缓存内部子视图,KVC 交换 layer,切换两次 secureTextEntry,恢复原图层。
二进制验证
未做。App Store 版 IPA 受 FairPlay 加密,无法合法取得解密的可执行文件。

2. 用 XOR 字节串隐藏私有拼写控制器,并用精确 ABI 调用它

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

做了什么:代码将私有类和 selector 的 UTF-8 字节逐字节 XOR 0x5A。代码只在运行时解码并查找。代码把带 BOOL 的语法检查调用转换为准确的 C 函数类型。

混淆还原:我按 byte ^ 0x5A 解码源码数组。结果是 UITextCheckingController、initWithClient:、preheatTextChecker、invalidate、checkSpellingForWordInRange: 和 checkGrammarForSentenceInRannge:onPause:。最后一个真实字符串含 Rannge 的双 n。属性键还原为 NSTextAlternativesDisplayStyle、NSTextAlternatives、alternativeStrings 和 primaryString。

为什么这样做:perform 不能安全传递尾部标量 BOOL。私有符号明文也更容易进入产物。运行时可用性检查让类不存在时直接关闭检查。

关键代码摘录

NativeTextChecking.swift · L4–L88
/// Runtime-assembled private symbol strings (XOR 0x5A) so no plaintext private
/// class/selector appears in the binary.
enum ObfuscatedStrings {
    static let key: UInt8 = 0x5A
    static func decode(_ bytes: [UInt8]) -> String {
        String(decoding: bytes.map { $0 ^ key }, as: UTF8.self)
    }
    static let classNameBytes: [UInt8] = [15, 19, 14, 63, 34, 46, 25, 50, 63, 57, 49, 51, 52, 61, 25, 53, 52, 46, 40, 53, 54, 54, 63, 40]
    static let selInitWithClient: [UInt8] = [51, 52, 51, 46, 13, 51, 46, 50, 25, 54, 51, 63, 52, 46, 96]
    static let selCheckGrammar: [UInt8] = [57, 50, 63, 57, 49, 29, 40, 59, 55, 55, 59, 40, 28, 53, 40, 9, 63, 52, 46, 63, 52, 57, 63, 19, 52, 8, 59, 52, 52, 61, 63, 96, 53, 52, 10, 59, 47, 41, 63, 96]
}
// ...
let sel = NSSelectorFromString(ObfuscatedStrings.decode(ObfuscatedStrings.selCheckGrammar))
guard controller.responds(to: sel) else { return }
typealias GrammarFn = @convention(c) (AnyObject, Selector, AnyObject, Bool) -> Void
let fn = unsafeBitCast(controller.method(for: sel), to: GrammarFn.self)
fn(controller, sel, textRange, true)

代价与风险:私有 UITextCheckingController 存在审核风险。selector、回调协议或 ABI 改变会导致功能关闭或崩溃。源码明确没有公开 API 回退。

证据链 · #01-02iOS 未做二进制验证
源码分析
已在 Telegram-iOS 固定提交中逐行确认,共 1 处源码位置。
报告摘要
怎么做到:NSSelectorFromString 与 method(for:) 提取 IMP,unsafeBitCast 为准确签名后调用。
二进制验证
未做。App Store 版 IPA 受 FairPlay 加密,无法合法取得解密的可执行文件。

3. 伪造 Info.plist 结果,让 App-Bound 域名可以动态增删

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

做了什么:代码交换 NSBundle.objectForInfoDictionaryKey:。当 WebKit 查询 WKAppBoundDomains 时,代码返回带引用计数的运行时域名列表。代码再经 dlsym 调用私有重载函数刷新指定数据存储。

混淆还原:@"WK" + @"WebsiteDataStoreReinitializeAppBoundDomains" 还原为 WKWebsiteDataStoreReinitializeAppBoundDomains。这是源码构造后传给 dlsym 的符号名。

为什么这样做:WKAppBoundDomains 通常在构建时写入 Info.plist。浏览器代码设置 limitsNavigationsToAppBoundDomains 后才登记当前主机。普通配置无法按网页生命周期动态改变该列表。

关键代码摘录

UIViewController+Navigation.m · L518–L543
+ (void)forceRefreshTrustedDomains:(WKWebsiteDataStore * _Nonnull)websiteDataStore {
    static void (*reinitializeFunction)(CFTypeRef dataStoreRef) = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSString *nameString = [NSString stringWithFormat:@"WK%@", @"WebsiteDataStoreReinitializeAppBoundDomains"];
        reinitializeFunction = dlsym(RTLD_DEFAULT, [nameString UTF8String]);
    });
    if (reinitializeFunction) {
        reinitializeFunction((__bridge CFTypeRef)(websiteDataStore));
    }
}
// ...
- (id)_65087dc8_objectForInfoDictionaryKey:(NSString *)key {
    if ([key isEqualToString:@"WKAppBoundDomains"]) {
        NSArray *result = [WebHelpers threadSafeTrustedDomains];
        if (result.count != 0) {
            return result;
        }
    }
    return [self _65087dc8_objectForInfoDictionaryKey:key];
}

代价与风险:它篡改全局 bundle 查询,并调用私有 C 符号。WebKit 改名、改参数或移除符号时会失效。存在审核风险。

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

4. 从私有键盘场景代理取得键盘窗口,并避开新版陷阱

做了什么:代码为未声明接口声明最小协议。代码通过 UIWindowScene.keyboardSceneDelegate.keyboardWindow 取得内部键盘窗口。每一步都先检查 selector 和结果类型。

为什么这样做:代码注释说明,iOS 16 后内部键盘窗口不在公开的 UIWindowScene.windows 和 UIApplication.windows。注释还说明旧的 UIRemoteKeyboardWindow remoteKeyboardWindowForScreen:create: 在链接 iOS 27 SDK 后会触发 brk #0。该路径刻意避开该入口。

关键代码摘录

UIViewController+Navigation.m · L333–L350 等 2 处
@protocol UIKeyboardSceneDelegateProtocol
- (UIWindow * _Nullable)keyboardWindow;
@end

@protocol UIWindowSceneKeyboardProtocol
- (id _Nullable)keyboardSceneDelegate;
@end
// ...
- (UIWindow * _Nullable)internalGetKeyboardForScene:(UIWindowScene * _Nullable)scene {
    if (scene == nil) {
        return nil;
    }
    if (![scene respondsToSelector:@selector(keyboardSceneDelegate)]) {
        return nil;
    }
    id keyboardSceneDelegate = [(id<UIWindowSceneKeyboardProtocol>)scene keyboardSceneDelegate];
    if (![keyboardSceneDelegate respondsToSelector:@selector(keyboardWindow)]) {
        return nil;
    }
    UIWindow *result = [(id<UIKeyboardSceneDelegateProtocol>)keyboardSceneDelegate keyboardWindow];
    if (![result isKindOfClass:[UIWindow class]]) {
        return nil;
    }
    return result;
}

代价与风险:keyboardSceneDelegate 和 keyboardWindow 都是私有接口。进程外键盘时会返回 nil。系统内部场景模型改变时会失效,并存在审核风险。

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

5. 用私有 cornerRadii 获得可动画的四角半径,避免离屏遮罩

#01-05Telegram-iOSUIKit报告精选 01-4源码分析私有 API

做了什么:代码探测私有 setter setCornerRadii:。代码按 QuartzCore 类型编码把四个 CGSize 装入 NSValue。代码用 KVC 读写私有 cornerRadii 属性。

为什么这样做:一个 cornerRadius 无法表达四个不同半径。代码注释指出,遮罩会造成离屏渲染,并且不会随图层自然动画。私有属性是可动画的图层属性。

关键代码摘录

CALayerCornerRadii.swift · L37–L124
public extension CALayer {
    static let cornerRadiiSupported: Bool = {
        return CALayer.instancesRespond(to: NSSelectorFromString("setCornerRadii:"))
    }()

    var cornerRadii: CornerRadii? {
        guard CALayer.cornerRadiiSupported,
              let value = self.value(forKey: cornerRadiiKey) as? NSValue else {
            return nil
        }
        return CornerRadii(boxedValue: value)
    }

    func setCornerRadii(_ radii: CornerRadii) {
        guard CALayer.cornerRadiiSupported else {
            return
        }
        self.setValue(radii.boxedValue, forKey: cornerRadiiKey)
    }
}

代价与风险:它依赖 CACornerRadii 的类型布局和私有 KVC 键。虽然先探测 selector,系统仍可改动值布局。该接口有审核风险。

证据链 · #01-05iOS 未做二进制验证
源码分析
已在 Telegram-iOS 固定提交中逐行确认,共 1 处源码位置。
报告摘要
怎么做到:构造 NSValue 封箱 CACornerRadii,instancesRespond 探测后调用。
二进制验证
未做。App Store 版 IPA 受 FairPlay 加密,无法合法取得解密的可执行文件。

6. 交换继承方法前先落地到目标类,避免污染父类

#01-06Telegram-iOSUIKit源码分析私有 API运行时 hack

做了什么:代码先尝试向目标类添加原 selector。添加成功说明原实现继承自父类。代码此时只在目标类安装交换后的入口,并把父类 IMP 放到替代 selector。只有原方法本来就在目标类时才直接交换。

为什么这样做:对继承来的 Method 直接 method_exchangeImplementations 会修改父类实现。这个常见细节会把一个子类 hook 意外扩散到其他子类。

关键代码摘录

RuntimeUtils.m · L21–L32
+ (void)swizzleInstanceMethodOfClass:(Class)targetClass
                     currentSelector:(SEL)currentSelector
                         newSelector:(SEL)newSelector {
    Method origMethod = class_getInstanceMethod(targetClass, currentSelector);
    Method newMethod = class_getInstanceMethod(targetClass, newSelector);
    if ((origMethod != nil) && (newMethod != nil)) {
        if (class_addMethod(targetClass, currentSelector,
                method_getImplementation(newMethod), method_getTypeEncoding(newMethod))) {
            class_replaceMethod(targetClass, newSelector,
                method_getImplementation(origMethod), method_getTypeEncoding(origMethod));
        } else {
            method_exchangeImplementations(origMethod, newMethod);
        }
    }
}

代价与风险:交换依赖方法签名兼容。多个模块 hook 同一 selector 时仍会产生加载顺序耦合。无明显私有 API 风险。

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

7. 把 UIMenuItem 的文字渲染偷换成图标渲染

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

做了什么:代码给带图标的菜单标题追加 U+FEFF U+200B 隐形后缀。代码交换 NSString.sizeWithAttributes: 与 UILabel 的绘制、布局和 frame setter。代码根据标题反查菜单项,抑制文字并嵌入居中的 UIImageView。

为什么这样做:旧 UIMenuController 的 UIMenuItem 没有图标 API。代码借系统创建的标签视图完成布局,不需要自行复刻菜单、命中测试或动作分派。

关键代码摘录

UIMenuItem+Icons.m · L30–L164
- (void)_78724db9_layoutSubviews {
    UIMenuItem *item = [[UIMenuController sharedMenuController] findImageItemByTitle:self.text];
    UIImage *image = item._tg_image;
    if (image == nil) {
        [self _78724db9_layoutSubviews];
        return;
    }
    CGPoint point = CGPointMake(ceil((self.bounds.size.width - image.size.width) / 2.0),
                                ceil((self.bounds.size.height - image.size.height) / 2.0));
    UIImageView *imageView = [self associatedObjectForKey:imageViewKey];
    if (imageView == nil) {
        imageView = [[UIImageView alloc] init];
        [self addSubview:imageView];
        [self setAssociatedObject:imageView forKey:imageViewKey associationPolicy:NSObjectAssociationPolicyRetain];
    }
    imageView.image = image;
    imageView.frame = CGRectMake(point.x, point.y, image.size.width, image.size.height);
}

代价与风险:这是对所有 NSString 和 UILabel 实例的全局 hook。它依赖旧菜单内部使用标签。与其他 swizzle 冲突时风险高。无私有 selector,但维护成本高。

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

8. 从私有 _scrollHeartbeat 抓显示链接,强制旧系统滚动使用满帧率

#01-08Telegram-iOSUIKit源码分析私有 API运行时 hack手势与滚动性能与内存

做了什么:代码在 iOS 15 上以 KVC 读取 UIScrollView 的私有 _scrollHeartbeat。代码标记该 CADisplayLink,并请求屏幕最大帧率。另一个 CADisplayLink swizzle 会再次把带标记的范围固定到最大值。

混淆还原:@"_" + @"scrollHeartbeat" 还原为 _scrollHeartbeat。源码将该字符串传给 valueForKey:。

为什么这样做:公开滚动 API 不暴露内部心跳显示链接。只给应用自建显示链接设高帧率不会改变 UIKit 滚动。该代码只针对 iOS 15,iOS 16 直接跳过。

关键代码摘录

UIViewController+Navigation.m · L291–L317
- (void)fixScrollDisplayLink {
    if (@available(iOS 16.0, *)) {
        return;
    }
    static NSString *scrollHeartbeatKey = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        scrollHeartbeatKey = [NSString stringWithFormat:@"_%@", @"scrollHeartbeat"];
    });
    id value = [self valueForKey:scrollHeartbeatKey];
    if ([value isKindOfClass:[CADisplayLink class]]) {
        CADisplayLink *displayLink = (CADisplayLink *)value;
        if ([displayLink associatedObjectForKey:forceFullRefreshRateKey] == nil) {
            [displayLink setAssociatedObject:@true forKey:forceFullRefreshRateKey];
            if (@available(iOS 15.0, *)) {
                float maxFps = [UIScreen mainScreen].maximumFramesPerSecond;
                if (maxFps > 61.0f) {
                    [displayLink setPreferredFrameRateRange:CAFrameRateRangeMake(maxFps, maxFps, maxFps)];
                }
            }
        }
    }
}

代价与风险:私有 ivar/KVC 名称可能改变。它改变系统滚动的功耗和调度策略。存在审核与版本兼容风险。

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

9. 先读私有方法签名,再选择 float 或 double 的 IMP 调用约定

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

做了什么:代码动态查找 CASpringAnimation 的私有求值 selector。代码读取第一个显式参数的 Objective-C 类型编码。代码按 float 或 double 选择不同函数指针。找不到时退化为线性值。

混淆还原:@"_" + @"solveForInput:" 还原为 _solveForInput:。源码仅用该 selector 的 IMP 求弹簧在给定相位的值。

为什么这样做:不同系统的私有 ABI 已变化。硬编码一种函数指针会违反调用约定。公开 Core Animation 没有同等的逐相位私有求值器。

关键代码摘录

UIKitUtils.m · L22–L49
- (CGFloat)valueAt:(CGFloat)t {
    static float (*impl)(id, float) = NULL;
    static double (*dimpl)(id, double) = NULL;
    dispatch_once(&onceToken, ^{
        Method method = class_getInstanceMethod([CASpringAnimation class],
            NSSelectorFromString([@"_" stringByAppendingString:@"solveForInput:"]));
        if (method) {
            const char *encoding = method_getTypeEncoding(method);
            NSMethodSignature *signature = [NSMethodSignature signatureWithObjCTypes:encoding];
            const char *argType = [signature getArgumentTypeAtIndex:2];
            if (strncmp(argType, "f", 1) == 0) {
                impl = (float (*)(id, float))method_getImplementation(method);
            } else if (strncmp(argType, "d", 1) == 0) {
                dimpl = (double (*)(id, double))method_getImplementation(method);
            }
        }
    });
    if (impl) return (CGFloat)impl(self, (float)t);
    if (dimpl) return (CGFloat)dimpl(self, (double)t);
    return t;
}

代价与风险:私有 selector 或返回语义改变时,线性退化可能造成曲线不一致。若类型编码出现第三种形式,函数会静默退化。存在审核风险。

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

10. 给弹簧动画写入私有高帧率原因码

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

做了什么:代码组合公开的 preferredFrameRateRange 和私有 KVC 键 highFrameRateReason。代码只在 iOS 15 及以上设置这两项。

为什么这样做:仅设置首选帧率范围可能不足以让系统把动画归类为高帧率工作。私有原因码显然参与该内部决策。原因码的具体系统语义未验证。

关键代码摘录

UIKitUtils.m · L68–L83
CABasicAnimation * _Nonnull make26SpringAnimationImpl(NSString * _Nonnull keyPath,
                                                        double duration) {
    CASpringAnimation *springAnimation = [CASpringAnimation animationWithKeyPath:keyPath];
    springAnimation.mass = 1.0f;
    springAnimation.stiffness = 555.027;
    springAnimation.damping = 47.118;
    springAnimation.duration = duration;
    springAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
    if (@available(iOS 17.0, *)) {
        springAnimation.allowsOverdamping = false;
    }
    if (@available(iOS 15.0, *)) {
        [springAnimation setValue:@(1048619) forKey:@"highFrameRateReason"];
        springAnimation.preferredFrameRateRange = CAFrameRateRangeMake(80.0, 120.0, 120.0);
    }
    return springAnimation;
}

代价与风险:highFrameRateReason 是私有 KVC 键。整数 1048619 没有源码内说明。该代码可能增加耗电,且存在审核风险。

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

11. 用 NSInvocation 安全探测并配置私有模糊效果

#01-11Telegram-iOSUIKit源码分析私有 API

做了什么:代码在 iOS 11 到 iOS 12 解析私有类,并经 NSInvocation 调用存在的 setter。辅助函数先取得 NSMethodSignature。不存在的 setter 会成为无操作。

混淆还原:类名是 _UICustomBlurEffect。setter 依次是 setBlurRadius:、setColorTint:、setColorTintAlpha:、setDarkeningTintAlpha:、setGrayscaleTintAlpha: 和 setSaturationDeltaFactor:。这些都由数组拼接得到。

为什么这样做:旧系统没有满足该参数组合的公开模糊配置。NSInvocation 比把不同标量类型强转为单一 performSelector: 更安全。iOS 13 及以上改用公开材质样式。

关键代码摘录

UIKitUtils.m · L110–L200
NSString *string = [@[@"_", @"UI", @"Custom", @"BlurEffect"] componentsJoinedByString:@""];
CustomBlurEffect *result = (CustomBlurEffect *)[NSClassFromString(string) effectWithStyle:0];

setField(result, [@[@"set", @"BlurRadius", @":"] componentsJoinedByString:@""], 10.0);
setNilField(result, [@[@"set", @"Color", @"Tint", @":"] componentsJoinedByString:@""]);
setField(result, [@[@"set", @"Color", @"Tint", @"Alpha", @":"] componentsJoinedByString:@""], 0.0);
setField(result, [@[@"set", @"Darkening", @"Tint", @"Alpha", @":"] componentsJoinedByString:@""], 0.0);
setField(result, [@[@"set", @"Grayscale", @"Tint", @"Alpha", @":"] componentsJoinedByString:@""], 0.0);
setField(result, [@[@"set", @"Saturation", @"Delta", @"Factor", @":"] componentsJoinedByString:@""], 1.0);

if ([UIScreen mainScreen].scale > 2.5f) {
    setField(result, @"setScale:", 0.3);
} else {
    setField(result, @"setScale:", 0.5);
}
return result;

代价与风险:私有类和 setter 有审核风险。返回对象若为空,后续辅助函数依赖 Objective-C 向 nil 发消息。渲染效果细节不在本笔记范围。

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

12. 将私有 _UIPortalView 包装为最小协议,并关闭其命中测试转发

#01-12Telegram-iOSUIKit源码分析私有 API

做了什么:代码拼接并解析私有门户视图类。代码以本地 UIKitPortalViewProtocol 访问已知属性。代码把门户设为不接收触摸,并按需要同步源视图的位置和变换。

混淆还原:数组拼接结果是 _UIPortalView。代码还通过同样的解析确认一个视图是否为该类,并读取其 sourceView。

为什么这样做:公开 UIKit 没有对应的源视图门户视图。最小协议避免直接把私有头文件引入编译期。类缺失时调用者得到 nil。

关键代码摘录

UIKitUtils.m · L211–L257
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;
}
return view;

代价与风险:属性和类都未公开。类存在不等于全部属性仍兼容。存在审核和版本兼容风险。

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

13. 检验私有方法的完整类型编码后才替换 IMP

#01-13Telegram-iOSUIKit报告精选 01-6源码分析私有 API运行时 hackCore Animation

做了什么:代码仅在 iOS 26 及以上注册。代码解析 UIKit Swift mangled 类名,再找 backdropLayer:didChangeLuma:。代码要求完整编码正好是 v32@0:8@16d24 后,才保存原 IMP 并替换。

混淆还原:TGEncodeText 的键为 0,所以它没有改变字符串。真实类名是 _TtC5UIKitP33_ACD4A08F4BE9D00246F2A9C24A80CA8817UISDFBackdropView。拼接的 selector 是 backdropLayer:didChangeLuma:。

为什么这样做:私有 Swift 类名和签名都可能变化。仅以 selector 存在就强转 IMP 可能破坏寄存器传参。完整编码检查减少了 ABI 不匹配风险。

关键代码摘录

UIViewController+Navigation.m · L378–L420
static void registerEffectViewOverrides(void) {
    NSMutableArray<NSString *> *nameList = [[NSMutableArray alloc] init];
    [nameList addObject:TGEncodeText(@"_TtC5UIKitP33_ACD4A08F4BE9D00246F2A9C24A80CA8817UISDFBackdropView", 0)];
    NSString *selectorString = [@"backdropLayer" stringByAppendingString:@":didChangeLuma:"];
    for (NSString *name in nameList) {
        Class classValue = NSClassFromString(name);
        if (classValue == nil) {
            continue;
        }
        Method method = (Method)[RuntimeUtils getMethodOfClass:classValue
                                      selector:NSSelectorFromString(selectorString)];
        if (method) {
            const char *typeEncoding = method_getTypeEncoding(method);
            if (strcmp(typeEncoding, "v32@0:8@16d24") == 0) {
                original_backdropLayerDidChangeLuma =
                    (id (*)(id, SEL, CALayer *, double))method_getImplementation(method);
                [RuntimeUtils replaceMethodImplementationOfClass:classValue
                    selector:NSSelectorFromString(selectorString)
                    replacement:(IMP)&replacement_backdropLayerDidChangeLuma];
            }
        }
        break;
    }
}

代价与风险:这是私有 UIKit 实现,并永久覆盖原 IMP。方法实现仍可能在类型编码不变时改变语义。液态玻璃视觉细节不在本笔记范围。

证据链 · #01-13iOS 未做二进制验证
源码分析
已在 Telegram-iOS 固定提交中逐行确认,共 1 处源码位置。
报告摘要
怎么做到:method_getTypeEncoding 比较 v32@0:8@16d24 后才保存原 IMP 与替换。
二进制验证
未做。App Store 版 IPA 受 FairPlay 加密,无法合法取得解密的可执行文件。

14. 通过私有 selector 让悬浮窗口既不改状态栏,也不夺取键盘焦点

#01-14Telegram-iOSUIKit源码分析私有 API文本与输入

做了什么:AboveStatusBarWindow 处于状态栏之上。代码在类初始化时,给它动态添加两个私有 selector。两个 selector 都复用返回 false 的公开命名实现。

混淆还原:第一个 selector 是 _canAffectStatusBarAppearance。第二个 selector 是 _canBecomeKeyWindow。源码分别由数组拼接和前缀拼接得到。

为什么这样做:仅重写公开 canBecomeKeyWindow 不保证 UIKit 会查询它的私有变体。动态补齐私有 selector 让覆盖窗口可点击,同时把其他点击穿透,并避免影响全局状态栏或成为 key window。

关键代码摘录

UIViewController+Navigation.m · L890–L941
- (BOOL)shouldAffectStatusBarAppearance {
    return false;
}

- (BOOL)canBecomeKeyWindow {
    return false;
}

+ (void)initialize {
    NSString *canAffectSelectorString = [@[@"_can", @"Affect", @"Status", @"Bar", @"Appearance"] componentsJoinedByString:@""];
    SEL canAffectSelector = NSSelectorFromString(canAffectSelectorString);
    Method shouldAffectMethod = class_getInstanceMethod(self, @selector(shouldAffectStatusBarAppearance));
    IMP canAffectImplementation = method_getImplementation(shouldAffectMethod);
    class_addMethod(self, canAffectSelector, canAffectImplementation,
                    method_getTypeEncoding(shouldAffectMethod));
    NSString *canBecomeKeySelectorString = [NSString stringWithFormat:@"_%@", NSStringFromSelector(@selector(canBecomeKeyWindow))];
    SEL canBecomeKeySelector = NSSelectorFromString(canBecomeKeySelectorString);
    Method canBecomeKeyMethod = class_getInstanceMethod(self, @selector(canBecomeKeyWindow));
    IMP canBecomeKeyImplementation = method_getImplementation(canBecomeKeyMethod);
    class_addMethod(self, canBecomeKeySelector, canBecomeKeyImplementation,
                    method_getTypeEncoding(canBecomeKeyMethod));
}

代价与风险:私有 selector 存在审核风险。系统若改为其他判定接口,窗口行为会变化。

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

15. Debug 录制器直接替换 UIKit 基类 IMP,采集原始滚动物理数据

#01-15Telegram-iOSUIKit源码分析私有 API运行时 hack手势与滚动

做了什么:调试专用录制器把 UIPanGestureRecognizer 的四个触摸入口替换为 block IMP。代码先执行原实现,再记录已经更新的速度和状态。代码还 hook setContentOffset:,以及存在时的私有橡皮筋计算器。

为什么这样做:采样计时器会丢失 UIKit 实际写入的每个 offset。先后顺序使触摸数据与 UIKit 刚算出的识别器状态配对。实例身份过滤让全局 hook 只记录附着的一个滚动视图。

关键代码摘录

UIScrollViewPhysicsSwizzler.swift · L75–L133
for (name, phase) in phases {
    let sel = NSSelectorFromString(name)
    guard let method = class_getInstanceMethod(UIPanGestureRecognizer.self, sel) else { continue }
    typealias Fn = @convention(c) (AnyObject, Selector, AnyObject, AnyObject) -> Void
    let original = unsafeBitCast(method_getImplementation(method), to: Fn.self)
    let block: @convention(block) (AnyObject, AnyObject, AnyObject) -> Void = { obj, touches, event in
        original(obj, sel, touches, event)
        let t = (event as? UIEvent)?.timestamp ?? CACurrentMediaTime()
        CaptureSink.current?.recordTouch(phase: phase, from: obj, eventTime: t)
    }
    method_setImplementation(method, imp_implementationWithBlock(block))
}
// ...
let sel = NSSelectorFromString("_rubberBandOffsetForOffset:maxOffset:minOffset:range:outside:")
guard let method = class_getInstanceMethod(UIScrollView.self, sel) else { return }
typealias Fn = @convention(c) (AnyObject, Selector, CGFloat, CGFloat, CGFloat, CGFloat,
                              UnsafeMutablePointer<ObjCBool>?) -> CGFloat
let original = unsafeBitCast(method_getImplementation(method), to: Fn.self)
// 录制原结果后,block 返回 out。
method_setImplementation(method, imp_implementationWithBlock(block))

代价与风险:源码明确标为 debug-only,且不应出货。method_setImplementation 不可逆。私有 _rubberBandOffsetForOffset:maxOffset:minOffset:range:outside: 有审核风险。

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

16. 关闭 UITextView 分块渲染,并识别 iOS 16 私有自动更正提示

#01-16Telegram-iOSUIKit源码分析私有 API文本与输入手势与滚动性能与内存

做了什么:构造输入框时,调用存在才执行的私有 _disableTiledViews。iOS 17 前,代码加一个不延迟也不取消触摸的 tap recognizer。它会把 @"UI" + @"AutocorrectInlinePrompt" 解析为 UIAutocorrectInlinePrompt,命中后执行清理回调。

为什么这样做:分块文本渲染会妨碍需要完整内容的特定输入场景。私有自动更正浮层不是业务代码可直接控制的对象。透明 tap recognizer 不拦截原始 UITextView 触摸流。

关键代码摘录

ChatInputTextViewImpl.m · L23–L70
if (disableTiling) {
    SEL selector = NSSelectorFromString(@"_disableTiledViews");
    if (selector && [self respondsToSelector:selector]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        [self performSelector:selector];
#pragma clang diagnostic pop
    }
}
if (@available(iOS 17.0, *)) {
} else {
    _tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self
        action:@selector(workaroundTapGesture:)];
    _tapRecognizer.cancelsTouchesInView = false;
    _tapRecognizer.delaysTouchesBegan = false;
    _tapRecognizer.delaysTouchesEnded = false;
    _tapRecognizer.delegate = self;
    [self addGestureRecognizer:_tapRecognizer];
}

代价与风险:_disableTiledViews 与 UIAutocorrectInlinePrompt 都是私有实现。禁用分块渲染可能增大长文本内存和绘制成本。手势竞争仍可能随 iOS 改变。

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

17. 旧系统用 KVC 强制设备方向,新系统改走场景几何更新

做了什么:代码将方向策略切为仅竖屏时,在 iOS 16 以上请求公开的 UIWindowScene.GeometryPreferences。旧系统则向 UIDevice 的 KVC 键 orientation 写入竖屏 raw value。

为什么这样做:旧 UIKit 没有场景几何更新 API。只调用 attemptRotationToDeviceOrientation 无法把物理方向强制改为竖屏。新版路径避免了旧 KVC hack。

关键代码摘录

NativeWindowHostView.swift · L103–L123
var orientations: UIInterfaceOrientationMask = defaultOrientations {
    didSet {
        if oldValue != self.orientations {
            if self.orientations == .portrait {
                if #available(iOSApplicationExtension 16.0, iOS 16.0, *) {
                    let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene
                    windowScene?.requestGeometryUpdate(.iOS(interfaceOrientations: .portrait))
                    self.setNeedsUpdateOfSupportedInterfaceOrientations()
                } else if UIDevice.current.orientation != .portrait {
                    let value = UIInterfaceOrientation.portrait.rawValue
                    UIDevice.current.setValue(value, forKey: "orientation")
                }
            } else {
                if #available(iOSApplicationExtension 16.0, iOS 16.0, *) {
                    self.setNeedsUpdateOfSupportedInterfaceOrientations()
                } else {
                    UIViewController.attemptRotationToDeviceOrientation()
                }
            }
        }
    }
}

代价与风险:UIDevice.orientation 的 KVC 写入未公开,存在审核风险。代码选择 connectedScenes.first,多窗口时未必是期望场景。旧路径的系统行为未验证。

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

18. 重写私有 UIWindow 方向回调,精确包住布局中的旋转状态

#01-18Telegram-iOSUIKit源码分析私有 API运行时 hack

做了什么:NativeWindow 覆盖私有 _update(toInterfaceOrientation:duration:force:)。代码在 super 前后切换布局中标志。代码在结束后把转换后的方向回传给自定义窗口宿主。

为什么这样做:普通 layoutSubviews 缺少“当前布局由方向切换触发”的边界。围绕 UIKit 私有回调包住标志,可以让上层避免在转场中错误触发自己的布局逻辑。

关键代码摘录

NativeWindowHostView.swift · L318–L339
override func layoutSubviews() {
    super.layoutSubviews()
    self.layoutSubviewsEvent?()
}

override func motionEnded(_ motion: UIEvent.EventSubtype, with event: UIEvent?) {
    super.motionEnded(motion, with: event)
    if motion == .motionShake {
        self.motionShakeImpl?()
    }
}

override func _update(toInterfaceOrientation arg1: Int32,
                      duration arg2: Double, force arg3: Bool) {
    self.updateIsUpdatingOrientationLayout?(true)
    super._update(toInterfaceOrientation: arg1, duration: arg2, force: arg3)
    self.updateIsUpdatingOrientationLayout?(false)
    let orientation = UIInterfaceOrientation(rawValue: Int(arg1)) ?? .unknown
    self.updateToInterfaceOrientation?(orientation)
}

代价与风险:私有覆盖对 SDK 和运行时签名高度敏感。方法签名或调用时机改变可能崩溃。存在审核风险。

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

19. 对运行时类表做哈希索引,避免私有类名进入二进制

#01-19Telegram-iOSUIKit源码分析私有 API运行时 hack

做了什么:Freedom 枚举全部 Objective-C 类。代码用带固定 seed 的 MurmurHash3 把类名映射到 uint32_t。调用点只保留哈希常量,再从映射获得运行时类。

为什么这样做:这比 NSClassFromString(@"私有类名") 更难把符号明文留在二进制。查找表只创建一次,后续查询是映射查询。

关键代码摘录

Freedom.mm · L124–L155
Class freedomClass(uint32_t name)
{
    static std::map<uint32_t, __unsafe_unretained Class> classMap;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^
    {
        int classCount = freedom_getClassList(NULL, 0);
        if (classCount > 0)
        {
            __unsafe_unretained Class *classList = (Class *)malloc(classCount * sizeof(Class));
            objc_getClassList(classList, classCount);
            for (int i = 0; i < classCount; i++) {
                const char *className = freedom_class_getName(classList[i]);
                uint32_t hashName = (uint32_t)murMurHashBytes32((void *)className,
                                                                (int)strlen(className));
                classMap[hashName] = classList[i];
            }
            free(classList);
        }
        freedom_initialized = true;
    });
    auto it = classMap.find(name);
    if (it != classMap.end()) return it->second;
    return nil;
}

代价与风险:哈希碰撞会选错类。系统类名变化会使查找失败。FreedomUIKit.m 中部分哈希对应的真实私有类名无法仅由本次源码确认,因此未验证。该混淆不能消除私有 API 的审核风险。

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

20. 自动生成装饰子类,再从 alloc 时替换每个实例的 ISA

做了什么:代码动态创建 Decorated<hash> 子类。代码从另一个类复制指定方法到装饰类。代码把原类元类的 alloc 替换为 freedomAllocImpl,所以每个新实例都会在分配后被 object_setClass 成装饰类。

为什么这样做:它不需改写目标类或全局交换同名方法。adjustDecoratedClass 还会为继承层次生成适配装饰类。这样可给未知私有类的实例附加少量覆盖。

关键代码摘录

Freedom.mm · L469–L524 等 2 处
id freedomAllocImpl(id self, SEL _cmd)
{
    static id (*nativeImp)(id, SEL) = NULL;
    dispatch_once(&onceToken, ^
    {
        nativeImp = (id (*)(id, SEL))class_getMethodImplementation(
            object_getClass([NSObject class]), @selector(alloc));
    });
    id result = nativeImp(self, _cmd);
    Class decoratedClass = adjustDecoratedClass(self);
    if (decoratedClass != Nil)
        object_setClass(result, decoratedClass);
    return result;
}
// ...
Class decoratedClass = objc_allocateClassPair(className,
    [[NSString alloc] initWithFormat:@"Decorated%" PRIx32 "", name].UTF8String, 0);
Method *methodList = class_copyMethodList(className, &methodCount);

代价与风险:这是高风险 ISA-swizzling。alloc 的所有权和类簇假设都可能被破坏。动态类名、哈希 selector 和私有对象布局都提高维护成本。

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

21. 直接改写系统返回手势识别器的 ISA,增加全屏导航手势

#01-21Telegram-iOSUIKit源码分析私有 API运行时 hack手势与滚动

做了什么:代码把 UIKit 创建的 interactivePopGestureRecognizer 改成自定义 TGNavigationPanGestureRecognizer。代码保留系统原 delegate,并以代理注入自定义开始和同时识别判断。遇到 QLPreviewController 时,代码又暂时改回 UIScreenEdgePanGestureRecognizer。

为什么这样做:系统返回手势的类与边缘限制通常不可替换。ISA 改写让既有手势对象保留 UIKit 已配置的 target 和状态,同时拥有自定义 pan 行为。

关键代码摘录

TGNavigationController.m · L129–L182
if (iosMajorVersion() >= 8 && !TGIsRTL())
{
    object_setClass(self.interactivePopGestureRecognizer,
                    [TGNavigationPanGestureRecognizer class]);
    self.interactivePopGestureRecognizer.delaysTouchesBegan = false;
    self.interactivePopGestureRecognizer.delaysTouchesEnded = true;
    self.interactivePopGestureRecognizer.cancelsTouchesInView = true;

    _gestureDelegate = [[TGNavigationGestureDelegateProxy alloc] init];
    _gestureDelegate.delegate = self.interactivePopGestureRecognizer.delegate;

    __weak TGNavigationController *weakSelf = self;
    _gestureDelegate.shouldBegin = ^bool(TGNavigationPanGestureRecognizer *gestureRecognizer)
    {
        __strong TGNavigationController *strongSelf = weakSelf;
        if (strongSelf != nil)
        {
            bool shouldBegin = [strongSelf _gestureRecognizerShouldBegin:gestureRecognizer];

代价与风险:对象真实布局必须与替换类兼容。系统私有手势子类变化会崩溃。该 legacy 路径还有 RTL 与 iOS 版本条件,维护成本高。

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

22. 把 NSNotificationCenter 的同步投递改造成可截获的中间件

#01-22Telegram-iOSUIKit源码分析私有 API运行时 hack性能与内存

做了什么:代码全局交换通知中心的同步投递方法。主线程处理器可先做工作、调用 passNotification 决定投递时机,或返回 true 完全吞掉投递。方向处理模块用它在旋转通知前后维护状态。

为什么这样做:普通 observer 只能在通知已投递时被调用。该中间件可改变原通知与自定义操作的先后顺序,且可不改变非主线程投递。

关键代码摘录

NotificationCenterUtils.m · L19–L68
- (void)_a65afc19_postNotificationName:(NSString *)aName
                                object:(id)anObject
                              userInfo:(NSDictionary *)aUserInfo
{
    if ([NSThread isMainThread]) {
        for (NotificationHandlerBlock handler in notificationHandlers())
        {
            if (handler(aName, anObject, aUserInfo, ^{
                [self _a65afc19_postNotificationName:aName object:anObject userInfo:aUserInfo];
            })) {
                return;
            }
        }
    }
    [self _a65afc19_postNotificationName:aName object:anObject userInfo:aUserInfo];
}
// ...
[RuntimeUtils swizzleInstanceMethodOfClass:[NSNotificationCenter class]
    currentSelector:@selector(postNotificationName:object:userInfo:)
    newSelector:@selector(_a65afc19_postNotificationName:object:userInfo:)];

代价与风险:它影响进程内每个主线程同步通知。重入、处理器顺序和异常都会影响全局行为。swizzle 名 _a65afc19_ 是工程命名,不是私有 API 混淆。

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

23. 把截图重复通知去重状态放在每个 observer,避免覆盖层漏报

做了什么:代码以每个 ScreenshotNotificationObserver 自己的最后时间戳去重。相隔少于 0.5 秒的相同截图通知会被丢弃。

为什么这样做:源码注释记录 UIKit 曾为同一次截图连续投递两次。共享全局时间戳会让聊天和覆盖其上的媒体画廊互相吞掉合法通知。实例级状态同时解决重复与多观察者问题。

关键代码摘录

ScreenCaptureDetection.swift · L53–L88
private let screenshotRepeatInterval: CFAbsoluteTime = 0.5
private final class ScreenshotNotificationObserver {
    private var observer: NSObjectProtocol?
    private var lastTimestamp: CFAbsoluteTime?

    init(_ f: @escaping () -> Void) {
        self.observer = NotificationCenter.default.addObserver(
            forName: UIApplication.userDidTakeScreenshotNotification,
            object: nil, queue: .main, using: { [weak self] _ in
                guard let self else { return }
                let timestamp = CFAbsoluteTimeGetCurrent()
                if let lastTimestamp = self.lastTimestamp,
                   timestamp - lastTimestamp < screenshotRepeatInterval {
                    return
                }
                self.lastTimestamp = timestamp
                f()
            })
    }
}

代价与风险:0.5 秒内的两次真实截图会被合并。该路径只影响事件报告,不会阻止截图。无明显私有 API 风险。

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

24. 只把 CAFilter 当作运行时工厂,不引入私有头文件

#01-24Telegram-iOSUIKit源码分析私有 API运行时 hackCore Animation

做了什么:代码以本地 GraphicsFilterProtocol 描述唯一需要的类方法。代码用 NSClassFromString(@"CAFilter") 解析私有 Core Animation 类。各工厂直接按滤镜名调用 filterWithName:。

为什么这样做:最小协议让编译器无需私有 SDK 头文件。类解析失败时 Objective-C 给 nil 发消息,工厂返回空值。调用方可选择不安装滤镜。

关键代码摘录

UIKitUtils.m · L259–L295
@protocol GraphicsFilterProtocol <NSObject>
- (NSObject * _Nullable)filterWithName:(NSString * _Nonnull)name;
@end

NSObject * _Nullable makeBlurFilter() {
    return [(id<GraphicsFilterProtocol>)NSClassFromString(@"CAFilter")
        filterWithName:@"gaussianBlur"];
}

NSObject * _Nullable makeVariableBlurFilter() {
    return [(id<GraphicsFilterProtocol>)NSClassFromString(@"CAFilter")
        filterWithName:@"variableBlur"];
}

NSObject * _Nullable makeLuminanceToAlphaFilter() {
    return [(id<GraphicsFilterProtocol>)NSClassFromString(@"CAFilter")
        filterWithName:@"luminanceToAlpha"];
}

代价与风险:CAFilter 和滤镜名均为私有接口。存在审核和系统兼容风险。本条只记录私有 API 的获取方式,不分析视觉效果细节。

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

本页目录

1. 借安全文本输入层给任意图层打上截图排除标记2. 用 XOR 字节串隐藏私有拼写控制器,并用精确 ABI 调用它3. 伪造 Info.plist 结果,让 App-Bound 域名可以动态增删4. 从私有键盘场景代理取得键盘窗口,并避开新版陷阱5. 用私有 cornerRadii 获得可动画的四角半径,避免离屏遮罩6. 交换继承方法前先落地到目标类,避免污染父类7. 把 UIMenuItem 的文字渲染偷换成图标渲染8. 从私有 _scrollHeartbeat 抓显示链接,强制旧系统滚动使用满帧率9. 先读私有方法签名,再选择 float 或 double 的 IMP 调用约定10. 给弹簧动画写入私有高帧率原因码11. 用 NSInvocation 安全探测并配置私有模糊效果12. 将私有 _UIPortalView 包装为最小协议,并关闭其命中测试转发13. 检验私有方法的完整类型编码后才替换 IMP14. 通过私有 selector 让悬浮窗口既不改状态栏,也不夺取键盘焦点15. Debug 录制器直接替换 UIKit 基类 IMP,采集原始滚动物理数据16. 关闭 UITextView 分块渲染,并识别 iOS 16 私有自动更正提示17. 旧系统用 KVC 强制设备方向,新系统改走场景几何更新18. 重写私有 UIWindow 方向回调,精确包住布局中的旋转状态19. 对运行时类表做哈希索引,避免私有类名进入二进制20. 自动生成装饰子类,再从 alloc 时替换每个实例的 ISA21. 直接改写系统返回手势识别器的 ISA,增加全屏导航手势22. 把 NSNotificationCenter 的同步投递改造成可截获的中间件23. 把截图重复通知去重状态放在每个 observer,避免覆盖层漏报24. 只把 CAFilter 当作运行时工厂,不引入私有头文件