You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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:

    1. 关闭非连续布局:将textView.layoutManager.allowsNonContiguousLayout设为false,强制布局管理器对全量文本做完整布局计算,避免局部填充字形空洞时出现范围计算错误
    2. 赋值前重置选中范围:在给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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 03:51:22