Cursor 详解:从基础使用到内存管理的完整指南
说实话,Cursor 是 Android 开发里最容易被忽视却又最关键的角色之一。我见过太多项目因为 Cursor 没用好,导致内存泄漏、UI 卡顿,甚至直接崩溃。今天我就把这块内容掰开揉碎了讲清楚。
什么是 Cursor?
Cursor 说白了就是一个指向数据行的指针。它不存储数据本身,而是提供了一种高效遍历数据库结果集的方式。你想想看,如果 ContentProvider 查询返回的是十万行数据,直接全部加载到内存里,那应用不炸才怪。
Cursor 的设计哲学就是「按需加载」。它只在内存中保留当前行和少量缓存数据,其他数据还在数据库文件里躺着。当你调用 moveToNext() 时,它才去读取下一行。
核心要点:Cursor 不是数据容器,而是数据游标。它让你能够以流式方式处理大量数据。
Cursor 的基本使用
先看一个最基础的查询示例:
// 查询所有联系人
Cursor cursor = getContentResolver().query(
ContactsContract.Contacts.CONTENT_URI,
null, // 返回所有列
null, // 没有筛选条件
null, // 没有参数
null // 默认排序
);
if (cursor != null) {
while (cursor.moveToNext()) {
String name = cursor.getString(
cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME)
);
long id = cursor.getLong(
cursor.getColumnIndex(ContactsContract.Contacts._ID)
);
// 处理数据...
}
cursor.close(); // 这一步千万不能忘!
}
嗯,这里要注意几个关键点:
- 判空检查:query 返回的 Cursor 可能为 null,一定要做判空
- 列索引缓存:
getColumnIndex()每次调用都会做字符串比较,我习惯在循环外先拿到索引 - 关闭 Cursor:不关闭会导致 Cursor 泄漏,数据库文件无法释放
我的习惯:把列索引定义成常量,比如 private static final int COLUMN_NAME = 1;,这样既快又安全。
CursorLoader:让数据加载不再阻塞 UI
直接在 UI 线程执行数据库查询?我曾经见过一个同事这么干,结果查询 5 万条数据,界面直接 ANR 了。这就是为什么我们需要 CursorLoader。
CursorLoader 是 LoaderManager 和 Cursor 的结合体。它自动在后台线程执行查询,查询完成后把结果回调到 UI 线程。
public class MyFragment extends Fragment
implements LoaderManager.LoaderCallbacks<Cursor> {
private static final int LOADER_ID = 1;
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
getLoaderManager().initLoader(LOADER_ID, null, this);
}
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
return new CursorLoader(
getActivity(),
MyContentProvider.CONTENT_URI,
PROJECTION,
null, null, null
);
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
// 数据加载完成,更新 UI
mAdapter.swapCursor(data);
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
// Loader 被重置,清除引用
mAdapter.swapCursor(null);
}
}
为什么推荐用 CursorLoader?三个理由:
- 自动管理生命周期:Activity/Fragment 销毁时自动停止查询
- 数据变更自动刷新:ContentProvider 数据变化时,Loader 会自动重新查询
- 配置变更不丢失:屏幕旋转时,Loader 会保留已有数据,不会重新查询
注意:CursorLoader 在 API 28 后被标记为废弃,Google 推荐使用 ViewModel + LiveData + Room 的组合。但如果你维护的是老项目,CursorLoader 依然是好用的方案。
CursorAdapter:Cursor 和 ListView 的桥梁
CursorAdapter 是专门为 Cursor 设计的 Adapter。它知道如何从 Cursor 中读取数据,并绑定到 ListView 的每一项上。
public class MyCursorAdapter extends CursorAdapter {
public MyCursorAdapter(Context context, Cursor c, int flags) {
super(context, c, flags);
}
@Override
public View newView(Context context, Cursor cursor, ViewGroup parent) {
// 创建新视图
LayoutInflater inflater = LayoutInflater.from(context);
return inflater.inflate(R.layout.list_item, parent, false);
}
@Override
public void bindView(View view, Context context, Cursor cursor) {
// 绑定数据到视图
TextView textView = view.findViewById(R.id.text);
String text = cursor.getString(
cursor.getColumnIndex(MyContract.COLUMN_NAME)
);
textView.setText(text);
}
}
使用 CursorAdapter 时,有几点我踩过坑:
- flags 参数:建议使用
FLAG_REGISTER_CONTENT_OBSERVER,这样 Cursor 数据变化时 Adapter 会自动刷新 - swapCursor vs changeCursor:
swapCursor不会关闭旧 Cursor,你需要手动关闭;changeCursor会自动关闭旧 Cursor - 避免在 bindView 中做耗时操作:bindView 在 UI 线程执行,频繁调用 getColumnIndex 会影响滑动流畅度
内存管理:Cursor 泄漏的罪与罚
Cursor 的内存管理,说白了就是「谁打开谁关闭」的原则。但实际项目中,我见过太多违反这个原则的情况。
最常见的泄漏场景:
- Activity 销毁了,Cursor 没关闭:Cursor 持有数据库连接引用,导致数据库文件无法释放
- Cursor 被静态变量引用:Activity 销毁后,静态变量还持有 Cursor,导致 Activity 无法被 GC
- Loader 的 Cursor 没有及时 swap:onLoadFinished 中不调用 swapCursor,旧 Cursor 一直存在
最佳实践:
- 使用 try-finally 确保 Cursor 被关闭
- 在 Activity 的 onDestroy 中关闭所有未关闭的 Cursor
- 使用 Loader 时,在 onLoaderReset 中调用 swapCursor(null)
- 不要将 Cursor 赋值给静态变量
知识体系总览
下面这张图总结了 Cursor 相关的核心知识点和它们之间的关系:
避坑指南:我踩过的那些坑
我曾经在一个社交应用里,用 Cursor 加载用户的好友列表。当时图省事,直接在 Activity 的 onCreate 里执行查询,结果好友数量超过 5000 时,界面卡了整整 3 秒才显示出来。后来换成 CursorLoader,问题迎刃而解。
还有一次,我在 Fragment 里忘记在 onDestroy 中关闭 Cursor,结果每次切换 Fragment 都会泄漏一个 Cursor。用户来回切换 10 次后,应用直接 OOM 崩溃。嗯,从那以后我养成了在 onDestroy 中统一清理资源的习惯。
调试技巧:使用 StrictMode 可以帮你发现 Cursor 泄漏。在 Application 的 onCreate 中开启:
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
.detectLeakedClosableObjects()
.penaltyLog()
.build());
这样每次 Cursor 泄漏时,Logcat 都会打印详细的堆栈信息。
总结
Cursor 虽然看起来简单,但用好了能大幅提升应用性能和稳定性。记住三个核心点:
- 能用 CursorLoader 就别手动管理 Cursor 生命周期
- CursorAdapter 的 swapCursor 和 changeCursor 要分清楚
- 内存管理是重中之重,泄漏一个 Cursor 可能就会导致整个应用崩溃
这些经验都是我在实际项目中一点一点积累出来的。希望你能少走一些弯路。