基于UITextView的代码编辑器着色卡顿及异步失效问题求助
看起来你遇到了UITextView代码着色卡顿的问题,而且尝试多线程优化时还没效果,我来帮你分析下问题根源,再给你几个可行的优化方案。
首先说下多线程没效果的核心原因:
- 你可能在后台线程操作了UI相关的最终赋值——设置
textView.attributedText必须在主线程执行,哪怕前面的着色逻辑在后台完成,最后一步也得切回主线程。 wordToColor作为全局变量,读写没有线程安全保护:如果后台线程读取它的时候,主线程正在动态修改,会导致数据不一致,自然看不到着色效果。
再说说性能卡顿的关键问题:
你的当前实现有几个效率极低的点:
- 每次文本变化都重新生成完整的
NSMutableAttributedString,且每个关键词单独遍历整个文本,关键词越多,重复遍历的次数就越多,耗时呈线性增长。 - 用递归方式查找关键词,递归本身有额外的栈开销,还容易出现不必要的重复计算。
- NSString和Swift String的频繁转换,也会带来额外的性能损耗。
方案一:用正则表达式批量匹配,大幅减少遍历次数
把所有需要着色的关键词拼成一个正则表达式,一次遍历就能匹配所有关键词的位置,统一完成着色,比单个关键词逐个遍历高效得多。
代码示例:
var wordToColor = [String: UIColor]() // 用串行队列保证wordToColor的线程安全读写 private let wordSyncQueue = DispatchQueue(label: "com.yourapp.wordColorQueue") func textViewDidChange(_ textView: UITextView) { updateLineText() } func updateLineText() { // 防抖处理:用户连续打字时,延迟0.1秒再执行着色,避免频繁触发耗时操作 NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(performColoring), object: nil) perform(#selector(performColoring), with: nil, afterDelay: 0.1) } @objc private func performColoring() { let currentContent = content // 后台线程处理耗时的着色逻辑 DispatchQueue.global().async { [weak self] in guard let self = self else { return } // 线程安全读取wordToColor let wordMap = self.wordSyncQueue.sync { self.wordToColor } if wordMap.isEmpty { DispatchQueue.main.async { self.textView.attributedText = NSAttributedString(string: currentContent) } return } // 构建正则表达式:转义关键词中的特殊字符,用|分隔所有关键词 let escapedWords = wordMap.keys.map { NSRegularExpression.escapedPattern(for: $0) } let pattern = "\\b(" + escapedWords.joined(separator: "|") + ")\\b" guard let regex = try? NSRegularExpression(pattern: pattern, options: []) else { DispatchQueue.main.async { self.textView.attributedText = NSAttributedString(string: currentContent) } return } let mutableAttrString = NSMutableAttributedString(string: currentContent) let fullRange = NSRange(location: 0, length: currentContent.utf16.count) // 遍历所有匹配结果,统一着色 regex.enumerateMatches(in: currentContent, options: [], range: fullRange) { result, _, stop in guard let matchRange = result?.range else { return } let matchedWord = (currentContent as NSString).substring(with: matchRange) if let color = wordMap[matchedWord] { mutableAttrString.addAttribute(.foregroundColor, value: color, range: matchRange) // 检查关键词前是否有.,有则改成紫色 if matchRange.location > 0 { let dotRange = NSRange(location: matchRange.location - 1, length: 1) let dotChar = (currentContent as NSString).substring(with: dotRange) if dotChar == "." { mutableAttrString.addAttribute(.foregroundColor, value: UIColor.purple, range: matchRange) } } } } // 回到主线程更新UI,同时检查文本是否一致(避免用户在处理过程中修改了文本) DispatchQueue.main.async { if self.content == currentContent { self.textView.attributedText = mutableAttrString } } } }
方案二:优化原有逻辑的线程安全与执行效率
如果你更倾向于保留原有单个关键词处理的逻辑,可以做以下调整:
- 用串行队列保证
wordToColor的线程安全。 - 把递归改成迭代,避免栈开销。
- 后台线程只负责构建
NSMutableAttributedString,最终UI赋值必须在主线程。
代码示例:
var wordToColor = [String: UIColor]() private let wordSyncQueue = DispatchQueue(label: "com.yourapp.wordColorQueue") func textViewDidChange(_ textView: UITextView) { updateLineText() } func updateLineText() { NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(performColoring), object: nil) perform(#selector(performColoring), with: nil, afterDelay: 0.1) } @objc private func performColoring() { let currentContent = content DispatchQueue.global().async { [weak self] in guard let self = self else { return } let wordMap = self.wordSyncQueue.sync { self.wordToColor } let mutableAttrString = NSMutableAttributedString(string: currentContent) for word in wordMap.keys { self.applyColoring(to: mutableAttrString, word: word, content: currentContent) } DispatchQueue.main.async { if self.content == currentContent { self.textView.attributedText = mutableAttrString } } } } // 把递归改成迭代,避免栈开销 private func applyColoring(to string: NSMutableAttributedString, word: String, content: String) { var startLocation = 0 let contentLength = content.utf16.count let wordLength = word.utf16.count while startLocation <= contentLength - wordLength { let searchRange = NSRange(location: startLocation, length: contentLength - startLocation) let foundRange = (content as NSString).range(of: word, options: [], range: searchRange) if foundRange.location == NSNotFound { break } if self.rangeCheck(range: foundRange) { if let color = wordToColor[word] { string.addAttribute(.foregroundColor, value: color, range: foundRange) if foundRange.location > 0 { let index0 = content.index(content.startIndex, offsetBy: foundRange.location - 1) if content[index0] == "." { string.addAttribute(.foregroundColor, value: UIColor.purple, range: foundRange) } } } } startLocation = foundRange.location + foundRange.length } }
额外优化建议
- 防抖处理:上面代码中加入了延迟执行和取消之前请求的逻辑,能避免用户连续打字时频繁触发着色操作,大幅减少性能消耗。
- 文本一致性检查:后台处理完成后,回到主线程要检查当前文本是否和处理时一致,避免设置过时的着色内容。
- 统一范围处理:尽量用
utf16.count计算NSRange,因为NSRange是基于UTF-16编码的,和NSString的长度逻辑一致,减少转换开销。
内容的提问来源于stack exchange,提问作者Anters Bear
相关产品推荐
相关产品推荐

