Uri 与 UriMatcher:ContentProvider 的“门牌号”与“导航员”

好,咱们继续深入 ContentProvider。上一章我们聊了整体架构,今天聚焦一个非常具体、但极其核心的细节——UriUriMatcher

说白了,Uri 就是 ContentProvider 的“门牌号”。你想访问哪个表、哪条数据,都得靠它来定位。而 UriMatcher 呢?它就像个“导航员”,帮你把 Uri 快速匹配到对应的操作逻辑上。

我个人习惯,写 ContentProvider 之前,先把 Uri 和 UriMatcher 规划好。这一步做扎实了,后面写 CRUD 代码会非常顺畅。否则,你想想看,每次都要手动解析字符串,那得多痛苦?

Uri 的结构:拆解这个“门牌号”

一个标准的 ContentProvider Uri 长这样:

content://com.example.app.provider/user/1

咱们把它拆开来看,一共四个部分:

组成部分 示例 含义
Scheme content:// 固定前缀,表示这是一个 ContentProvider 的 Uri
Authority com.example.app.provider 唯一标识,对应 AndroidManifest 中 provider 的 authorities
Path /user/1 资源路径,可以有多段,用来区分表或具体数据
Query(可选) ?name=xxx 查询参数,跟 Web 的 URL 一样

这里有个细节:Scheme 必须是 content://,这是 Android 系统规定的。Authority 必须跟你在 AndroidManifest 里声明的 authorities 完全一致,否则系统找不到你的 Provider。

注意:Authority 是大小写敏感的。我曾经见过一个项目,因为 Manifest 里写的是小写,代码里拼了大写,结果查了半天才发现。嗯,这种低级错误,排查起来真的很浪费时间。

Path 的设计:单数还是复数?

Path 的设计其实有讲究。我个人习惯遵循 RESTful 风格:

  • 操作整个表:用复数,比如 /users
  • 操作单条记录:用复数 + ID,比如 /users/1

为什么不用单数?你想想看,/user/user/1 在语义上容易混淆。用复数 /users 表示集合,/users/1 表示集合中的某个元素,逻辑更清晰。

当然,这不是强制规定。你完全可以用 /user 表示表,/user/# 表示具体行。但团队协作时,统一风格很重要。

UriMatcher 的匹配规则:从“门牌号”到“导航员”

好,Uri 定义好了,接下来怎么用?总不能每次都在 query、insert 方法里写 if-else 判断吧?

这时候 UriMatcher 就登场了。它的核心作用就一个:把 Uri 映射成一个整数常量。这样你在 CRUD 方法里,只需要 switch 这个整数就行了。

匹配规则其实很简单,就三种通配符:

通配符 含义 示例
# 匹配任意数字 /user/# 匹配 /user/1/user/99
* 匹配任意文本 /user/* 匹配 /user/abc/user/xyz
无通配符 精确匹配 /user 只匹配 /user
核心要点:UriMatcher 的匹配是从左到右、逐段进行的。通配符只匹配当前段,不会跨段。比如 /user/#/detail 不会匹配 /user/1,因为后面还有 /detail 没匹配上。

代码实现:手把手写一个 UriMatcher

光说不练假把式。咱们直接上代码,看看实际项目中怎么用。

第一步,定义常量:

public class UserProvider extends ContentProvider {
    // 定义匹配码,一般用常量表示
    private static final int USERS = 100;          // 匹配 /users
    private static final int USER_ID = 101;        // 匹配 /users/#
    
    private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
    
    static {
        // 添加匹配规则
        sUriMatcher.addURI("com.example.app.provider", "users", USERS);
        sUriMatcher.addURI("com.example.app.provider", "users/#", USER_ID);
    }
}

第二步,在 CRUD 方法中使用:

@Override
public Cursor query(Uri uri, String[] projection, String selection,
                    String[] selectionArgs, String sortOrder) {
    int match = sUriMatcher.match(uri);
    
    switch (match) {
        case USERS:
            // 查询整个表
            return queryAllUsers();
        case USER_ID:
            // 查询单条记录,从 Uri 中提取 ID
            String userId = uri.getLastPathSegment();
            return queryUserById(userId);
        default:
            throw new IllegalArgumentException("Unknown URI: " + uri);
    }
}

看到没?代码瞬间变得清晰了。你不需要关心 Uri 长什么样,只需要关心匹配码是多少。

我的习惯:匹配码我喜欢用 100、200、300 这样的区间,而不是 1、2、3。这样以后新增规则时,可以在区间内插入,不用改已有的常量。比如 100-199 给用户表,200-299 给订单表,一目了然。

避坑指南:UriMatcher 的常见陷阱

嗯,这里我要多说几句。UriMatcher 虽然简单,但坑也不少。我踩过的,都列出来:

  • 顺序问题:UriMatcher 的匹配顺序是 后添加的规则优先匹配。如果你先添加了 /users/#,后添加了 /users/1,那么 /users/1 会匹配到 USER_ID,而不是你期望的精确匹配。所以,精确匹配要放在通配符前面
  • Authority 必须一致:addURI 的第一个参数必须跟 Manifest 中的 authorities 完全一致。我曾经因为多写了一个点,排查了半小时。
  • 不要滥用通配符:能用精确匹配就用精确匹配。通配符用多了,UriMatcher 的性能会下降,虽然不明显,但能避免就避免。
特别注意:UriMatcher 的 match 方法返回的是 int,如果没匹配到,返回的是你在构造时传入的 UriMatcher.NO_MATCH(默认是 -1)。所以 switch 里一定要加 default 分支,抛出异常或返回空结果。否则,你可能会在不知不觉中返回了错误的数据。

实战经验:多表场景下的 UriMatcher 设计

我记得在一个电商项目中,一个 Provider 要管理用户、订单、商品三张表。如果所有规则都写在一个 UriMatcher 里,会变得非常臃肿。

我的做法是:按业务模块拆分 UriMatcher。比如:

private static final UriMatcher sUserMatcher = new UriMatcher(UriMatcher.NO_MATCH);
private static final UriMatcher sOrderMatcher = new UriMatcher(UriMatcher.NO_MATCH);
private static final UriMatcher sProductMatcher = new UriMatcher(UriMatcher.NO_MATCH);

static {
    // 用户相关
    sUserMatcher.addURI(AUTHORITY, "users", USERS);
    sUserMatcher.addURI(AUTHORITY, "users/#", USER_ID);
    
    // 订单相关
    sOrderMatcher.addURI(AUTHORITY, "orders", ORDERS);
    sOrderMatcher.addURI(AUTHORITY, "orders/#", ORDER_ID);
    
    // 商品相关
    sProductMatcher.addURI(AUTHORITY, "products", PRODUCTS);
    sProductMatcher.addURI(AUTHORITY, "products/#", PRODUCT_ID);
}

然后在 query 方法里,根据 Uri 的 path 前缀,选择对应的 UriMatcher 去匹配。这样每个 Matcher 只负责自己的模块,代码清晰,也方便维护。

当然,如果你的表不多,一个 UriMatcher 也完全够用。这个看个人习惯,没有绝对的对错。

总结一下

Uri 和 UriMatcher 是 ContentProvider 的基石。Uri 负责“指路”,UriMatcher 负责“导航”。把这两块搞明白,你的 Provider 代码会非常干净、易维护。

下一章,咱们聊聊 ContentProvider 的 CRUD 操作,看看 query、insert、update、delete 具体怎么写。到时候你会发现,有了 UriMatcher,这些操作写起来就像搭积木一样简单。