使用 NSAttributedString 为 UITextView 中的文本着色真的很慢

Coloring text in UITextView with NSAttributedString is really slow

我正在 UITextView 之上制作一个简单的代码查看器/编辑器,所以我想为一些关键字(变量、函数等)着色,以便于查看在 IDE 中。我正在使用 NSAttributedString 来执行此操作,并在循环中使用函数 apply(...) 在范围内着色(见下文)。然而,当有很多单词需要着色时,它开始变得 非常慢 并卡住键盘(在模拟器上不是那么多,但在实际设备上确实很慢)。我以为我可以使用线程来解决这个问题,但是当我 运行 DispatchQueue.global().async {...} 中的应用函数时,它根本不会着色。通常如果在主线程中有一些需要 运行 的 UI 调用,它会打印出错误/崩溃,这样我就可以找到添加 DispatchQueue.main.sync {...} 的位置,我已经尝试了各种方法地方,但它仍然不起作用。关于如何解决这个问题有什么建议吗?


调用更新

func textViewDidChange(_ textView: UITextView) {
    updateLineText()
}

更新函数

var wordToColor = [String:UIColor]()

func updateLineText() {

    var newText = NSMutableAttributedString(string: content)

    // some values are added to wordToColor here dynamically. This is quite fast and can be done asynchronously.

    // when this is run asynchronously it doesn't color at all...
    for word in wordToColor.keys {
        newText = apply(string: newText, word: word)
    }

    textView.attributedText = newText
}

应用函数

func apply (string: NSMutableAttributedString, word: String) -> NSMutableAttributedString {
    let range = (string.string as NSString).range(of: word)
    return apply(string: string, word: word, range: range, last: range)
}

func apply (string: NSMutableAttributedString, word: String, range: NSRange, last: NSRange) -> NSMutableAttributedString {
    if range.location != NSNotFound {

        if (rangeCheck(range: range)) {
            string.addAttribute(NSAttributedStringKey.foregroundColor, value: wordToColor[word], range: range)
            if (range.lowerBound != 0) {
                let index0 = content.index(content.startIndex, offsetBy: range.lowerBound-1)
                if (content[index0] == ".") {
                    string.addAttribute(NSAttributedStringKey.foregroundColor, value: UIColor.purple, range: range)
                }
            }

        }

        let start = last.location + last.length
        let end = string.string.count - start
        let stringRange = NSRange(location: start, length: end)
        let newRange = (string.string as NSString).range(of: word, options: [], range: stringRange)
        apply(string: string, word: word, range: newRange, last: range)
    }
    return string
}

这将更多的是一些分析和一些建议,而不是完整的代码实现。

您当前的代码完全重新扫描所有文本并重新应用用户在文本视图中键入的每个字符的所有属性。显然这是非常低效的。

一个可能的改进是实施 shouldChangeTextInRange 委托。然后您可以从现有的属性字符串开始,然后只处理正在更改的范围。您可能需要处理两边的一些文本,但这比重新处理整个文本要高效得多。

你或许可以将两者结合起来。如果当前文本小于某个合适的大小,则进行全扫描。一旦达到临界大小,就进行部分更新。

另一个考虑是在后台进行属性字符串的所有扫描和创建,但使其不可中断。每个文本更新您的取消和当前处理并重新开始。在用户停止输入足够长的时间以完成处理之前,不要使用新计算的属性文本实际更新文本视图。

但我会使用 Instruments 并分析代码。查看花费最多时间的内容。是找话吗?它是在创建属性字符串吗?是不是一直在设置textview的attributedText属性?

您也可以考虑深入研究 Core Text。也许 UITextView 不太适合您的任务。

我有一个记录器功能,我可以在其中记录我进行的所有服务调用,并可以在该日志中搜索特定字符串。我在文本字段中显示文本并在搜索时突出显示文本。我将下面的 func 与 Regex 一起使用,它并不慢。希望对你有帮助。

    func searchText(searchString: String) {
    guard let baseString = loggerTextView.text else {
        return
    }
    let attributed = NSMutableAttributedString(string: baseString)
    do {
        let regex = try! NSRegularExpression(pattern: searchString,options: .caseInsensitive)
        for match in regex.matches(in: baseString, options: NSRegularExpression.MatchingOptions(), range: NSRange(location: 0, length: baseString.count)) as [NSTextCheckingResult] {
            attributed.addAttribute(NSBackgroundColorAttributeName, value: UIColor.yellow, range: match.range)
        }
        attributed.addAttribute(NSFontAttributeName, value: UIFont.regularFont(ofSize: 14.0), range: NSRange(location: 0, length: attributed.string.count))
        self.loggerTextView.attributedText = attributed
    }
}