Swift Range(NSRange,in:String)可失败初始化器越界崩溃排查
崩溃原因分析与修复方案
问题1:为什么做了范围校验、且用了可失败初始化器,线上仍然触发崩溃?
核心原因有三个,都是本地Playground测试覆盖不到的场景:
- 校验逻辑存在漏洞,无法拦截非法值
你当前的校验nsrange.location + nsrange.length <= self.utf16.count有两个明显缺陷:- 没有校验
nsrange.location、nsrange.length为非负数:如果offset(from:to:)方法在选中范围无效的异常状态下返回负数(比如返回-1),负数和正数的字符串长度比较时结果永远为真,会直接通过校验。这些负的索引值传给底层NSString的C接口时,会被转为无符号大整数,正好对应你看到的接近2^48的异常索引值。 - 没有处理整数溢出:如果读到内存脏值导致location、length是超大正整数,相加时溢出会变成很小的数值,也会绕过你的校验逻辑。
- 没有校验
Range.init(NSRange, in: String)存在桥接bug,不会在所有非法场景返回nil
你在Playground测试用的是不可变的常量String,而线上崩溃时底层桥接的是UITextView内部持有的可变字符串NSBigMutableString。在iOS 15、iOS 16的早期系统版本中,当传入的NSRange偏移没有对齐Unicode字符边界、或者底层是可变字符串实例时,Swift标准库在遍历字符边界的过程中会直接调用characterAtIndex:触发OC层的越界异常,根本不会走返回nil的可失败逻辑——这是Swift String和NSString桥接的已知历史问题,静态测试场景很难覆盖。- UIKit跨线程访问/状态竞态导致脏值
UITextView作为UIKit组件,所有属性(包括selectedTextRange、text、位置偏移计算方法)都必须在主线程访问。如果你在后台线程调用wordAtCaret,不仅可能读到已经被释放的内存脏值(异常大索引的另一来源),还可能出现「拿到的caretRange对应旧文本长度,而读取的text已经被修改为更短的新内容」的状态不一致问题,自然会触发越界。
问题2:修复方案
按优先级落地以下修改即可彻底解决这类崩溃:
- 补全范围校验逻辑
替换原来的单行校验为完整的合法判断,同时处理整数溢出:func isValidNSRange(_ range: NSRange, for string: String) -> Bool { let utf16Count = string.utf16.count // 排除负数索引 guard range.location >= 0, range.length >= 0 else { return false } // 排除起始位置越界 guard range.location <= utf16Count else { return false } // 计算结束位置时判断溢出,溢出直接判定为非法 let (endIndex, overflow) = range.location.addingReportingOverflow(range.length) guard !overflow, endIndex <= utf16Count else { return false } return true } - 强制所有UITextView相关操作在主线程执行,且原子化读取上下文
读取选中范围和文本的操作必须放在同一个主线程调用块中,避免中间出现文本变更导致状态不一致:var wordAtCaret: (word: String, range: NSRange)? { // 已经在主线程就直接执行,否则同步切主线程取数据 if Thread.isMainThread { return fetchWordAtCaret() } else { return DispatchQueue.main.sync(execute: fetchWordAtCaret) } } // 抽离核心逻辑,保证range和text在同一上下文读取 private func fetchWordAtCaret() -> (word: String, range: NSRange)? { guard let selectedRange = self.selectedTextRange else { return nil } let location = offset(from: beginningOfDocument, to: selectedRange.start) let length = offset(from: selectedRange.start, to: selectedRange.end) let caretRange = NSRange(location: location, length: length) let currentText = self.text ?? "" return currentText.word(at: caretRange) } - 增加OC异常捕获兜底
Swift本身无法自动捕获OC层抛出的异常,不要完全依赖Range(nsrange, in:)的可失败逻辑,可以桥接一个简单的OC异常捕获工具,调用转换方法时只要捕获到底层异常就直接返回nil,用极低的成本挡住系统桥接bug导致的崩溃。 - 高频输入场景减少桥接开销:如果是在用户输入的实时回调中取光标位置的单词,可以优先用UITextInput协议自带的
textInRange:接口取文本,减少Swift String和NSString之间的桥接次数,从根源上降低触发桥接bug的概率。
内容的提问来源于stack exchange,提问作者lidium
相关产品推荐
相关产品推荐

