ContentProvider注解:自动生成Provider声明与URI匹配代码
说实话,ContentProvider 在 Android 开发中是个挺尴尬的存在。
你说它没用吧,跨进程数据共享全靠它。你说它好用吧,写起来是真的繁琐——一个简单的增删改查,你得写 URI 匹配、写 MIME 类型、写各种 switch-case。我早期做 IM 项目的时候,光一个联系人 Provider 就写了三百多行模板代码,改个表结构还得同步改 URI 常量,烦得很。
那能不能用注解处理器把这些重复劳动干掉?当然可以。今天我们就聊聊怎么用注解自动生成 ContentProvider 的声明和 URI 匹配代码。
痛点在哪?
先看看传统写法有多折磨人。你写一个 ContentProvider,大概要做这几件事:
- 定义一堆 URI 常量,比如
CONTENT_URI = Uri.parse("content://com.example.app.provider/table") - 在
onCreate()里注册 UriMatcher - 在
query()、insert()等方法里写 switch-case 匹配 URI - 在 AndroidManifest.xml 里声明
<provider>标签
这些工作有什么技术含量吗?没有。纯粹是体力活。而且一旦表多了,代码膨胀得厉害。我曾经维护过一个项目,Provider 类接近 2000 行,一半以上是 URI 匹配和 switch-case。
核心思路:用注解标注数据表,用注解处理器在编译期生成 Provider 代码和 URI 匹配逻辑。你只需要关心业务,模板代码交给机器。
定义注解:告诉处理器你的表长什么样
我们先定义两个注解。一个标注在表类上,一个标注在字段上。
// 标注数据表
@Target(AnnotationTarget.CLASS)
@Retention(AnnotationRetention.SOURCE)
annotation class Table(
val tableName: String,
val authority: String = ""
)
// 标注字段
@Target(AnnotationTarget.FIELD)
@Retention(AnnotationRetention.SOURCE)
annotation class Column(
val name: String = "",
val primaryKey: Boolean = false
)
这里有个细节:authority 我留了默认值。为什么?因为一个应用通常只有一个 authority,我习惯在 Application 级别的注解里统一配置,表注解里就不重复写了。当然你也可以每个表单独指定,看项目需求。
用法很简单:
@Table(tableName = "users", authority = "com.example.app.provider")
data class User(
@Column(name = "_id", primaryKey = true) val id: Long = 0,
@Column(name = "name") val name: String = "",
@Column(name = "age") val age: Int = 0
)
注解处理器:编译期生成 Provider
处理器要做三件事:
- 扫描所有带
@Table注解的类 - 解析字段信息,生成建表语句和 URI 常量
- 生成 ContentProvider 子类,包含完整的 URI 匹配和 CRUD 实现
核心代码大概长这样:
@AutoService(Processor::class)
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@SupportedAnnotationTypes("com.example.annotation.Table")
class ProviderProcessor : AbstractProcessor() {
override fun process(annotations: Set<TypeElement>, roundEnv: RoundEnvironment): Boolean {
val tables = roundEnv.getElementsAnnotatedWith(Table::class.java)
if (tables.isEmpty()) return false
val providerBuilder = StringBuilder()
providerBuilder.append("""
package com.example.generated;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
public class AutoGeneratedProvider extends ContentProvider {
private static final String AUTHORITY = "com.example.app.provider";
private static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
""".trimIndent())
// 为每个表生成 URI 匹配代码
var codeIndex = 0
for (table in tables) {
val tableName = table.getAnnotation(Table::class.java).tableName
providerBuilder.append("""
private static final int ${tableName.toUpperCase()}_CODE = $codeIndex;
private static final int ${tableName.toUpperCase()}_ID_CODE = ${codeIndex + 1};
static {
URI_MATCHER.addURI(AUTHORITY, "$tableName", ${tableName.toUpperCase()}_CODE);
URI_MATCHER.addURI(AUTHORITY, "$tableName/#", ${tableName.toUpperCase()}_ID_CODE);
}
""".trimIndent())
codeIndex += 2
}
// 生成 query/insert/update/delete 方法
providerBuilder.append("""
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
SQLiteDatabase db = getDatabase();
switch (URI_MATCHER.match(uri)) {
""".trimIndent())
for (table in tables) {
val tableName = table.getAnnotation(Table::class.java).tableName
providerBuilder.append("""
case ${tableName.toUpperCase()}_CODE:
return db.query("$tableName", projection, selection, selectionArgs, null, null, sortOrder);
case ${tableName.toUpperCase()}_ID_CODE:
String id = uri.getLastPathSegment();
return db.query("$tableName", projection, "_id=?", new String[]{id}, null, null, sortOrder);
""".trimIndent())
}
providerBuilder.append("""
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}
}
// ... 类似生成 insert、update、delete
}
""".trimIndent())
// 写入文件
val file = processingEnv.filer.createSourceFile("com.example.generated.AutoGeneratedProvider")
file.openWriter().use { it.write(providerBuilder.toString()) }
return true
}
}
我的习惯:生成代码时,我会把每个表的 URI 匹配代码单独抽成一个内部类。这样如果某个表逻辑特殊,用户可以继承生成的 Provider 并重写对应方法。别把所有代码揉在一个大 switch-case 里,后期维护会想骂人。
自动生成 AndroidManifest 声明
代码生成了,但别忘了 AndroidManifest.xml 里还得声明 Provider。手动写?太 low 了。我们可以用另一个注解处理器或者 Gradle 插件来做。
简单方案:在处理器里生成一个 provider-declaration.xml 文件,然后在主工程的 AndroidManifest 里用 <merge> 标签引入。不过这种方式需要配合 Gradle 的 manifest merger 机制,稍微有点绕。
更直接的做法:在处理器里生成一个 Java 类,里面包含一个静态方法,返回 Provider 的 authority 列表。然后在主工程的 Application 里动态注册 Provider。嗯,这个方案我实际用过,但有个坑——动态注册的 Provider 在某些国产 ROM 上可能不生效。
我曾经踩过的坑:动态注册 ContentProvider 在 Android 10 以下没问题,但在某些定制 ROM(比如 MIUI、EMUI)上,系统会提前扫描 Manifest 中的静态声明。如果你只动态注册,其他应用可能找不到你的 Provider。所以稳妥起见,还是生成 Manifest 片段比较靠谱。
我现在的做法是:在处理器里生成一个 ProviderManifest.kt 文件,里面用注解标记 authority 和表名。然后在 Gradle 插件里读取这个文件,自动注入到 AndroidManifest 中。这样既不用手动写,又能保证兼容性。
SVG 流程图:注解处理器的完整工作流
实际项目中的取舍
说实话,自动生成 ContentProvider 的方案并不是银弹。我总结了几点经验:
| 场景 | 推荐做法 | 原因 |
|---|---|---|
| 纯本地数据库,不对外暴露 | 不用 Provider,直接用 Room | Provider 有跨进程开销,没必要 |
| 需要对外提供数据 | 注解生成 Provider | 减少模板代码,统一规范 |
| Provider 逻辑复杂(权限校验、数据转换) | 半自动:生成基础代码,手动扩展 | 完全自动可能不够灵活 |
| 多人协作的大型项目 | 注解 + 代码审查 | 确保生成的代码符合团队规范 |
我的建议:如果你用的是 Room,其实 Room 本身已经帮你处理了大部分 Provider 的模板工作。但如果你因为某些原因不能使用 Room(比如遗留项目、或者需要更底层的控制),那注解生成 Provider 是个很好的折中方案。
避坑指南
最后分享几个我实际踩过的坑:
- URI 匹配顺序:UriMatcher 的匹配规则是顺序匹配,所以要把精确匹配(带 ID)的规则放在前面,通配规则放在后面。生成代码时注意顺序。
- 多线程问题:ContentProvider 的方法可能在不同线程调用。生成的代码里,数据库操作要加同步锁,否则会出现并发问题。我早期生成的代码没注意这个,线上出了好几次数据错乱。
- MIME 类型:别忘了生成
getType()方法。很多第三方库(比如 CursorLoader)依赖 MIME 类型来判断数据格式。生成的代码里要自动返回vnd.android.cursor.dir/vnd.<table>或vnd.android.cursor.item/vnd.<table>。 - 表名冲突:如果多个模块定义了同名的表,生成的代码会冲突。我习惯在注解里加一个
module字段,用模块名做前缀来避免冲突。
嗯,关于 ContentProvider 注解自动生成,核心思路就是这些。说白了,就是把那些你不想写的、容易写错的、改起来麻烦的代码,交给机器去生成。你只需要关注业务逻辑和表结构设计,剩下的脏活累活,注解处理器帮你干了。
公众号:蓝海资料掘金营,微信deep3321