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

Swift Range(NSRange,in:String)可失败初始化器越界崩溃排查

崩溃原因分析与修复方案

问题1:为什么做了范围校验、且用了可失败初始化器,线上仍然触发崩溃?

核心原因有三个,都是本地Playground测试覆盖不到的场景:

  • 校验逻辑存在漏洞,无法拦截非法值
    你当前的校验nsrange.location + nsrange.length <= self.utf16.count有两个明显缺陷:
    1. 没有校验nsrange.location、nsrange.length为非负数:如果offset(from:to:)方法在选中范围无效的异常状态下返回负数(比如返回-1),负数和正数的字符串长度比较时结果永远为真,会直接通过校验。这些负的索引值传给底层NSString的C接口时,会被转为无符号大整数,正好对应你看到的接近2^48的异常索引值。
    2. 没有处理整数溢出:如果读到内存脏值导致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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.30 12:45:25