目录
你手机里的通讯录,存储了所有联系人的信息。如果你想把这些联系人信息分享给其他App,就可以通过ContentProvider来实现。
一、什么是 ContentProvider
ContentProvider 是 Android 四大组件之一,负责实现跨应用程序的数据共享与访问,通过统一接口封装数据存储细节,提供标准化操作方式。
1. 核心特点
- 数据共享桥梁:提供安全的跨应用数据共享机制,允许不同应用通过 URI(统一资源标识符) 访问数据,无需直接操作对方数据库或文件系统。
- 统一数据接口:独立于底层数据存储形式(SQLite、文件或网络),对外均以表格形式组织数据,简化调用方操作逻辑(query()、insert()、update()、delete()等标准接口)。
- 跨进程通信支持:底层基于 Android Binder 机制实现进程间通信(IPC),确保数据交互高效且安全。
- 数据安全:ContentProvider可以通过权限控制来保护数据。比如,你可以设置只有特定应用才能访问你的数据。
2. 典型使用场景
场景 | 说明 |
---|---|
系统数据访问 | 读取通讯录、短信、媒体文件等系统内置数据。 |
应用间数据共享 | 例如电商应用向物流应用提供订单状态数据。 |
内部数据封装 | 统一管理应用内多模块数据访问逻辑(如本地缓存与网络数据整合)。 |
二、创建和使用 ContentProvider
1. 定义数据模型与 Contract 类
使用 Contract 类 统一管理 URI、表名及列名,提升代码可维护性:
public final class UserContract {
public static final String AUTHORITY = "com.example.provider";
public static final Uri CONTENT_URI
= Uri.parse("content://" + AUTHORITY + "/users");
public static class UserEntry implements BaseColumns {
public static final String TABLE_NAME = "users";
public static final String COLUMN_NAME = "name";
public static final String COLUMN_AGE = "age";
}
}
2. 实现 Provider 类
继承 ContentProvider
并重写关键方法,集成 SQLite 数据库操作( CRUD) :
public class UserProvider extends ContentProvider {
private SQLiteOpenHelper mDbHelper;
@Override
public boolean onCreate() {
mDbHelper = new UserDatabaseHelper(getContext()); // 自定义数据库帮助类
return true; // 初始化数据库等资源,返回 true 表示成功
}
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
SQLiteDatabase db = mDbHelper.getReadableDatabase();
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
qb.setTables(UserEntry.TABLE_NAME);
Cursor cursor = qb.query(db, projection, selection, selectionArgs,
null, null, sortOrder);
cursor.setNotificationUri(getContext().getContentResolver(), uri);
return cursor; // 查询数据,返回 Cursor 对象
}
@Override
public Uri insert(Uri uri, ContentValues values) {
// 插入数据,返回新记录的 URI
}
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
// 更新数据,返回受影响的行数
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
// 删除数据,返回受影响的行数
}
@Override
public String getType(Uri uri) {
// 返回 URI 对应的 MIME 类型
}
}
onCreate()
在主线程执行,要避免耗时操作。可以在onCreate()
中初始化线程池,或在 CRUD 方法中启动AsyncTask
。- 所有 CRUD 方法需处理线程安全问题。
3. 定义 URI 规则
- URI 结构:
content://<Authority>/<Path>/<ID>
- Authority:唯一标识 Provider(需在
AndroidManifest.xml
中声明)。 - Path:标识数据表或资源类型(如
user
、order
)。
- Authority:唯一标识 Provider(需在
- 使用 UriMatcher:根据 URI 区分操作类型(如单条或多条数据)
private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
static {
sUriMatcher.addURI(UserContract.AUTHORITY, "users", USERS);
sUriMatcher.addURI(UserContract.AUTHORITY, "users/#", USER_ID);
}
@Override
public Uri insert(Uri uri, ContentValues values) {
switch (sUriMatcher.match(uri)) {
case USERS:
long id = db.insert(UserEntry.TABLE_NAME, null, values);
return ContentUris.withAppendedId(uri, id);
default:
throw new IllegalArgumentException("Unsupported URI: " + uri);
}
}
4. 注册与权限控制
在 AndroidManifest.xml
中声明provider:
<!-- 定义自定义权限 -->
<permission android:name="com.example.READ_USERS"
android:protectionLevel="dangerous" />
<permission android:name="com.example.WRITE_USERS"
android:protectionLevel="dangerous" />
<!-- 应用权限到 Provider -->
<provider
android:name=".UserProvider" <!-- Provider 实现类的全路径 -->
android:authorities="com.example.provider" <!-- 唯一标识符,与Contract类一致 -->
android:exported="true" <!-- 是否允许其他应用访问(默认 false) -->
android:readPermission="com.example.READ_USERS"
android:writePermission="com.example.WRITE_USERS" />
android:name
:指向继承ContentProvider
的具体实现类(如UserProvider
)。android:authorities
:唯一标识 Provider 的 URI 前缀(如content://com.example.provider/users
)exported
:是否允许其他应用访问(默认为false
)。protectionLevel
设为dangerous
表示需用户手动授权。readPermission
/writePermission
:自定义权限控制。
在 Provider 方法中动态检查权限:
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
if (getContext().checkCallingPermission("com.example.READ_DATA")
!= PackageManager.PERMISSION_GRANTED) {
throw new SecurityException("Permission denied");
}
// 执行查询逻辑
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
if (getContext().checkCallingPermission("com.example.WRITE_USERS")
!= PackageManager.PERMISSION_GRANTED) {
throw new SecurityException("Permission denied");
}
// 删除逻辑
}
注意:静态权限声明需配合动态校验
5. 客户端调用
调用方配置:
<manifest ...>
<!-- 声明权限 -->
<uses-permission android:name="com.example.READ_USERS" />
<uses-permission android:name="com.example.WRITE_USERS" />
<application ...>
<!-- 无 Provider 声明,直接通过 ContentResolver 调用 -->
</application>
</manifest>
通过 ContentResolver
访问和操作 Provider 数据:
// 插入数据
ContentValues values = new ContentValues();
values.put(UserEntry.COLUMN_NAME, "Alice");
values.put(UserEntry.COLUMN_AGE, 25);
getContentResolver().insert(UserContract.CONTENT_URI, values);
// 查询数据
Cursor cursor = getContentResolver().query(
UserContract.CONTENT_URI,
new String[]{UserEntry._ID, UserEntry.COLUMN_NAME},
UserEntry.COLUMN_AGE + " > ?",
new String[]{"20"},
null
);
调用方无需声明
<provider>
,但需通过权限声明和 URI 匹配访问目标 Provider,并遵循 Provider 方的路径与权限规则。
- 客户端无需直接操作 Provider 实例。
- 跨进程时需声明权限。
三、跨应用权限控制
配置目标 | 实现方式 |
---|---|
跨应用调用权限 | 调用方声明 <uses-permission> ,Provider 方配置 android:exported="true" 。 |
动态权限申请 | 针对 dangerous 级别权限,调用方需在运行时请求用户授权。 |
路径级访问控制 | Provider 方通过 <path-permission> 细化权限,调用方需匹配声明。 |
1. 声明 Provider 权限
<!-- 定义自定义权限 -->
<permission android:name="com.example.READ_USERS"
android:protectionLevel="dangerous" />
<permission android:name="com.example.WRITE_USERS"
android:protectionLevel="dangerous" />
<!-- 应用权限到 Provider -->
<provider
android:name=".UserProvider" <!-- Provider 实现类的全路径 -->
android:authorities="com.example.provider" <!-- 唯一标识符,与Contract类一致 -->
android:exported="true" <!-- 是否允许其他应用访问(默认 false) -->
android:readPermission="com.example.READ_USERS"
android:writePermission="com.example.WRITE_USERS" />
protectionLevel
设为dangerous
表示需用户手动授权。readPermission
/writePermission
:自定义权限控制。
2. 路径级权限细化(可选)
若 Provider 方通过 <path-permission>
限制特定路径,调用方需确保拥有对应权限:
<!-- Provider 方配置 -->
<provider ...>
<path-permission
android:pathPrefix="/admin"
android:permission="com.example.ADMIN_PERMISSION" />
</provider>
3. 调用方配置
<manifest ...>
<!-- 声明权限 -->
<uses-permission android:name="com.example.READ_USERS" />
<uses-permission android:name="com.example.WRITE_USERS" />
<!-- 如果存在路径细化,调用方需声明额外权限 -->
<uses-permission android:name="com.example.ADMIN_PERMISSION" />
<application ...>
<!-- 无 Provider 声明,直接通过 ContentResolver 调用 -->
</application>
</manifest>
4. 动态权限申请
在调用方的 Activity/Fragment 中实现动态权限申请流程:
public class MainActivity extends AppCompatActivity {
private static final int REQUEST_READ_PERMISSION = 100;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 检查权限
if (ContextCompat.checkSelfPermission(this, "com.example.READ_USERS")
!= PackageManager.PERMISSION_GRANTED) {
// 权限未授予,显示申请弹窗
ActivityCompat.requestPermissions(this,
new String[]{"com.example.READ_USERS"},
REQUEST_READ_PERMISSION);
} else {
// 已授权,执行数据访问
queryData();
}
}
// 处理权限申请结果
@Override
public void onRequestPermissionsResult(int requestCode,
@NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_READ_PERMISSION) {
if (grantResults.length > 0 && grantResults[0]
== PackageManager.PERMISSION_GRANTED) {
queryData();
} else {
// 权限被拒绝,提示用户
Toast.makeText(this, "权限被拒绝,无法读取数据",
Toast.LENGTH_SHORT).show();
}
}
}
private void queryData() {
// 通过 ContentResolver 访问 Provider 数据
Cursor cursor = getContentResolver().query(
UserContract.CONTENT_URI,
null, null, null, null
);
// 处理查询结果...
}
}
同一权限组内的权限只需申请一次(如
READ_CONTACTS
和WRITE_CONTACTS
属于同一组)
4. 用户拒绝后引导设置
若用户勾选“不再询问”,需引导用户前往系统设置手动开启权限(可通过 shouldShowRequestPermissionRationale
判断)。
if (ActivityCompat.shouldShowRequestPermissionRationale(this,
"com.example.READ_USERS")) {
// 用户之前可能拒绝过权限但未勾选“不再询问”
// 展示解释性弹窗后再次申请
} else {
// 用户勾选“不再询问”或系统禁止权限(如厂商定制 ROM 限制)
// 跳转系统设置界面手动开启权限
Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
intent.setData(Uri.parse("package:" + getPackageName()));
startActivity(intent);
}
若用户从未请求过该权限,
shouldShowRequestPermissionRationale()
也会返回false
。但此时代码通常不会进入此分支(因首次请求时直接调用requestPermissions()
)。部分设备可能不支持
Settings.ACTION_APPLICATION_DETAILS_SETTINGS
,需增加异常捕获并提示用户手动查找权限设置 。
四、数据变更通知
角色 | 职责 |
---|---|
客户端 | 注册 ContentObserver 并实现 onChange 回调逻辑(如刷新 UI)。 |
ContentProvider | 数据变更时调用 notifyChange 触发通知。 |
系统服务 | 通过 ContentService 统一管理观察者,完成消息分发。 |
1. 客户端注册观察者
在使用数据的客户端(如 Activity、Fragment)中,通过 ContentResolver
注册 ContentObserver
,并指定监听的目标 URI,从而实时更新UI。
// 使用者(Activity)通过ContentResolver注册观察者
getContentResolver().registerContentObserver(
UserContract.CONTENT_URI,
true, // 是否监听子 URI
new ContentObserver(new Handler()) {
@Override
public void onChange(boolean selfChange) {
// 数据变化时触发回调
}
}
);
registerContentObserver
是客户端主动调用的方法,用于绑定观察者与目标数据 URI。true
表示监听该 URI 及其所有子路径(如content://com.example.provider/users/
)的数据变更。
2. 提供者触发通知
在 ContentProvider 中,当数据发生变更(如 insert
、update
、delete
)时,需调用 notifyChange
方法触发回调:
// 在 Provider 的 insert/update/delete 方法中
getContext().getContentResolver().notifyChange(uri, null);
notifyChange
会通知所有注册了该 URI 的观察者。- 可通过第二个参数
observer
指定跳过特定观察者(通常设为null
)。
3. 系统级支持
- ContentService:负责管理所有注册的观察者,以树形结构维护 URI 监听关系,实现高效的跨进程通知分发。
- Binder 机制:底层通过 Binder 传递观察者对象(封装为
Transport
代理),确保跨进程通信的可行性。
客户端需主动注册观察者监听 URI,而通知触发由 Provider 发起,两者通过系统服务协同实现实时数据同步。
五、多表关联与视图
场景 | CODE_BOOKS_WITH_AUTHORS (多表关联查询) | CODE_VIEW_BOOKS (视图查询) |
---|---|---|
实现方式 | 运行时动态拼接 SQL JOIN | 预定义视图(静态 SQL) |
灵活性 | 支持动态筛选条件(如 LIKE ) | 筛选条件受视图定义限制 |
性能 | 依赖索引优化 | 视图可预编译优化 |
适用场景 | 需要灵活关联条件的查询 | 高频复杂查询(如报表) |
字段冲突解决 | 需手动设置列别名(AS ) | 视图定义时已解决别名问题 |
1. 定义 Contract 类
public final class BookstoreContract {
public static final String AUTHORITY = "com.example.bookstore.provider";
public static final Uri BASE_URI = Uri.parse("content://" + AUTHORITY);
// 动态 JOIN 查询的 URI 路径
public static class BooksWithAuthors implements BaseColumns {
public static final String PATH = "books_with_authors";
public static final Uri CONTENT_URI
= Uri.withAppendedPath(BASE_URI, PATH);
public static final String CONTENT_TYPE
= "vnd.android.cursor.dir/vnd.bookstore.books_with_authors";
}
// 视图查询的 URI 路径
public static class ViewBooks implements BaseColumns {
public static final String PATH = "view_books";
public static final Uri CONTENT_URI
= Uri.withAppendedPath(BASE_URI, PATH);
public static final String CONTENT_TYPE
= "vnd.android.cursor.dir/vnd.bookstore.view_books";
}
}
2. ContentProvider 实现类
- 多表动态关联查询(
CODE_BOOKS_WITH_AUTHORS
)- 用
SQLiteQueryBuilder
动态构建LEFT JOIN
查询,关联books
表和authors
表。 setTables()
指定关联逻辑,setProjectionMap()
解决字段冲突(如列别名定义)。
- 用
- 视图查询(
CODE_VIEW_BOOKS
)- 预定义视图
view_books_with_authors
,将LEFT JOIN
逻辑固化到数据库中。 - 直接通过视图名称查询数据,简化复杂查询的调用过程。
- 预定义视图
public class BookstoreProvider extends ContentProvider {
private static final String TAG = "BookstoreProvider";
private SQLiteOpenHelper mDbHelper;
// UriMatcher 标识码定义
private static final int CODE_BOOKS_WITH_AUTHORS = 100; // 动态 JOIN 查询
private static final int CODE_VIEW_BOOKS = 200; // 视图查询
private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
static {
sUriMatcher.addURI(
BookstoreContract.AUTHORITY,
BookstoreContract.BooksWithAuthors.PATH,
CODE_BOOKS_WITH_AUTHORS
);
sUriMatcher.addURI(
BookstoreContract.AUTHORITY,
BookstoreContract.ViewBooks.PATH,
CODE_VIEW_BOOKS
);
}
// 列名映射(解决多表字段冲突)
private static final HashMap<String, String> sBooksMap
= new HashMap<>();
static {
sBooksMap.put(BooksWithAuthors._ID, "books._id AS _id");
sBooksMap.put("title", "books.title");
sBooksMap.put("author_name", "authors.name AS author_name");
}
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
SQLiteQueryBuilder queryBuilder = new SQLiteQueryBuilder();
String table = null; // 默认表名
switch (sUriMatcher.match(uri)) {
// 动态 JOIN 查询(CODE_BOOKS_WITH_AUTHORS)
case CODE_BOOKS_WITH_AUTHORS:
queryBuilder.setTables("books LEFT JOIN authors ON books.author_id = authors._id");
queryBuilder.setProjectionMap(sBooksMap);
break;
// 视图查询(CODE_VIEW_BOOKS)
case CODE_VIEW_BOOKS:
table = "view_books_with_authors"; // 直接查询预定义的视图
queryBuilder.setTables(table);
break;
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}
SQLiteDatabase db = mDbHelper.getReadableDatabase();
Cursor cursor;
if (table == null) {
// 动态 JOIN 查询
cursor = queryBuilder.query(
db,
projection,
selection,
selectionArgs,
null, // groupBy
null, // having
sortOrder
);
} else {
// 视图查询(直接使用标准查询)
cursor = db.query(
table,
projection,
selection,
selectionArgs,
null, // groupBy
null, // having
sortOrder
);
}
cursor.setNotificationUri(getContext().getContentResolver(), uri);
return cursor;
}
@Override
public boolean onCreate() {
mDbHelper = new BookstoreDbHelper(getContext());
return true;
}
// 其他必要方法(insert/update/delete/getType)省略...
}
3. 创建数据库视图(SQLiteOpenHelper)
public class BookstoreDbHelper extends SQLiteOpenHelper {
private static final String DATABASE_NAME = "bookstore.db";
private static final int DATABASE_VERSION = 1;
// 基表定义
private static final String SQL_CREATE_BOOKS =
"CREATE TABLE books (" +
"_id INTEGER PRIMARY KEY AUTOINCREMENT," +
"title TEXT NOT NULL," +
"author_id INTEGER," +
"FOREIGN KEY(author_id) REFERENCES authors(_id))";
private static final String SQL_CREATE_AUTHORS =
"CREATE TABLE authors (" +
"_id INTEGER PRIMARY KEY AUTOINCREMENT," +
"name TEXT NOT NULL)";
// 视图定义(固化多表关联逻辑)
private static final String SQL_CREATE_VIEW_BOOKS_WITH_AUTHORS =
"CREATE VIEW view_books_with_authors AS " +
"SELECT books._id, books.title, authors.name AS author_name " +
"FROM books LEFT JOIN authors ON books.author_id = authors._id";
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(SQL_CREATE_AUTHORS);
db.execSQL(SQL_CREATE_BOOKS);
db.execSQL(SQL_CREATE_VIEW_BOOKS_WITH_AUTHORS); // 创建视图
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL("DROP VIEW IF EXISTS view_books_with_authors");
db.execSQL("DROP TABLE IF EXISTS books");
db.execSQL("DROP TABLE IF EXISTS authors");
onCreate(db);
}
}
4. 客户端调用对比
// 动态 JOIN 查询(CODE_BOOKS_WITH_AUTHORS)
Cursor dynamicJoinCursor = getContentResolver().query(
BookstoreContract.BooksWithAuthors.CONTENT_URI,
new String[]{"title", "author_name"},
"author_name LIKE ?",
new String[]{"张%"},
null
);
// 视图查询(CODE_VIEW_BOOKS)
Cursor viewCursor = getContentResolver().query(
BookstoreContract.ViewBooks.CONTENT_URI,
new String[]{"title", "author_name"},
null,
null,
"title ASC"
);
六、异步处理
场景 | 推荐方案 | 优势 | 缺点 |
---|---|---|---|
批量写入 | 线程池 + Handler | 高吞吐量,可控并发度 | 代码复杂度高,回调繁琐 |
列表数据加载 | AsyncQueryHandler | 自动线程管理,简化代码 | Android 11+ 已废弃,推荐用协程 |
实时数据同步 | ContentObserver | 精准监听特定数据源变化 | 频繁更新可能引发过多回调,延迟+消耗 |
1. 异步批量插入(线程池 + Handler)
场景:后台线程执行大数据量插入,避免阻塞 UI 线程(主线程)。
public class BookProvider extends ContentProvider {
private ExecutorService mExecutor = Executors.newFixedThreadPool(4);
private Handler mHandler = new Handler(Looper.getMainLooper());
private SQLiteOpenHelper mDbHelper;
@Override
public boolean onCreate() {
mDbHelper = new BookDbHelper(getContext());
return true;
}
@Override
public int bulkInsert(Uri uri, ContentValues[] values) {
mExecutor.execute(() -> {
SQLiteDatabase db = mDbHelper.getWritableDatabase();
db.beginTransaction();
try {
for (ContentValues value : values) {
db.insert("books", null, value);
}
db.setTransactionSuccessful();
} finally {
db.endTransaction();
// 主线程通知数据变更
mHandler.post(() -> {
getContext().getContentResolver().notifyChange(uri, null);
});
}
});
return values.length; // 返回预计插入数量(实际需回调确认)
}
}
调用示例:
ContentValues[] valuesArray = new ContentValues[100];
// 填充 valuesArray...
ContentResolver resolver = getContentResolver();
resolver.bulkInsert(BookContract.CONTENT_URI, valuesArray);
- 使用线程池管理并发写入
- 通过
Handler
切换至主线程发送变更通知
2. 异步查询(AsyncQueryHandler)
场景:列表页异步加载数据并自动更新 UI。
public class BookListActivity extends AppCompatActivity {
private ListView mListView;
private SimpleCursorAdapter mAdapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_book_list);
mListView = findViewById(R.id.list_view);
// 初始化适配器
mAdapter = new SimpleCursorAdapter(this,
android.R.layout.simple_list_item_2,
null,
new String[]{"title", "author"},
new int[]{android.R.id.text1, android.R.id.text2},
0);
mListView.setAdapter(mAdapter);
// 启动异步查询:
AsyncQueryHandler asyncHandler
= new AsyncQueryHandler(getContentResolver()) {
@Override
protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
mAdapter.swapCursor(cursor);
}
};
asyncHandler.startQuery(0, null, BookContract.CONTENT_URI,
new String[]{"_id", "title", "author"},
"category=?",
new String[]{"tech"},
"rating DESC");
}
}
AsyncQueryHandler
自动管理线程切换- 查询结果直接更新
CursorAdapter
关联的 UI
3. 数据变更监听(ContentObserver)
场景:实时监测收藏夹数据变化并刷新界面。
public class FavoriteFragment extends Fragment {
private FavoriteAdapter mAdapter;
private ContentObserver mObserver;
@Override
public void onResume() {
super.onResume();
// 注册监听器:
mObserver = new ContentObserver(new Handler()) {
@Override
public void onChange(boolean selfChange) {
loadData(); // 数据变化时重新加载
}
};
getActivity().getContentResolver().registerContentObserver(
BookContract.Favorite.CONTENT_URI,
true,
mObserver
);
}
@Override
public void onPause() {
super.onPause();
// 注销监听防止内存泄漏:
getActivity().getContentResolver().unregisterContentObserver(mObserver);
}
private void loadData() {
new AsyncTask<Void, Void, Cursor>() {
@Override
protected Cursor doInBackground(Void... voids) {
return getActivity().getContentResolver().query(
BookContract.Favorite.CONTENT_URI,
null, null, null,
"last_updated DESC"
);
}
@Override
protected void onPostExecute(Cursor cursor) {
mAdapter.swapCursor(cursor);
}
}.execute();
}
}
ContentObserver
监听指定 URI 的数据变化AsyncTask
执行后台查询(注意 Android 11+ 已弃用,建议替换为协程或线程池)