Android 系统为应用间的数据隔离设计了 "沙箱机制"------ 每个应用都有独立的进程和数据目录,默认情况下,一个应用无法直接访问另一个应用的私有数据(如 SQLite 数据库、文件)。但实际开发中,跨应用数据共享是高频需求:比如读取手机联系人、访问相册图片、自定义应用间的数据互通。这时,ContentProvider(内容提供者)就成了 Android 四大组件中专门解决跨进程数据共享的核心组件。
一、什么是ContentProvider
ContentProvider 是 Android 四大组件之一(其余为 Activity、Service、BroadcastReceiver),本质是一套标准化的跨进程数据访问接口。它封装了底层数据存储的逻辑(比如 SQLite 操作、文件读写),对外提供统一的增删改查(CRUD)接口,屏蔽了数据存储的细节 ------ 无论底层是 SQLite、文件还是网络数据,调用者都能用相同的方式访问。
核心特点
- 跨进程通信(IPC):基于 Android 核心的 Binder 机制实现,天然支持不同应用 / 进程间的数据交互;
- 数据封装:调用者无需关心数据的存储位置和方式,只需调用统一接口;
- 权限控制:可精细控制哪些应用能读 / 写数据,保障数据安全;
- 单例特性:每个 ContentProvider 实例全局唯一,多个应用访问时共享同一个实例。
二、核心概念
使用 ContentProvider 前,必须先掌握两个核心概念------URI(数据地址)和 MIME(数据类型)。
URI:ContentProvider 的 "唯一地址"
每个 ContentProvider 都有一个唯一的 URI(统一资源标识符),用于定位要访问的 "数据资源",类似网络请求的 URL。
URI标准格式:
content://<authority>/<path>/<id>
content://:ContentProvider 的固定协议前缀;authority:授权符(通常为应用包名 + 自定义后缀,如com.example.bookprovider),保证 URI 全局唯一;path:数据路径,标识要访问的数据集(如books表示图书表、contacts表示联系人表);id:可选,单条数据的唯一标识(如books/5表示 ID 为 5 的图书)。
MIME:ContentProvider 的 "数据类型描述"
MIME(多用途互联网邮件扩展类型)用于告诉调用者 "返回的数据是什么类型",ContentProvider 需重写 getType(Uri uri) 方法返回对应 MIME 类型。
ContentProvider 专属 MIME 格式
-
多条数据(集合):
vnd.android.cursor.dir/<自定义类型> -
单条数据:
vnd.android.cursor.item/<自定义类型>// 多条图书数据的 MIME
"vnd.android.cursor.dir/vnd.example.book"
// 单条图书数据的 MIME
"vnd.android.cursor.item/vnd.example.book"
三、ContentProvider核心类
ContentProvider 的使用依赖三个核心类,形成 "提供者 - 访问者 - 观察者" 的完整闭环:
ContentProvider(数据提供者)
自定义 ContentProvider 需继承此类,并重写核心方法实现数据的 CRUD 逻辑:
| 方法 | 作用 | 注意事项 |
|---|---|---|
onCreate() |
初始化(如创建数据库),运行在主线程 | 不能做耗时操作 |
query() |
查询数据,返回 Cursor 对象 | Cursor 使用后必须关闭 |
insert() |
插入数据,返回新数据的 URI | 插入成功后需通知观察者 |
update() |
更新数据,返回受影响的行数 | - |
delete() |
删除数据,返回受影响的行数 | - |
getType() |
返回 URI 对应的 MIME 类型 | 必须按规范实现 |
ContentResolver(数据访问者)
普通应用不能直接调用 ContentProvider 的方法,必须通过 ContentResolver 间接访问。Activity/Context 可通过 getContentResolver() 获取实例,其核心方法与 ContentProvider 一一对应:
java
// 查询数据
Cursor cursor = getContentResolver().query(uri, projection, selection, selectionArgs, sortOrder);
// 插入数据
Uri newUri = getContentResolver().insert(uri, contentValues);
// 更新数据
int updateCount = getContentResolver().update(uri, contentValues, selection, selectionArgs);
// 删除数据
int deleteCount = getContentResolver().delete(uri, selection, selectionArgs);
ContentObserver(数据观察者)
用于监听 ContentProvider 中的数据变化,实现 "数据变化 → UI 同步" 的联动。使用步骤:
- 自定义观察者,重写
onChange()方法; - 注册观察者;
- 数据变化时触发通知;
- 销毁时注销观察者。
java
// 1. 自定义观察者
ContentObserver observer = new ContentObserver(new Handler(Looper.getMainLooper())) {
@Override
public void onChange(boolean selfChange) {
super.onChange(selfChange);
// 数据变化时重新查询
queryBooks();
}
};
// 2. 注册观察者(true 表示监听子 URI)
getContentResolver().registerContentObserver(bookUri, true, observer);
// 3. ContentProvider 中触发通知(插入/更新/删除后调用)
getContext().getContentResolver().notifyChange(uri, null);
// 4. 注销观察者(避免内存泄漏)
@Override
protected void onDestroy() {
super.onDestroy();
getContentResolver().unregisterContentObserver(observer);
}
四、示例
访问系统 ContentProvider(读取手机联系人)
系统内置了大量 ContentProvider(联系人、短信、相册等),以读取联系人为例:
步骤 1:添加权限(AndroidManifest.xml)
XML
<!-- 静态权限声明 -->
<uses-permission android:name="android.permission.READ_CONTACTS" />
步骤 2:动态申请权限 + 读取联系人
java
public class ContactReaderActivity extends AppCompatActivity {
private static final int REQUEST_READ_CONTACTS = 100;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 6.0+ 动态申请权限
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_READ_CONTACTS);
} else {
readContacts();
}
}
// 读取联系人核心逻辑
private void readContacts() {
// 联系人 ContentProvider 的 URI
Uri contactUri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI;
// 要查询的字段:姓名、手机号
String[] projection = {
ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,
ContactsContract.CommonDataKinds.Phone.NUMBER
};
// 查询数据
Cursor cursor = getContentResolver().query(contactUri, projection, null, null, null);
if (cursor != null) {
while (cursor.moveToNext()) {
String name = cursor.getString(0);
String phone = cursor.getString(1);
Log.d("Contact", "姓名:" + name + ",手机号:" + phone);
}
cursor.close(); // 必须关闭 Cursor
}
}
// 权限申请回调
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == REQUEST_READ_CONTACTS) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
readContacts();
} else {
Toast.makeText(this, "拒绝权限无法读取联系人", Toast.LENGTH_SHORT).show();
}
}
}
}
自定义 ContentProvider(跨应用共享图书数据)
假设开发 "图书管理" 应用,对外提供图书数据的增删改查接口:
步骤 1:封装 SQLite 数据库
java
public class BookDBHelper extends SQLiteOpenHelper {
private static final String DB_NAME = "BookDB";
private static final int DB_VERSION = 1;
// 图书表(必须包含 _id 字段,Cursor 适配器依赖)
public static final String TABLE_BOOK = "books";
public static final String COLUMN_ID = "_id";
public static final String COLUMN_NAME = "name";
public static final String COLUMN_PRICE = "price";
// 创建表 SQL
private static final String CREATE_TABLE = "CREATE TABLE " + TABLE_BOOK + " (" +
COLUMN_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
COLUMN_NAME + " TEXT, " +
COLUMN_PRICE + " REAL)";
public BookDBHelper(Context context) {
super(context, DB_NAME, null, DB_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(CREATE_TABLE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
db.execSQL("DROP TABLE IF EXISTS " + TABLE_BOOK);
onCreate(db);
}
}
步骤2:自定义 ContentProvider
java
public class BookContentProvider extends ContentProvider {
// 1. 定义唯一 Authority
public static final String AUTHORITY = "com.example.bookprovider";
// 2. 定义 URI 匹配码
private static final int CODE_BOOK_ALL = 1;
private static final int CODE_BOOK_SINGLE = 2;
// 3. 初始化 UriMatcher
private static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
static {
URI_MATCHER.addURI(AUTHORITY, "books", CODE_BOOK_ALL);
URI_MATCHER.addURI(AUTHORITY, "books/#", CODE_BOOK_SINGLE);
}
private BookDBHelper dbHelper;
private SQLiteDatabase db;
@Override
public boolean onCreate() {
dbHelper = new BookDBHelper(getContext());
db = dbHelper.getWritableDatabase();
return true;
}
// 查询数据
@Nullable
@Override
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection,
@Nullable String[] selectionArgs, @Nullable String sortOrder) {
Cursor cursor = null;
switch (URI_MATCHER.match(uri)) {
case CODE_BOOK_ALL:
// 查询所有图书
cursor = db.query(TABLE_BOOK, projection, selection, selectionArgs, null, null, sortOrder);
break;
case CODE_BOOK_SINGLE:
// 查询单本图书(提取 ID)
long id = ContentUris.parseId(uri);
cursor = db.query(TABLE_BOOK, projection, COLUMN_ID + "=?",
new String[]{String.valueOf(id)}, null, null, sortOrder);
break;
default:
throw new IllegalArgumentException("未知 URI:" + uri);
}
// 设置通知 URI,数据变化时触发观察者
if (cursor != null) {
cursor.setNotificationUri(getContext().getContentResolver(), uri);
}
return cursor;
}
// 返回 MIME 类型
@Nullable
@Override
public String getType(@NonNull Uri uri) {
switch (URI_MATCHER.match(uri)) {
case CODE_BOOK_ALL:
return "vnd.android.cursor.dir/vnd.example.book";
case CODE_BOOK_SINGLE:
return "vnd.android.cursor.item/vnd.example.book";
default:
return null;
}
}
// 插入数据
@Nullable
@Override
public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
Uri newUri = null;
if (URI_MATCHER.match(uri) == CODE_BOOK_ALL) {
long id = db.insert(TABLE_BOOK, null, values);
newUri = ContentUris.withAppendedId(uri, id);
// 通知数据变化
getContext().getContentResolver().notifyChange(uri, null);
} else {
throw new IllegalArgumentException("不支持的 URI:" + uri);
}
return newUri;
}
// 删除数据
@Override
public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
int deleteCount = 0;
switch (URI_MATCHER.match(uri)) {
case CODE_BOOK_ALL:
deleteCount = db.delete(TABLE_BOOK, selection, selectionArgs);
break;
case CODE_BOOK_SINGLE:
long id = ContentUris.parseId(uri);
deleteCount = db.delete(TABLE_BOOK, COLUMN_ID + "=?", new String[]{String.valueOf(id)});
break;
default:
throw new IllegalArgumentException("未知 URI:" + uri);
}
if (deleteCount > 0) {
getContext().getContentResolver().notifyChange(uri, null);
}
return deleteCount;
}
// 更新数据
@Override
public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection,
@Nullable String[] selectionArgs) {
int updateCount = 0;
switch (URI_MATCHER.match(uri)) {
case CODE_BOOK_ALL:
updateCount = db.update(TABLE_BOOK, values, selection, selectionArgs);
break;
case CODE_BOOK_SINGLE:
long id = ContentUris.parseId(uri);
updateCount = db.update(TABLE_BOOK, values, COLUMN_ID + "=?", new String[]{String.valueOf(id)});
break;
default:
throw new IllegalArgumentException("未知 URI:" + uri);
}
if (updateCount > 0) {
getContext().getContentResolver().notifyChange(uri, null);
}
return updateCount;
}
}
步骤 3:注册 ContentProvider(AndroidManifest.xml)
XML
<provider
android:name=".BookContentProvider"
android:authorities="com.example.bookprovider"
android:exported="true" <!-- 允许其他应用访问 -->
android:readPermission="com.example.bookprovider.READ_BOOK" <!-- 读权限 -->
android:writePermission="com.example.bookprovider.WRITE_BOOK" /> <!-- 写权限 -->
<!-- 自定义权限(可选) -->
<permission
android:name="com.example.bookprovider.READ_BOOK"
android:protectionLevel="normal" />
<permission
android:name="com.example.bookprovider.WRITE_BOOK"
android:protectionLevel="normal" />
步骤 4:其他应用访问该 ContentProvider
java
// 插入图书
Uri bookUri = Uri.parse("content://com.example.bookprovider/books");
ContentValues values = new ContentValues();
values.put("name", "Android开发艺术探索");
values.put("price", 69.9);
Uri newUri = getContentResolver().insert(bookUri, values);
// 查询图书
Cursor cursor = getContentResolver().query(bookUri, null, null, null, null);
if (cursor != null) {
while (cursor.moveToNext()) {
String name = cursor.getString(cursor.getColumnIndex("name"));
double price = cursor.getDouble(cursor.getColumnIndex("price"));
Log.d("BookClient", "图书:" + name + ",价格:" + price);
}
cursor.close();
}
ContentProvider 的跨进程能力源于 Android 的 Binder 机制,其核心流程:
- 调用者通过
ContentResolver发起 CRUD 请求; ContentResolver将请求封装为 Binder 调用,发送给系统的ContentService;ContentService根据 URI 的authority找到对应的 ContentProvider 实例;- ContentProvider 执行具体的数据库 / 文件操作,返回结果;
- 结果通过 Binder 回传给
ContentResolver,最终到调用者手中。