Android随笔-ContentProvider

Android ContentProvider 详解

一、ContentProvider 是什么

ContentProvider 是 Android 四大组件之一,用于跨应用数据共享统一的数据访问接口。它封装了底层数据存储(SQLite、文件、网络等),对外提供标准化的 CRUD 操作。

核心定位:

  • 数据共享:不同应用之间安全地共享数据
  • 统一接口 :对外提供标准化的 insert / query / update / delete / getType 接口
  • 权限控制:通过 URI 和权限机制控制数据访问粒度
  • 底层透明:调用方无需关心数据实际存储方式(SQLite、文件、网络等)

注意:如果仅在应用内部使用数据,直接使用 SQLiteOpenHelper 或 Room 即可,无需 ContentProvider。


二、核心原理

架构设计

复制代码
应用A(数据提供方)                    应用B(数据消费方)
    ↓                                      ↓
ContentProvider(封装数据)            ContentResolver(统一入口)
    ↓                                      ↓
SQLite / File / Network                ContentResolver.query/insert/update/delete
    ↓                                      ↓
底层数据存储                            通过 URI 定位 + 权限校验

URI 格式

复制代码
content://com.example.provider/user/1
│      │                    │    │
│      │                    │    └── 具体记录 ID(可选)
│      │                    └────── 表名 / 路径
│      └─────────────────────────── 授权标识(Authority)
└────────────────────────────────── 协议前缀(固定为 content://)

核心类

复制代码
Context
    ↓
getContentResolver()
    ↓
ContentResolver(抽象类,应用层统一入口)
    ↓
ApplicationContentResolver(ContextImpl 内部类)
    ↓
ContentProviderProxy(Binder 代理,位于调用方进程)
    ↓
【Binder 跨进程】
    ↓
ContentProviderNative / ContentProvider.Transport(Binder 实体,位于提供方进程)
    ↓
ContentProvider(抽象基类,用户继承实现)
    ↓
具体 Provider 实现(如 UserProvider)

三、与 SQLite 的区别

特性 ContentProvider SQLiteOpenHelper / Room
使用范围 跨应用数据共享 应用内部数据存储
访问方式 通过 URI + ContentResolver 直接调用 DAO / SQL
权限控制 ✅ 支持细粒度权限控制 ❌ 无
实现复杂度 高(需继承 ContentProvider)
性能 有 IPC 开销 直接访问,性能更好
线程安全 系统管理 需自行管理
适用场景 通讯录、媒体库、日历等系统数据 应用私有数据

四、完整使用示例

1. 定义 Contract(契约类)

java 复制代码
public class UserContract {

    // Authority
    public static final String AUTHORITY = "com.example.provider";

    // URI 基础
    public static final Uri BASE_CONTENT_URI = Uri.parse("content://" + AUTHORITY);

    // 表路径
    public static final String PATH_USERS = "users";

    // 完整 URI
    public static final Uri CONTENT_URI = BASE_CONTENT_URI.buildUpon()
            .appendPath(PATH_USERS)
            .build();

    // MIME 类型
    public static final String CONTENT_TYPE = 
            "vnd.android.cursor.dir/vnd.com.example.users";
    public static final String CONTENT_ITEM_TYPE = 
            "vnd.android.cursor.item/vnd.com.example.users";

    // 表字段
    public static class Columns {
        public static final String _ID = BaseColumns._ID;
        public static final String NAME = "name";
        public static final String AGE = "age";
        public static final String EMAIL = "email";
    }
}

2. 实现 ContentProvider

java 复制代码
public class UserProvider extends ContentProvider {

    private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
    private static final int USERS = 100;      // 多行
    private static final int USER_ID = 101;    // 单行

    static {
        sUriMatcher.addURI(UserContract.AUTHORITY, "users", USERS);
        sUriMatcher.addURI(UserContract.AUTHORITY, "users/#", USER_ID);
    }

    private UserDbHelper mDbHelper;

    @Override
    public boolean onCreate() {
        mDbHelper = new UserDbHelper(getContext());
        return true;
    }

    @Nullable
    @Override
    public Cursor query(@NonNull Uri uri, @Nullable String[] projection,
                       @Nullable String selection, @Nullable String[] selectionArgs,
                       @Nullable String sortOrder) {
        SQLiteDatabase db = mDbHelper.getReadableDatabase();
        Cursor cursor;

        switch (sUriMatcher.match(uri)) {
            case USERS:
                cursor = db.query("users", projection, selection, selectionArgs,
                        null, null, sortOrder);
                break;
            case USER_ID:
                long id = ContentUris.parseId(uri);
                cursor = db.query("users", projection, 
                        "_id=?", new String[]{String.valueOf(id)},
                        null, null, sortOrder);
                break;
            default:
                throw new IllegalArgumentException("Unknown URI: " + uri);
        }

        // 注册内容观察者,数据变化时自动通知
        cursor.setNotificationUri(getContext().getContentResolver(), uri);
        return cursor;
    }

    @Nullable
    @Override
    public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
        SQLiteDatabase db = mDbHelper.getWritableDatabase();
        long id;

        switch (sUriMatcher.match(uri)) {
            case USERS:
                id = db.insert("users", null, values);
                break;
            default:
                throw new IllegalArgumentException("Unknown URI: " + uri);
        }

        if (id > 0) {
            Uri resultUri = ContentUris.withAppendedId(uri, id);
            // 通知观察者数据变化
            getContext().getContentResolver().notifyChange(resultUri, null);
            return resultUri;
        }
        throw new SQLException("Failed to insert row into " + uri);
    }

    @Override
    public int delete(@NonNull Uri uri, @Nullable String selection,
                     @Nullable String[] selectionArgs) {
        SQLiteDatabase db = mDbHelper.getWritableDatabase();
        int rows;

        switch (sUriMatcher.match(uri)) {
            case USERS:
                rows = db.delete("users", selection, selectionArgs);
                break;
            case USER_ID:
                long id = ContentUris.parseId(uri);
                rows = db.delete("users", "_id=?", 
                        new String[]{String.valueOf(id)});
                break;
            default:
                throw new IllegalArgumentException("Unknown URI: " + uri);
        }

        if (rows > 0) {
            getContext().getContentResolver().notifyChange(uri, null);
        }
        return rows;
    }

    @Override
    public int update(@NonNull Uri uri, @Nullable ContentValues values,
                     @Nullable String selection, @Nullable String[] selectionArgs) {
        SQLiteDatabase db = mDbHelper.getWritableDatabase();
        int rows;

        switch (sUriMatcher.match(uri)) {
            case USERS:
                rows = db.update("users", values, selection, selectionArgs);
                break;
            case USER_ID:
                long id = ContentUris.parseId(uri);
                rows = db.update("users", values, "_id=?",
                        new String[]{String.valueOf(id)});
                break;
            default:
                throw new IllegalArgumentException("Unknown URI: " + uri);
        }

        if (rows > 0) {
            getContext().getContentResolver().notifyChange(uri, null);
        }
        return rows;
    }

    @Nullable
    @Override
    public String getType(@NonNull Uri uri) {
        switch (sUriMatcher.match(uri)) {
            case USERS:
                return UserContract.CONTENT_TYPE;
            case USER_ID:
                return UserContract.CONTENT_ITEM_TYPE;
            default:
                throw new IllegalArgumentException("Unknown URI: " + uri);
        }
    }
}

