CursorIndexOutOfBoundsException是Android平台上非常典型的一个运行时异常,几乎每个写过数据库或者ContentProvider相关代码的开发者都遇到过。它的报错信息通常是Index 0 requested, with a size of 0或者Index 5 requested, with a size of 3这样的形式,含义非常直白:你想访问的行号超出了游标实际承载的数据范围。这个异常本身不可怕,可怕的是很多开发者不理解Cursor的内部机制,只是机械地加上try-catch了事,结果同样的崩溃换一个入口又出现了。要彻底解决它,必须先弄清楚Cursor是如何定位数据的。

一、理解Cursor的position机制
Cursor可以看成指向查询结果集的一个指针,它内部维护着一个position属性,取值范围是-1到getCount()-1。这里有个非常关键的细节:Cursor刚被创建或者刚执行moveToFirst之前,position是-1,也就是指向第一行之前的位置。很多新手会直接写出cursor.getString(0)这样的代码,此时position仍然是-1,哪怕查询结果里有数据,也会直接抛出CursorIndexOutOfBoundsException。
正确的流程是先调用moveToFirst()把游标移动到第一行,再通过getColumnIndexOrThrow拿到列索引去取值。moveToFirst()的返回值是一个boolean,如果结果集为空,它会返回false而不是抛异常,这正是我们判断空结果的最佳时机。下面这段代码展示了标准的安全读取方式:
Cursor cursor = null;
try {
cursor = db.query("user", null, "age > ?",
new String[]{"18"}, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
int nameIndex = cursor.getColumnIndexOrThrow("name");
do {
String name = cursor.getString(nameIndex);
// 处理每一行数据
} while (cursor.moveToNext());
} else {
// 结果集为空,走空数据逻辑,而不是崩溃
}
} finally {
if (cursor != null) {
cursor.close();
}
}
注意代码里用的是do-while结构而不是while,因为moveToFirst已经完成了第一次定位,循环体内直接取值即可,moveToNext负责往后走。如果用while(cursor.moveToNext())配合前面已经moveToFirst的写法,会跳过第一行数据,这是另一种隐蔽的逻辑错误。
二、最常见的几种触发场景与修复方案
第一种场景是查询结果为空却没有判断。比如用户搜索一个不存在的关键字,SQL语句本身合法,Cursor也正常返回,但getCount()是0。此时任何moveToFirst之后的取值操作都会越界。修复方式就是在取值前严格检查count或者moveToFirst的返回值。
第二种场景是getColumnIndex返回-1导致的连锁错误。当我们查询的列名在结果集中不存在时,getColumnIndex会返回-1,有些开发者没有校验这个返回值,直接把-1传给了getString,从而引发越界。这里推荐统一使用getColumnIndexOrThrow,列不存在时它会抛出IllegalArgumentException,问题在开发阶段就能暴露,而不是线上诡异的越界崩溃。
// 错误写法:列名写错时 index 为 -1,后续取值崩溃
int index = cursor.getColumnIndex("usr_name");
String name = cursor.getString(index);
// 正确写法:列不存在直接抛出明确异常,便于定位问题
int index = cursor.getColumnIndexOrThrow("user_name");
String name = cursor.getString(index);
第三种场景出现在Cursor与ListView、RecyclerView的适配器配合时。比较典型的是CursorAdapter或者自定义Adapter中,getView方法里拿到的position可能是-1(例如某些特殊调用),或者数据刷新时机不对,游标已经swap到新的Cursor,旧position却还在被使用。针对这种情况,建议在取值前加一层防御性判断:
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
if (!cursor.moveToPosition(position)) {
return; // position 非法,直接跳过,避免越界
}
String title = cursor.getString(cursor.getColumnIndexOrThrow("title"));
holder.tvTitle.setText(title);
}
moveToPosition的返回值同样是boolean,目标位置不存在时返回false,这个特性非常适合做防御性编程。此外,在Activity或Fragment销毁时要记得关闭Cursor并置空回调,避免异步查询回调时访问已关闭的游标,那也会抛出另一种形式的异常。
三、借助架构层面的手段根治问题
单纯靠加判断只能治标,如果项目里到处都是手写Cursor遍历,漏掉一两处只是时间问题。更好的做法是把数据库访问收敛到Repository或DAO层,上层只接触实体对象列表,Cursor的开关、遍历、判空全部封装在一个地方。以现在主流的Room数据库为例,它通过注解生成代码,查询结果直接映射成List<User>,Cursor完全被隐藏,越界问题从根源上被消除了。
@Dao
public interface UserDao {
@Query("SELECT * FROM user WHERE age > :minAge")
List<User> getUsersOlderThan(int minAge);
}
如果项目暂时无法迁移到Room,也可以自己封装一个统一的查询工具方法,比如定义一个泛型函数,接收查询参数和一个行转换器,内部完成moveToFirst、循环取值、关闭资源的全部工作,业务代码只负责描述每一行如何转成对象。这样Cursor相关的风险点就被压缩到了一个函数内部,排查和测试都变得简单。
最后补充一点调试技巧:遇到线上上报的CursorIndexOutOfBoundsException时,先看异常信息里的size数值。size为0基本可以断定是空结果集没判断,size大于0但index越界则多半是循环边界写错或并发修改。结合堆栈里的取值语句,通常几分钟能定位到具体代码行。养成先判空、再定位、再取值、用完即关的四个习惯,这个异常基本就不会再出现在你的崩溃榜单上了。
CursorIndexOutOfBoundsExceptionAndroid Cursor游标越界修改时间:2026-09-13 23:27:02