Content URI 详解:URI 结构、URI 匹配、MIME 类型、UriMatcher 使用

各位同学,今天我们来聊聊 Content Provider 的门牌号——Content URI。说白了,URI 就是 Content Provider 对外暴露的地址。没有它,外部应用根本找不到你的数据在哪儿。

我记得刚接触 Android 时,总觉得 URI 这东西不就是个字符串吗?有什么好讲的。直到有一次,我写了个 Provider,结果第三方应用死活连不上,查了半天才发现是 URI 路径写错了。嗯,从那以后,我再也不敢小看这个「门牌号」了。

URI 结构:拆开来看其实很简单

一个标准的 Content URI 长这样:

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

我们把它拆开来看,其实就四个部分:

组成部分 示例 说明
Scheme content:// 固定前缀,表示这是一个 Content Provider 的 URI
Authority com.example.app.provider 唯一标识,通常是包名+provider 后缀
Path /user 数据表或数据集合的路径
ID /1 可选,表示某一条具体记录

我个人习惯把 Authority 想象成「小区名」,Path 是「楼栋号」,ID 是「门牌号」。这样一想,是不是好理解多了?

关键点:没有 ID 的 URI 表示操作整个集合,有 ID 的 URI 表示操作单条记录。这是 Content Provider 设计中的核心约定。

URI 匹配:UriMatcher 是怎么工作的?

你想想看,当外部应用传进来一个 URI,Provider 怎么知道它想查哪个表?是查 user 还是查 order?这时候就需要 UriMatcher 登场了。

UriMatcher 说白了就是一个「路径路由器」。它把 URI 字符串解析成整数常量,然后你在代码里 switch 这个常量就行。

来看一个我实际项目中的例子:

public class MyProvider extends ContentProvider {
    private static final int USERS = 1;
    private static final int USER_ID = 2;
    private static final int ORDERS = 3;
    private static final int ORDER_ID = 4;

    private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);

    static {
        uriMatcher.addURI("com.example.app.provider", "user", USERS);
        uriMatcher.addURI("com.example.app.provider", "user/#", USER_ID);
        uriMatcher.addURI("com.example.app.provider", "order", ORDERS);
        uriMatcher.addURI("com.example.app.provider", "order/#", ORDER_ID);
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
                        String[] selectionArgs, String sortOrder) {
        switch (uriMatcher.match(uri)) {
            case USERS:
                // 查询所有用户
                break;
            case USER_ID:
                // 查询单个用户
                break;
            default:
                throw new IllegalArgumentException("Unknown URI: " + uri);
        }
    }
}

这里有个细节要注意:# 表示匹配任意数字,* 表示匹配任意文本。我在项目中遇到过有人把 #* 搞混,结果匹配总是不对。记住:# 是数字通配符,* 是文本通配符。

小技巧:UriMatcher 的匹配是从上到下的,所以如果你有多个相似的路径,把更具体的放在前面。比如 /user/me/user/#,应该先添加 /user/me

MIME 类型:告诉调用方你返回的是什么

MIME 类型,说白了就是告诉调用方:「我返回的数据是啥格式」。Content Provider 支持两种 MIME 类型:

  • 单条记录:vnd.android.cursor.item/vnd.yourdomain.yourentity
  • 多条记录:vnd.android.cursor.dir/vnd.yourdomain.yourentity

举个例子,如果我的 Provider 返回用户列表,MIME 类型应该是:

vnd.android.cursor.dir/vnd.example.user

如果返回单个用户:

vnd.android.cursor.item/vnd.example.user

为什么要区分?因为调用方可以根据 MIME 类型判断是操作集合还是单条记录。比如 ContentResolver 的 notifyChange() 方法,会根据 MIME 类型决定如何刷新 UI。

避坑指南:我曾经在项目中忘记重写 getType() 方法,结果返回了 null。调用方拿到 null 后直接崩溃了。记住:getType() 必须返回正确的 MIME 类型,不能偷懒。

UriMatcher 使用的最佳实践

用多了 UriMatcher,我总结了几条经验:

  1. 常量命名要有意义:别用 CODE_1CODE_2 这种,用 USERSUSER_ID 一目了然。
  2. 匹配失败要抛异常:default 分支一定要抛出 IllegalArgumentException,方便调试。
  3. 路径设计要合理:别把路径搞得太深,一般两层就够了。比如 /user/1 可以,/user/1/order/2 就有点过了。

说到路径设计,我画了一张图,帮你理清 URI 匹配的整个流程:

Content URI 匹配流程 外部传入 URI UriMatcher.match(uri) → 返回整数常量 switch(uriMatcher.match(uri)) 进入对应分支 USERS → 查询所有用户 USER_ID → 查询单个用户 default → 抛异常 匹配失败时抛出 IllegalArgumentException,方便定位问题

看到没?整个流程其实很清晰。URI 进来,UriMatcher 匹配,switch 分发,各走各的路。

总结一下

Content URI 这块,说白了就是三件事:

  • URI 结构:content:// + Authority + Path + ID,记住这个公式就行。
  • URI 匹配:用 UriMatcher 把字符串转成整数,switch 处理。
  • MIME 类型:告诉调用方你返回的是单条还是多条数据。

嗯,内容就这些。下一章我们聊聊 Content Provider 的 CRUD 操作,到时候会用到今天讲的 URI 知识。各位先把这块吃透,后面就顺了。


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