性能优化:查询优化、索引使用、缓存策略、异步加载

Content Provider 的性能问题,说白了就是数据怎么拿得快、拿得稳。我见过太多应用因为 Provider 查询慢,导致界面卡顿、ANR 频发。今天咱们就把这块硬骨头啃下来。

查询优化:别让数据库做多余的事

查询优化是性能优化的第一道关卡。我个人习惯,写任何查询前先问自己三个问题:真的需要这么多数据吗?能不能少查几次?查询条件能不能更精确?

投影(Projection)的精打细算

很多开发者喜欢用 null 作为投影参数,意思是「把所有列都返回」。这其实是个坏习惯。你想想看,如果一张表有 20 列,而界面只需要显示 3 列,那 17 列的数据就白白浪费了内存和 I/O。

// 不推荐:返回所有列
Cursor cursor = getContentResolver().query(
    uri, null, selection, selectionArgs, null);

// 推荐:只返回需要的列
String[] projection = new String[]{
    BaseColumns._ID,
    "title",
    "create_time"
};
Cursor cursor = getContentResolver().query(
    uri, projection, selection, selectionArgs, null);

小提示:投影列越少,数据库需要读取的数据页就越少,内存占用也越低。我在优化一个新闻客户端时,仅靠缩小投影就把查询时间从 120ms 降到了 45ms。

选择条件(Selection)的精准打击

选择条件要尽量利用索引。比如按时间范围查询时,如果时间字段有索引,数据库就能快速定位到数据块,而不是全表扫描。

// 不推荐:模糊匹配导致索引失效
String selection = "title LIKE '%Android%'";

// 推荐:精确匹配或前缀匹配
String selection = "category = ? AND create_time > ?";
String[] selectionArgs = new String[]{"Android", "2024-01-01"};

注意:% 开头的 LIKE 查询会让索引失效。我曾经排查过一个线上问题,用户搜索时卡死 3 秒,最后发现就是 LIKE 查询导致的全表扫描。

排序与分页的配合

排序字段一定要有索引,否则数据库需要先做文件排序(filesort),性能极差。分页时不要用 OFFSET 跳过大偏移量,而是用「游标分页」。

// 不推荐:大偏移量分页
String sortOrder = "create_time DESC LIMIT 20 OFFSET 10000";

// 推荐:游标分页
String selection = "create_time < ?";
String[] selectionArgs = new String[]{lastCreateTime};
String sortOrder = "create_time DESC LIMIT 20";

索引使用:数据库的加速器

索引就像书的目录,没有它就得一页页翻。但索引也不是越多越好,它会影响写入性能。

索引设计原则

场景 索引策略 说明
频繁查询的列 单列索引 如用户 ID、状态字段
多条件查询 复合索引 注意最左前缀原则
排序字段 索引排序 避免文件排序
唯一性约束 唯一索引 同时保证性能和完整性

嗯,这里要注意复合索引的「最左前缀原则」。比如你建了 (category, create_time) 的复合索引,那么查询条件必须从 category 开始才能用到索引。只查 create_time 的话,索引就废了。

核心要点:EXPLAIN QUERY PLAN 检查查询是否走了索引。我在项目中遇到过,明明建了索引但查询还是慢,一查发现是类型不匹配导致索引失效——字符串字段传了整数参数。

缓存策略:内存换时间

缓存是性能优化的杀手锏。但缓存用不好,轻则数据不一致,重则内存泄漏。

多级缓存架构

我个人习惯用三级缓存:内存缓存 → 磁盘缓存 → 网络/数据库。查询时先查内存,没有就查磁盘,再没有才去查数据库。

public class CacheManager {
    private LruCache<String, Cursor> memoryCache;
    private DiskLruCache diskCache;

    public CacheManager() {
        // 内存缓存:最大 4MB
        memoryCache = new LruCache<>(4 * 1024 * 1024) {
            @Override
            protected int sizeOf(String key, Cursor cursor) {
                return cursor.getCount() * 100; // 估算
            }
        };
    }

    public Cursor get(String key) {
        Cursor cursor = memoryCache.get(key);
        if (cursor != null) return cursor;

        cursor = diskCache.get(key);
        if (cursor != null) {
            memoryCache.put(key, cursor);
            return cursor;
        }

        return null; // 需要从数据库加载
    }
}

避坑指南:Cursor 对象不能跨线程共享,缓存时一定要深拷贝或者用 JSON 序列化。我曾经因为直接缓存 Cursor 导致 IllegalStateException,排查了半天才发现是 Cursor 被关闭后还在使用。

缓存失效策略

数据变更时,缓存必须及时失效。我常用的做法是:

  • 主动失效:增删改操作后,清除相关缓存键
  • 定时失效:设置 TTL,比如 5 分钟后自动过期
  • 版本号控制:给数据加版本号,版本变更时全量刷新

异步加载:别在主线程干活

Content Provider 的查询操作绝对不能放在主线程。Android 系统会监控主线程响应时间,超过 5 秒就会 ANR。

LoaderManager 的进化

早期我们用 CursorLoader,它基于 AsyncTaskLoader,能自动管理生命周期。但现在我更推荐 LiveData + Room 的组合,或者直接用 Kotlin 协程。

// 使用协程异步查询
viewModelScope.launch(Dispatchers.IO) {
    val cursor = withContext(Dispatchers.IO) {
        contentResolver.query(uri, projection, selection, args, sortOrder)
    }
    // 切换到主线程更新 UI
    withContext(Dispatchers.Main) {
        adapter.swapCursor(cursor)
    }
}

批量操作的异步处理

如果需要批量插入或更新数据,一定要用 ContentProviderOperation 批量提交,而不是一条条执行。

ArrayList<ContentProviderOperation> ops = new ArrayList<>();
for (DataItem item : items) {
    ops.add(ContentProviderOperation.newInsert(uri)
        .withValue("title", item.title)
        .withValue("content", item.content)
        .build());
}
getContentResolver().applyBatch(authority, ops);

警告:批量操作也要在后台线程执行。我见过有人把 1000 条插入放在主线程,结果界面卡了 10 秒,直接被用户差评。

知识体系总览

下面这张图总结了 Content Provider 性能优化的核心逻辑,你可以把它当作检查清单来用。

Content Provider 性能优化体系 查询优化 · 精确投影 · 精准选择条件 · 游标分页 索引使用 · 单列/复合索引 · 最左前缀原则 · EXPLAIN 分析 缓存策略 · 内存/磁盘缓存 · 多级缓存架构 · 失效策略 异步加载 · 协程/线程池 · LiveData 观察 · 批量操作 核心目标:减少 I/O 次数 → 降低响应时间 → 避免 ANR 实践建议 1. 先用 EXPLAIN 分析查询计划 2. 再根据热点查询设计索引 3. 最后用缓存和异步加载兜底

性能优化没有银弹,但把这四个维度都做到位,你的 Content Provider 基本就不会出大问题。记住一个原则:能少查就少查,能缓存就缓存,别在主线程干活。做到这三点,性能至少提升 80%。

最后说一句:性能优化是持续的过程。我每次发布版本前,都会用 StrictMode 跑一遍,检查有没有磁盘或网络操作在主线程执行。这个习惯帮我避免了好几次线上事故。


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