3. SQLite 数据库辅助类

java 复制代码
public class UserDbHelper extends SQLiteOpenHelper {

    private static final String DATABASE_NAME = "users.db";
    private static final int DATABASE_VERSION = 1;

    public UserDbHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL("CREATE TABLE users (" +
                "_id INTEGER PRIMARY KEY AUTOINCREMENT," +
                "name TEXT NOT NULL," +
                "age INTEGER," +
                "email TEXT)");
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        db.execSQL("DROP TABLE IF EXISTS users");
        onCreate(db);
    }
}

4. AndroidManifest 注册

xml 复制代码
<provider
    android:name=".provider.UserProvider"
    android:authorities="com.example.provider"
    android:exported="true"
    android:readPermission="com.example.provider.READ_USER"
    android:writePermission="com.example.provider.WRITE_USER"
    android:grantUriPermissions="true" />

5. 其他应用访问(消费方)

java 复制代码
// 查询
public void queryUsers(Context context) {
    Cursor cursor = context.getContentResolver().query(
            UserContract.CONTENT_URI,
            new String[]{UserContract.Columns._ID, 
                        UserContract.Columns.NAME, 
                        UserContract.Columns.AGE},
            null, null, null);

    if (cursor != null) {
        while (cursor.moveToNext()) {
            long id = cursor.getLong(cursor.getColumnIndex(UserContract.Columns._ID));
            String name = cursor.getString(cursor.getColumnIndex(UserContract.Columns.NAME));
            int age = cursor.getInt(cursor.getColumnIndex(UserContract.Columns.AGE));
            Log.d("User", "id=" + id + ", name=" + name + ", age=" + age);
        }
        cursor.close();
    }
}

