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

