在iOS26下,发现coretext相关的崩溃率激增,比如苹果开发者网站反馈的这个案例 developer.apple.com/forums/thre…
经过研究,这个原因主要是含有大量零宽字符、组合记号的字符,在系统调用boundingRectWithSize时异常导致的,需要过滤掉字符中,大量重复的控制字符、孤立字符、零宽字符、组合记号可以解决这个问题。
适配范围
- 重点防护 iOS 26 上已知 CoreText 崩溃路径(但其他系统版本也具备兜底意义)
快速开始
1) 集成代码
将以下文件加入工程(同一 Target):
JKRCoreTextSafety.h/.m(或JKRTextSafetyShared.h+ 实现文件)NSString+JKRTextBoundingSafetyGlobal.m(可选:全局 hookboundingRect)- 确保
jkr_isSafeString访问器只在一个实现文件中定义(避免类别重复实现带来的运行时冲突)。
2) 初始化(可选,全局 hook)
只在iOS26系统下,才hook防止崩溃,待iOS26修复后,回退。
// App 启动时,默认hook全局UILabel setText、setAttributedText,自动清洗
JKRInstallTextBoundingSafety();
// App 启动时,默认hook boundingRectWithSize,计算bound前自动清洗
JKRInstallUILabelTextSafety();
作用:为
NSString/NSAttributedString的boundingRect...注入“尺寸钳制 + 文本/属性清洗”。
3) 直接调用(按需)
#import "JKRCoreTextSafety.h"
// 纯文本
JKRTextSanitizeStat st = {0};
NSString *safe = JKRSanitizePlainString(rawString, &st);
BOOL ok = jkr_isSafe(st); // 是否命中异常(false 表示发生清洗/修复)
// 富文本
NSAttributedString *san = JKRSanitizeAttributedString(attr, &st);
// 仅修属性(不改文字)
BOOL fontFix,kernFix,baseFix,colorFix,strokeFix,paraFix = NO;
NSDictionary *attrs2 = jkr_localFixAttributes(attrs, &fontFix, &kernFix, &baseFix, &colorFix, &strokeFix, ¶Fix);
// 测量保护
CGRect r = [safe boundingRectWithSize:JKRFixMeasureSize(size)
options:opts
attributes:attrs2
context:NULL];
API 说明
纯文本
NSString * _Nullable JKRSanitizePlainString(NSString * _Nullable s, JKRTextSanitizeStat * _Nullable st);
NSString * JKRSanitizePlainStringEasy(NSString * _Nullable s);
- 删除 控制字符(Cc,保留
\n/\t),孤立代理项 →U+FFFD - 零宽字符 限额(默认 16,超出丢弃)
- BiDi 控制符 限额(默认 8,且同一 run 折叠为一个)
- 组合记号(Mn/Me)每簇上限(默认 8),并做脚本相容性过滤
- 簇首为记号(无 base)时,优先尝试回粘到前一阿拉伯 base/Tatweel
富文本
NSAttributedString * _Nullable JKRSanitizeAttributedString(NSAttributedString * _Nullable attr, JKRTextSanitizeStat * _Nullable st);
NSAttributedString * JKRSanitizeAttributedStringEasy(NSAttributedString * _Nullable attr);
- 基于文本清洗后的索引映射重放原属性
- 属性值异常将被钳制/清理(见下)
属性修复
NSDictionary<NSAttributedStringKey,id> *
jkr_localFixAttributes(NSDictionary *attrs,
BOOL *fontFix, BOOL *kernFix, BOOL *baseFix,
BOOL *colorFix, BOOL *strokeWidthFix, BOOL *paraFix);
- 字体:
pointSize<=0 / NaN/Inf / >600→ 系统 14pt;CTFont 非法类型 → 替换 NSKern/kCTKern:[-50,50]区间钳制;非数移除- 基线偏移:
[-200,200];非数移除 - 颜色:类型不匹配直接移除(防 CT 路径触发)
- 描边:
[-10,10];非数移除 - 段落:
lineSpacing/lineHeightMultiple/hyphenationFactor/...的NaN/Inf/矛盾值归零/矫正
安全判断与尺寸钳制
BOOL jkr_isSafe(JKRTextSanitizeStat st);
CGSize JKRFixMeasureSize(CGSize sz);
jkr_isSafe:若任何清洗/修复发生(或长度变化等),返回NO,便于上报与观测。JKRFixMeasureSize:将NaN/Inf/<=0/超大值钳到安全上限(默认 100000)。
安全标记
@interface NSString (JKRSafeCheck)
@property (nonatomic) BOOL jkr_isSafeString;
@end
@interface NSAttributedString (JKRSafeCheck)
@property (nonatomic) BOOL jkr_isSafeString;
@end
- 清洗返回的字符串已自动打
jkr_isSafeString = YES,Hook 会走轻量路径(仅修属性/尺寸钳制)。
配置项(默认)
static const NSUInteger kJKRMaxBidiTotal = 8; // 全文 BiDi 控制符上限(run 内折叠)
static const NSUInteger kJKRMaxCombiningPerCluster = 8; // 单簇组合记号上限(Mn/Me)
static const NSUInteger kJKRMaxZeroWidthTotal = 16; // 全文零宽字符上限
FOUNDATION_EXPORT const CGFloat JKRDefaultFontPt = 14.0;
可按业务调整,建议保守优先,先稳定再优化视觉。
日志与观测
在 JKRCoreTextSafety.h 顶部:
#define kJKROpenCTSLog 1
#if DEBUG && kJKROpenCTSLog
#define JKRTextSafetyLog(...) NSLog(__VA_ARGS__)
#else
#define JKRTextSafetyLog(...)
#endif
jkr_isSafe(st) == NO时会输出各类命中项,便于灰度/监控。- 线上建议采集
JKRTextSanitizeStat的计数与比例,做上游数据治理。
常见问题
1) 为什么要“脚本相容性过滤”?
iOS 26 的崩溃路径与跨脚本 Mn/Me 堆叠强相关(例如拉丁 base 上叠阿拉伯记号)。过滤后仍保留 VS/FE2x 等Common 记号,尽量不影响正常显示。
2) 这会“误伤”正常阿拉伯文字吗?
不会。阿拉伯记号仅允许叠到阿拉伯 base/Tatweel,这是阿拉伯正字法的预期。仅跨脚本滥用会被过滤或限流(同时有簇上限 8)。
3) 例子
原始:"🌹᭄ͥғᷢєͥяᷤ💍💘🎸"
处理:过滤掉跨脚本和超额的组合记号、折叠 LRM run,保留主要 base 与 emoji,避免崩溃。
4) Hook 会影响性能吗?
枚举 composed sequences + 轻量判定,实际开销可控;多数文本不命中时只做尺寸钳制与必要属性校验。对富文本大段渲染建议做异步测量缓存。
5) 重复定义 jkr_isSafeString 会怎样?
请确保访问器只实现一次。多个 Target/静态库重复实现可能导致类别冲突、行为不一致甚至崩溃。建议抽到单独 JKRSafeFlag.m,其他文件仅引头。
测试建议
-
构造 5 组样例:
- 纯拉丁 + 正常组合记号(上限内)
- 阿拉伯 base + 阿拉伯记号(上限内)
- 拉丁 base + 阿拉伯记号(应被过滤)
- 无 base 记号开头(应回粘/或
U+FFFD) - 超额零宽 / 超额 BiDi run
-
分别走:
boundingRectWithSize:、CTFramesetterCreateWithAttributedString+CTFramesetterSuggestFrameSizeWithConstraints,确保一致不崩。
目录结构(示例)
JKRFixiOS26CoreTextCrash/
├── TextSafety/
│ ├── JKRCoreTextSafety.h
│ ├── JKRCoreTextSafety.m
│ ├── NSString+JKRTextBoundingSafetyGlobal.h // 可选(全局 hook)
│ ├── NSString+JKRTextBoundingSafetyGlobal.m
│ ├── UILabel+JKRTextSafetyGlobal.h // 可选(全局 hook)
│ └── UILabel+JKRTextSafetyGlobal.m
└── TestDemo