iOS Swift UITextView加载含长文本td的HTML转富文本崩溃问题
- 实现逻辑:读取大体积HTML文件为
String类型,做适配调整后转换为NSAttributedString/NSMutableAttributedString,完成富文本拼接、修改操作后,将结果展示在UITextView控件中。 - 异常表现:多数HTML文件可正常加载展示,部分体积更大的文件会触发崩溃,报错信息如下:
Thread 1: "*** -[NSBigMutableString getCharacters:range:]: Range {7825, 1} out of bounds; string length 7811"
- 复现规律:打印生成的最终富文本无异常,可正常输出预期内容;仅在执行
self.textView.attributedText = attributedText赋值操作时触发崩溃;删除大文件中部分内容后代码可正常运行。 - 核心复现代码:
let htmlText = try? String(contentsOfFile: filePath, encoding: String.Encoding.utf8) ... htmlText.html(fontSize: fontSizeBody, textAlign: .natural, mainTextColor: Theme.Color.darkLight) ... // 若干NSMutableAttributedString拼接、修改操作 ... let attributedText = NSAttributedString(attributedString: mutableAttributedText) print(attributedText) // 此处无异常,可正常打印预期富文本内容 self.textView.attributedText = attributedText // 仅大文件在此处触发崩溃
- 定位结果:触发崩溃的HTML片段为
table标签下某td标签内包含极长无分隔连续字符串,示例结构如下:
<table> <tr> <td> adsfsdfadsf: </td> <td> hkavdhsghasgdvfhjgasvdfhgasdvhjfgvashjgdfvjhagsvdfhjavsdhjfvajhsdgfhjagsvfjhagsvjdhfvajhsdgfvjhasgvdfjhgahkavdhsghasgdvfhjgasvdfhgasdvhjfgvashjgdfvjhagsvdfhjavsdhjfvajhsdgfhjagsvfjhagsvjdhfvajhsdgfvjhasgvdfjhga </td> </tr> </table>
- 崩溃调用栈核心特征:崩溃发生在系统内部
NSLayoutManager的布局计算流程中,而非业务代码手动操作字符串的逻辑,最后业务代码调用点为-[UITextView setAttributedText:]。
该崩溃是iOS系统TextKit 1框架的已知遗留bug,在iOS 12~iOS 16版本均有概率复现:
当富文本中存在嵌套在table结构内、无任何空白/分隔符的超长连续字符串时,系统布局引擎在计算文本断行、填充字形布局空洞的过程中,会错误计算字符偏移量,尝试访问超出字符串实际长度的范围,最终触发越界异常。
由于崩溃发生在控件内部布局阶段,因此生成的富文本本身完全正常,仅在赋值给UITextView触发内部排版时才会崩溃,和业务代码手动操作富文本的逻辑无关。
按改造成本从低到高,可选择以下方案:
方案1:预处理HTML,为超长连续字符串插入零宽断行标记(推荐)
在HTML转富文本之前,先对原始HTML字符串做预处理:匹配长度超过50个字符的连续无空白字符片段,每隔20个字符插入一个零宽空格\u{200B}。
零宽空格属于不可见字符,不会在页面上展示额外空白,也不会影响文本选择、复制等交互,用户完全无感知,但可以给系统布局引擎提供合法的断行位置,从根源上避免布局计算的偏移错误。
参考处理逻辑:// 匹配连续50个以上非空白字符的长片段 let pattern = "([^\\s]{50,})" guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else { return htmlText } // 对匹配到的长片段,每隔20个字符插入零宽空格 func processLongText(_ raw: String) -> String { var res = "" let chars = Array(raw) for idx in 0..<chars.count { if idx != 0 && idx % 20 == 0 { res.append("\u{200B}") } res.append(chars[idx]) } return res }该处理不会破坏HTML标签结构:HTML标签的属性、标签名本身不会出现这么长的连续无空白字符,即使匹配到插入零宽空格也不会影响HTML解析逻辑。
方案2:调整UITextView配置,规避异常布局触发条件
若不想修改原始HTML内容,可通过调整控件配置绕过bug:- 关闭非连续布局:将
textView.layoutManager.allowsNonContiguousLayout设为false,强制布局管理器对全量文本做完整布局计算,避免局部填充字形空洞时出现范围计算错误 - 赋值前重置选中范围:在给
attributedText赋值前,先将selectedRange设为NSMakeRange(0, 0),避免赋值时系统自动滚动光标位置触发异常布局计算
参考代码:
// 赋值前配置 textView.layoutManager.allowsNonContiguousLayout = false textView.selectedRange = NSRange(location: 0, length: 0) textView.attributedText = attributedText注意:关闭非连续布局后,超大文本首次加载速度会略有下降,因为需要全量计算所有文本的布局信息,但可以解决90%以上的同类越界崩溃。
- 关闭非连续布局:将
方案3:替换渲染组件(适用于超大HTML场景)
如果需要加载的HTML文件普遍体积较大(单文件超过1MB)、结构复杂,原生UITextView的HTML渲染本身存在内存占用高、解析慢、兼容性差的问题,可直接换用WKWebView加载本地HTML,稳定性和渲染兼容性远优于UITextView;也可以使用第三方富文本解析库提前完成HTML转义,绕过系统原生HTML转富文本的已知bug。
- 大体积HTML转
NSAttributedString的操作非常耗时,不要放在主线程执行,建议放到后台队列处理,完成后再回到主线程赋值给控件,避免卡主线程触发watchdog崩溃。 - 尽量不要大范围修改table生成的富文本段落的
paragraphStyle属性,这类操作很容易破坏TextKit内部的布局状态,触发其他类型的崩溃。
内容的提问来源于stack exchange,提问作者sw1ft3r

