Uri 与 UriMatcher:ContentProvider 的“门牌号”与“导航员”
好,咱们继续深入 ContentProvider。上一章我们聊了整体架构,今天聚焦一个非常具体、但极其核心的细节——Uri 和 UriMatcher。
说白了,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。
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 |
/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 长什么样,只需要关心匹配码是多少。
避坑指南:UriMatcher 的常见陷阱
嗯,这里我要多说几句。UriMatcher 虽然简单,但坑也不少。我踩过的,都列出来:
- 顺序问题:UriMatcher 的匹配顺序是 后添加的规则优先匹配。如果你先添加了
/users/#,后添加了/users/1,那么/users/1会匹配到USER_ID,而不是你期望的精确匹配。所以,精确匹配要放在通配符前面。 - Authority 必须一致:addURI 的第一个参数必须跟 Manifest 中的 authorities 完全一致。我曾经因为多写了一个点,排查了半小时。
- 不要滥用通配符:能用精确匹配就用精确匹配。通配符用多了,UriMatcher 的性能会下降,虽然不明显,但能避免就避免。
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,这些操作写起来就像搭积木一样简单。