// 插入
public void insertUser(Context context, String name, int age, String email) {
    ContentValues values = new ContentValues();
    values.put(UserContract.Columns.NAME, name);
    values.put(UserContract.Columns.AGE, age);
    values.put(UserContract.Columns.EMAIL, email);

    Uri uri = context.getContentResolver().insert(UserContract.CONTENT_URI, values);
    Log.d("User", "Inserted: " + uri);
}

// 更新
public void updateUser(Context context, long id, String newName) {
    ContentValues values = new ContentValues();
    values.put(UserContract.Columns.NAME, newName);

    Uri uri = ContentUris.withAppendedId(UserContract.CONTENT_URI, id);
    int rows = context.getContentResolver().update(uri, values, null, null);
    Log.d("User", "Updated " + rows + " rows");
}

// 删除
public void deleteUser(Context context, long id) {
    Uri uri = ContentUris.withAppendedId(UserContract.CONTENT_URI, id);
    int rows = context.getContentResolver().delete(uri, null, null);
    Log.d("User", "Deleted " + rows + " rows");
}

6. 注册内容观察者(监听数据变化)

java 复制代码
public class UserObserver extends ContentObserver {

    public UserObserver(Handler handler) {
        super(handler);
    }

    @Override
    public void onChange(boolean selfChange, Uri uri) {
        super.onChange(selfChange, uri);
        Log.d("UserObserver", "Data changed: " + uri);
        // 刷新 UI 或重新查询
    }
}

// 注册
UserObserver observer = new UserObserver(new Handler(Looper.getMainLooper()));
getContentResolver().registerContentObserver(
    UserContract.CONTENT_URI, 
    true,  // true = 监听子 URI 变化
    observer
);

// 注销
getContentResolver().unregisterContentObserver(observer);

五、系统 ContentProvider

Android 系统提供了大量内置 ContentProvider:

Provider Authority 用途
Contacts com.android.contacts 联系人数据
MediaStore media 图片、音频、视频
Calendar com.android.calendar 日历事件
CallLog com.android.calllog 通话记录
Browser com.android.browser 浏览器书签/历史
Settings settings 系统设置
Downloads downloads 下载管理

读取系统联系人示例

java 复制代码
// 需要权限:READ_CONTACTS
Cursor cursor = getContentResolver().query(
    ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
    new String[]{
        ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,
        ContactsContract.CommonDataKinds.Phone.NUMBER
    },
    null, null, null
);

if (cursor != null) {
    while (cursor.moveToNext()) {
        String name = cursor.getString(0);
        String number = cursor.getString(1);
        Log.d("Contact", name + ": " + number);
    }
    cursor.close();
}

六、注意事项

1. 权限声明

xml 复制代码
<!-- 提供方声明权限 -->
<permission android:name="com.example.provider.READ_USER"
    android:protectionLevel="normal" />
<permission android:name="com.example.provider.WRITE_USER"
    android:protectionLevel="normal" />

<!-- 消费方申请权限 -->
<uses-permission android:name="com.example.provider.READ_USER" />
<uses-permission android:name="com.example.provider.WRITE_USER" />

2. 线程安全

java 复制代码
// ⚠️ ContentProvider 的 CRUD 方法可能被多线程同时调用
// 需确保 SQLiteDatabase 操作线程安全

// ✅ 使用 synchronized 或 SQLiteDatabase 自带锁
@Override
public Cursor query(Uri uri, String[] projection, String selection,
                   String[] selectionArgs, String sortOrder) {
    synchronized (this) {
        // 数据库操作
    }
}

