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

RecyclerView显示蓝牙设备报Size 1 Index 1越界异常崩溃

问题描述

通过RecyclerView展示附近扫描到的蓝牙设备列表,应用运行一段时间后会突发崩溃,崩溃大概率触发于蓝牙设备新增或移除的场景。已添加所有必要的空值校验,也在数据插入、变更时调用对应notify方法通知适配器更新,尝试替换为notifyDataSetChanged()全量更新后仍然会崩溃,始终未定位到遗漏的问题点。

相关代码

SensorLookup类(RecyclerView所在页面类)

private val scanResults = mutableListOf<ScanResult>()

private fun setupRecyclerView() {
    scan_results_recycler_view.apply {
        adapter = scanResultAdapter
        layoutManager = LinearLayoutManager(
                this@SensorLookup,
                RecyclerView.VERTICAL,
                false
        )
        isNestedScrollingEnabled = false
    }

    val animator = scan_results_recycler_view.itemAnimator
    if (animator is SimpleItemAnimator) {
        animator.supportsChangeAnimations = false
    }
}

private val scanCallback = object : ScanCallback() {
        @RequiresApi(Build.VERSION_CODES.N)
        override fun onScanResult(callbackType: Int, result: ScanResult) {
            val indexQuery = scanResults.indexOfFirst { it.device.address == result.device.address }
            if (indexQuery != -1) { // 同地址扫描结果已存在
                scanResults[indexQuery] = result
                scanResultAdapter.notifyItemChanged(indexQuery)
                //scanResultAdapter.notifyDataSetChanged()
            } else {
                with(result.device) {
                    //Timber.i("Found BLE device! Name: ${name ?: "Unnamed"}, address: $address")
                }
                scanResults.add(result)
                scanResultAdapter.notifyItemInserted(scanResults.size - 1)
                //scanResultAdapter.notifyDataSetChanged()
            }
        }
        override fun onScanFailed(errorCode: Int) {
            //Timber.e("onScanFailed: code $errorCode")
        }
    }

ScanResultAdapter类(RecyclerView对应的适配器类)

class ScanResultAdapter(
    private val items: List<ScanResult>,
    private val sensorInfoHandler: SensorInfoHandler, // sql数据库实例
    private val onClickListener: ((device: ScanResult) -> Unit)
) : RecyclerView.Adapter<ScanResultAdapter.ViewHolder>() {


override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
    val view = parent.context.layoutInflater.inflate(
        R.layout.row_scan_result,
        parent,
        false
    )


    return ViewHolder(view, sensorInfoHandler, onClickListener)
}

override fun getItemCount() = items.size

override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    val item = items[position]
    holder.bind(item)
}

class ViewHolder(
    private val view: View,
    private val sensorInfoHandler: SensorInfoHandler,
    private val onClickListener: ((device: ScanResult) -> Unit)
) : RecyclerView.ViewHolder(view) {

    @SuppressLint("MissingPermission")
    fun bind(result: ScanResult) {
        val deviceName: String = result.device.name ?: "Unnamed"

        if(!result.device.name.isNullOrBlank() || !result.device.name.isNullOrEmpty()) {
            val nameParts = result.device.name.split("_")
            if (!nameParts.isNullOrEmpty()) {
                val cursor: Cursor = sensorInfoHandler.getById(nameParts[1].trim()) // 报错位置
                if (cursor.moveToFirst()) {
                    val friendlyname = sensorInfoHandler.getFriendlyName(cursor)
                    val friendlyname2 = sensorInfoHandler.getFriendlyName2(cursor)
                    if ((friendlyname.isNullOrEmpty() || friendlyname.isNullOrBlank() || friendlyname == "-" || friendlyname == "null") && (!friendlyname2.isNullOrEmpty() || !friendlyname2.isNullOrBlank() || friendlyname2 != "-" || friendlyname2 != null)) {
                       // 业务代码
                    } else if ((friendlyname2.isNullOrEmpty() || friendlyname2.isNullOrBlank() || friendlyname2 == "-" || friendlyname2 == null) && (!friendlyname.isNullOrEmpty() || !friendlyname.isNullOrBlank() || friendlyname != "-" || friendlyname != "null")) {
                        // 业务代码
                    } else if (!friendlyname.isNullOrEmpty() || !friendlyname.isNullOrBlank() || friendlyname != "-" || friendlyname != "null" || !friendlyname2.isNullOrEmpty() || !friendlyname2.isNullOrBlank() || friendlyname2 != "-" || friendlyname2 != null) {
                        // 业务代码
                    }
                } else {
                    // 业务代码
                }
            }
        }
    }
}
}
问题根因与修复方案
  • 线程违规:非主线程操作UI与适配器
    蓝牙ScanCallback默认运行在Binder后台线程,并非主线程。你直接在回调中修改数据源scanResults、调用适配器notify系列方法,属于典型的跨线程操作UI组件,当设备上下线频繁、回调并发触发时,会出现数据源修改和适配器读取不同步的问题,哪怕用notifyDataSetChanged()也会触发崩溃。
    修复:将回调内所有修改数据源、调用适配器更新的逻辑全部切到主线程执行,可以用runOnUiThread或者协程Dispatchers.Main调度包裹对应代码块。

  • 数组越界:设备名分割后长度校验缺失
    你标记的报错位置存在两个逻辑漏洞:一是判断设备名非空的条件用了||(或)逻辑,判断完全失效;二是对设备名执行split("_")后,没有校验分割后的数组长度是否≥2,就直接取索引为1的元素。如果扫描到的蓝牙设备名不含下划线,分割后数组长度仅为1,直接取[1]会直接抛出IndexOutOfBoundsException崩溃。
    修复:将空判断的||改为&&确保设备名合法,分割设备名后先判断数组长度满足取值要求再做后续数据库查询,同时避免在bind方法里用和参数同名的变量引发混淆。

  • 资源泄漏:Cursor未正常关闭
    每次列表绑定都查询数据库返回Cursor对象,但所有业务分支走完后都没有调用cursor.close()释放资源,长时间运行、频繁刷新列表时会累积大量未关闭的Cursor,轻则触发内存泄漏,重则导致数据库操作异常崩溃。
    修复:Cursor使用完成后必须调用close方法释放资源,推荐用Kotlin的use扩展函数包裹Cursor操作,实现自动关闭。

  • 并发修改风险:适配器直接持有外部可变列表引用
    适配器构造函数直接接收外部的可变列表引用,列表修改和适配器读取没有同步保护,当后台线程修改列表时,getItemCount()返回的列表长度和实际读items[position]时的列表长度可能不一致,同样会触发索引越界崩溃。
    修复:推荐使用ListAdapter配合DiffUtil处理列表数据更新,不要直接把外部可变列表传给适配器,每次数据更新时传入不可变的列表副本,从根源避免并发修改问题。

内容的提问来源于stack exchange,提问作者Do Ji

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 18:57:34