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

iOS16及以上NSAttributedString解析HTML在UITextView崩溃求助

问题:iOS16+下NSAttributedString解析HTML赋值给UITextView崩溃

崩溃信息

Assertion failed: (forceBreakIndex > NSMaxRange(lastResortNode->lineBreak.range)), function -[_NSOptimalLineBreaker _calculateOptimalWrapping], file NSOptimalLineBreaker.m, line 1706

问题背景

该崩溃仅在iOS16及以上版本触发,iOS15及以下运行正常;部分HTML内容可正常解析,部分会触发崩溃(无法提供崩溃的HTML内容)。用户使用的解析代码如下:

func setHTMLContent(_ htmlContent: String, font: UIFont, textColor: UIColor) {
    guard let attributedString = try? NSMutableAttributedString(data: htmlContent.data(using: .utf8)!, options: [.documentType: NSAttributedString.DocumentType.html], documentAttributes: nil) else { return }
        
    attributedString.addAttributes([.font: font, .foregroundColor: textColor], range: NSRange(location: 0, length: attributedString.length))
        self.attributedText = attributedString
}

// 调用方式
SomeTextView.setHTMLContent("Some HTML content", font: UIFont.systemFont(ofSize: 14), textColor: .white)

原因分析

  1. iOS16对NSAttributedString的HTML解析引擎做了底层重构,尤其是文本换行、布局计算逻辑(对应崩溃类_NSOptimalLineBreaker),旧版本兼容的异常HTML结构(如空标签、嵌套格式错误、异常换行节点)在新引擎下会触发断言失败。
  2. 全局调用addAttributes覆盖全范围属性,会强制替换HTML解析生成的原有排版属性(如段落样式、行高),导致布局计算时的索引逻辑冲突,触发断言。

解决方案

1. 预处理HTML内容

过滤或修复可能触发问题的HTML结构,减少异常节点:

func sanitizeHTML(_ html: String) -> String {
    var cleanedHTML = html
    // 移除空的p/div标签
    cleanedHTML = cleanedHTML.replacingOccurrences(of: "<p>\\s*</p>", with: "", options: .regularExpression)
    cleanedHTML = cleanedHTML.replacingOccurrences(of: "<div>\\s*</div>", with: "", options: .regularExpression)
    // 合并连续换行标签
    cleanedHTML = cleanedHTML.replacingOccurrences(of: "<br\\s*/?>\\s*<br\\s*/?>", with: "<br/>", options: .regularExpression)
    // 移除多余空白字符
    cleanedHTML = cleanedHTML.replacingOccurrences(of: "\\s{2,}", with: " ", options: .regularExpression)
    return cleanedHTML
}

在解析前调用该方法处理HTML内容。

2. 避免全局覆盖属性,改用属性合并

遍历每个字符的属性范围,合并自定义字体和颜色,保留原有排版属性:

func setHTMLContent(_ htmlContent: String, font: UIFont, textColor: UIColor) {
    let cleanedHTML = sanitizeHTML(htmlContent)
    guard let data = cleanedHTML.data(using: .utf8),
          let attributedString = try? NSMutableAttributedString(data: data, options: [.documentType: NSAttributedString.DocumentType.html], documentAttributes: nil) else {
        // 解析失败降级为普通文本
        self.text = htmlContent
        self.font = font
        self.textColor = textColor
        return
    }
    
    // 遍历属性范围,合并自定义样式
    attributedString.enumerateAttributes(in: NSRange(location: 0, length: attributedString.length), options: []) { attrs, range, _ in
        var mergedAttrs = attrs
        mergedAttrs[.font] = font
        mergedAttrs[.foregroundColor] = textColor
        attributedString.setAttributes(mergedAttrs, range: range)
    }
    
    self.attributedText = attributedString
}

3. 捕获断言异常做兜底

由于Swift默认不捕获Objective-C断言类异常,需借助Objective-C工具类捕获崩溃,降级为普通文本展示:

// ExceptionCatcher.h
#import <Foundation/Foundation.h>

NS_INLINE BOOL catchException(void(^tryBlock)(void)) {
    @try {
        tryBlock();
        return YES;
    } @catch (NSException *exception) {
        NSLog(@"Caught layout exception: %@", exception);
        return NO;
    }
}

在Swift中调用:

func setHTMLContent(_ htmlContent: String, font: UIFont, textColor: UIColor) {
    let cleanedHTML = sanitizeHTML(htmlContent)
    guard let data = cleanedHTML.data(using: .utf8),
          let attributedString = try? NSMutableAttributedString(data: data, options: [.documentType: NSAttributedString.DocumentType.html], documentAttributes: nil) else {
        self.text = htmlContent
        self.font = font
        self.textColor = textColor
        return
    }
    
    attributedString.enumerateAttributes(in: NSRange(location: 0, length: attributedString.length), options: []) { attrs, range, _ in
        var mergedAttrs = attrs
        mergedAttrs[.font] = font
        mergedAttrs[.foregroundColor] = textColor
        attributedString.setAttributes(mergedAttrs, range: range)
    }
    
    // 尝试赋值,捕获异常
    let success = catchException {
        self.attributedText = attributedString
    }
    
    if !success {
        // 崩溃降级
        self.text = htmlContent
        self.font = font
        self.textColor = textColor
    }
}

4. 替换为第三方HTML解析库

使用稳定的第三方库替代系统API,比如DTCoreText,这类库独立于系统解析引擎,版本兼容性更好。

内容的提问来源于stack exchange,提问作者Aditya Ramadhan

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 15:12:02