3. 性能优化

java 复制代码
// ⚠️ 每次 IPC 有性能开销,避免频繁小数据量操作
// ✅ 批量操作使用 ContentProviderOperation

ArrayList<ContentProviderOperation> operations = new ArrayList<>();
for (User user : users) {
    operations.add(ContentProviderOperation.newInsert(UserContract.CONTENT_URI)
        .withValue(UserContract.Columns.NAME, user.name)
        .withValue(UserContract.Columns.AGE, user.age)
        .build());
}
getContentResolver().applyBatch(UserContract.AUTHORITY, operations);

4. 数据变化通知

java 复制代码
// ✅ 数据修改后必须通知观察者,否则消费方无法感知变化
getContext().getContentResolver().notifyChange(uri, null);

// 消费方注册观察者监听变化
getContentResolver().registerContentObserver(uri, true, observer);

5. Android 11+ 包可见性

xml 复制代码
<!-- Android 11+ 需要声明要访问的其他应用 -->
<queries>
    <provider android:authorities="com.example.provider" />
</queries>

6. 不要滥用 ContentProvider

java 复制代码
// ❌ 错误:应用内部数据也用 ContentProvider
// 应用内部直接使用 Room / SQLiteOpenHelper 即可

// ✅ 正确:仅在需要跨应用共享数据时使用 ContentProvider

七、总结

问题 答案
ContentProvider 是什么? Android 跨应用数据共享组件,封装底层存储
核心原理? 通过 URI 定位数据,AMS 调度,IPC 通信
与 SQLite 区别? ContentProvider 是封装层,SQLite 是存储层;Provider 支持跨应用
适用场景? 通讯录、媒体库等系统数据;应用间数据共享
现代替代方案? 应用内部用 Room;跨应用用 FileProvider / 自定义 Provider
注意事项? 权限控制、线程安全、IPC 性能开销、数据变化通知

一句话总结:ContentProvider 是 Android 的"数据共享网关",通过 URI 和权限机制实现安全跨应用数据访问;应用内部数据管理优先使用 Room,仅在需要跨应用共享时才使用 ContentProvider。

层级 实现机制
应用层 ContentResolver 统一入口,封装底层细节
Framework ApplicationContentResolver → ActivityThread 缓存管理
系统服务 AMS 统一调度 Provider 启动、权限校验、观察者管理
IPC 机制 Binder 跨进程调用,CursorWindow 共享内存零拷贝传输
线程模型 onCreate 主线程,CRUD 默认 Binder 线程池
权限控制 AMS checkComponentPermission,基于 UID 校验

另一句话总结 :ContentProvider 的底层本质是 Binder IPC + 共享内存 + AMS 进程调度,调用方通过 ContentResolver 获取 Binder 代理,跨进程调用 Provider 的 CRUD 方法;Cursor 数据通过 ashmem 共享内存实现零拷贝传输,权限校验由 AMS 在调用前统一拦截。

相关推荐
心中有国也有家17 小时前
AtomGit Flutter 鸿蒙客户端:Canvas 绘制进阶-路径、渐变与混合模式
android·javascript·flutter·华为·harmonyos
hunterandroid18 小时前
WorkManager 可靠性实战:唯一任务、重试与幂等设计
android·前端
程序员老刘18 小时前
Android 17的10个AI功能,国内用户真正能用的有几个?
android·flutter·ai编程
花开彼岸天~19 小时前
鸿蒙应用开发实战【94】— 实战多卡号场景管理最佳实践
android·华为·harmonyos·鸿蒙系统
用户416596736935519 小时前
视频源文件音频轨异常分析记录
android
我命由我1234520 小时前
集成 mupdf-android 的示例做为模块使用报错,程序包android.support.v4.view不存在
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
雨白1 天前
嵌套 ScrollView 滑动冲突实战:实现内部优先滚动
android
阿pin1 天前
Android随笔-IntentService详解(了解)
android·intentservice
海天鹰1 天前
content://com.android.externalstorage.documents/document/primary%3A
android
阿pin1 天前
Android随笔-WorkManager 与 AlarmManager
android·alarmmanager·workmanager