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?三个理由:

  1. 自动管理生命周期:Activity/Fragment 销毁时自动停止查询
  2. 数据变更自动刷新:ContentProvider 数据变化时,Loader 会自动重新查询
  3. 配置变更不丢失:屏幕旋转时,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 changeCursorswapCursor 不会关闭旧 Cursor,你需要手动关闭;changeCursor 会自动关闭旧 Cursor
  • 避免在 bindView 中做耗时操作:bindView 在 UI 线程执行,频繁调用 getColumnIndex 会影响滑动流畅度

内存管理:Cursor 泄漏的罪与罚

Cursor 的内存管理,说白了就是「谁打开谁关闭」的原则。但实际项目中,我见过太多违反这个原则的情况。

最常见的泄漏场景:

  1. Activity 销毁了,Cursor 没关闭:Cursor 持有数据库连接引用,导致数据库文件无法释放
  2. Cursor 被静态变量引用:Activity 销毁后,静态变量还持有 Cursor,导致 Activity 无法被 GC
  3. Loader 的 Cursor 没有及时 swap:onLoadFinished 中不调用 swapCursor,旧 Cursor 一直存在

最佳实践:

  • 使用 try-finally 确保 Cursor 被关闭
  • 在 Activity 的 onDestroy 中关闭所有未关闭的 Cursor
  • 使用 Loader 时,在 onLoaderReset 中调用 swapCursor(null)
  • 不要将 Cursor 赋值给静态变量

知识体系总览

下面这张图总结了 Cursor 相关的核心知识点和它们之间的关系:

Cursor 直接使用 手动查询 + 手动关闭 CursorLoader 后台查询 + 自动刷新 CursorAdapter 数据绑定 + 视图复用 moveToFirst/moveToNext onCreateLoader newView/bindView 内存管理核心原则 谁打开谁关闭 · 及时 swapCursor · 避免静态引用

避坑指南:我踩过的那些坑

我曾经在一个社交应用里,用 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 可能就会导致整个应用崩溃

这些经验都是我在实际项目中一点一点积累出来的。希望你能少走一些弯路。

公众号:蓝海资料掘金营,微信deep3321