Android ContentProvider 使用与源码详解

148 阅读20分钟

核心概念:数据共享的“前台服务员”

想象一下,每个 Android 应用就像一家独立的商店,商店内部有自己的仓库(应用私有数据)。为了保护隐私和安全,Android 系统给每个商店(应用)都装了坚固的围墙(沙箱机制),禁止其他商店直接翻墙进来拿东西。

现在,假设你开了家“通讯录商店”(Contacts App),里面存了所有人的联系方式(联系人数据库)。其他商店(比如短信App、邮件App)想合法地获取这些联系方式来发短信或邮件,该怎么办?

ContentProvider (CP) 就是这个解决方案!它相当于商店专门设置的“前台服务员”:

  1. 职责明确:  它只负责提供(Provide)商店(应用)内部特定数据(如联系人、短信、相册、自定义数据库等)的访问接口(Content)。
  2. 统一窗口:  它定义了一套标准的访问方式(增删改查,即 CRUD),任何其他应用(客户端)只要按照这个标准来“窗口”请求,就能获取或修改数据。
  3. 权限管控:  服务员不会随便把数据给任何人。它严格执行店规(权限声明)。其他商店(客户端应用)必须先在“营业执照”(AndroidManifest.xml)里声明自己获得了相应的权限(如 READ_CONTACTS),服务员才会提供服务。
  4. 抽象接口:  服务员只关心“你要什么数据”(通过 URI 指定)和“你要做什么操作”(增删改查)。至于数据是存在 SQLite 数据库、文件、还是网络,服务员后面(即 CP 的实现者)自己处理,客户端完全不用关心。
  5. 跨应用桥梁:  最重要的是,这个服务员机制是 Android 系统提供的跨应用通信(IPC)  的核心方式之一。它让数据可以在不同应用间安全、受控地流动。

关键概念 1:URI - 数据的“门牌号”

  • 是什么?  统一资源标识符 (Uniform Resource Identifier)。

  • 干什么用?  唯一标识 ContentProvider 所管理的特定数据集或单条数据。就像你要去图书馆找一本书,需要知道书名和书架位置(URI)。

  • 标准格式:
    content://<authority>/<path>/<id>

    • content://  固定前缀,表明这是一个指向 ContentProvider 的 URI。
    • <authority>  最重要的部分!  相当于 ContentProvider 的全局唯一域名。它告诉系统:“我要找哪个服务员?”。通常使用应用的包名com.example.app)或包名加特定后缀(com.example.app.provider)来确保唯一性。必须在 Provider 的 AndroidManifest.xml 声明中精确匹配!
    • <path>  (可选)标识数据的类型或集合。例如,contacts 表示联系人集合,photos 表示照片集合。可以有层级,如 user/123
    • <id>  (可选)标识集合中的单条特定数据。通常是一个数字 ID。例如,content://com.android.contacts/contacts/5 表示 ID 为 5 的联系人。
  • 例子:

    • content://com.android.contacts/contacts - 指向系统联系人应用提供的所有联系人的集合。
    • content://com.android.contacts/contacts/5 - 指向 ID 为 5 的那个特定联系人。
    • content://com.example.myapp.provider/books - 指向你的应用 (com.example.myapp) 提供的名为 myapp.provider 的 CP 所管理的所有书籍的集合。
    • content://com.example.myapp.provider/books/42 - 指向 ID 为 42 的那本特定书籍。

关键概念 2:ContentResolver - 客户的“请求代理”

  • 是什么?  应用中的一个核心类 (android.content.ContentResolver)。

  • 干什么用?  客户端应用(想访问数据的那一方)并不直接创建或访问 ContentProvider 对象。相反,它通过 ContentResolver 这个系统提供的统一代理来发送请求。

  • 工作原理:

    1. 你在客户端代码中获取 ContentResolver 实例(通过 context.getContentResolver())。
    2. 你调用 ContentResolver 的方法 (insert()query()update()delete()),并传入目标数据的 URI 和其他操作参数(如要查询的列、查询条件、要插入的值等)。
    3. ContentResolver 拿着这个 URI,去找系统(主要是 ActivityManagerService)问:“嘿,系统大哥,这个 URI (content://com.android.contacts/contacts) 是哪个服务员 (Authority) 管的呀?把那个服务员的联系方式(一个代理对象)给我呗”。
    4. 系统找到对应的 ContentProvider(如果还没启动,可能会先启动它所在的进程),并返回一个可以跟那个服务员通信的“电话线”(通常是基于 Binder 的 IPC 代理)。
    5. ContentResolver 通过这个“电话线”把你的请求转发给真正的 ContentProvider 实例。
    6. ContentProvider 在自己的应用进程里执行实际的数据库/文件操作。
    7. 操作结果(如新插入数据的 URI、查询到的 Cursor、受影响的行数)再通过“电话线”和 ContentResolver 返回给你的客户端代码。
  • 好处:  对客户端完全透明地处理了跨进程通信的复杂性。客户端只需要和 ContentResolver 打交道,感觉就像在访问本地数据一样简单(虽然背后可能是跨进程的)。

如何使用 ContentProvider(客户端视角)

假设你想查询系统联系人:

  1. 声明权限 (AndroidManifest.xml):

    <manifest ...>
        <uses-permission android:name="android.permission.READ_CONTACTS" /> <!-- 查询需要读权限 -->
        <application ...>
            ...
        </application>
    </manifest>
    

    运行时还需要动态申请(针对 Android 6.0+)。

  2. 获取 ContentResolver:

    ContentResolver resolver = getContentResolver(); // 在 Activity, Service 等 Context 中调用
    
  3. 构建查询 URI:  指向联系人的集合。

    Uri contactUri = ContactsContract.Contacts.CONTENT_URI; // 系统预定义好的联系人 URI
    
  4. 指定查询参数 (可选):

    • Projection:  要查询哪些列(字段)?如 new String[]{ContactsContract.Contacts._ID, ContactsContract.Contacts.DISPLAY_NAME}
    • Selection:  查询条件 (WHERE 子句),如 ContactsContract.Contacts.DISPLAY_NAME + " = ?"
    • SelectionArgs:  填充 Selection 中 ? 占位符的参数值,如 new String[]{"Alice"}
    • SortOrder:  排序方式,如 ContactsContract.Contacts.DISPLAY_NAME + " ASC"
  5. 执行查询 (query()):

    Cursor cursor = resolver.query(
            contactUri,           // URI
            projection,          // 要查询的列
            selection,           // WHERE 条件
            selectionArgs,       // WHERE 条件的参数
            sortOrder            // 排序
    );
    
  6. 处理查询结果 (Cursor):

    if (cursor != null && cursor.moveToFirst()) {
        do {
            long id = cursor.getLong(cursor.getColumnIndex(ContactsContract.Contacts._ID));
            String name = cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME));
            // ... 使用 id 和 name ...
        } while (cursor.moveToNext());
        cursor.close(); // 非常重要!用完必须关闭
    }
    
  7. 其他操作类似 (insert, update, delete):

    • 插入 (insert()):  传入目标集合 URI 和包含数据的 ContentValues 对象。返回新插入项的 URI。
    • 更新 (update()):  传入目标 URI (可以是单条或集合),包含更新数据的 ContentValues,以及可选的 selection 和 selectionArgs。返回受影响的行数。
    • 删除 (delete()):  传入目标 URI (可以是单条或集合),以及可选的 selection 和 selectionArgs。返回删除的行数。

关键概念 3:Cursor - 数据的“临时集装箱”

  • 是什么?  一个接口 (android.database.Cursor),代表查询结果的集合。

  • 干什么用?  当你执行 query() 操作时,结果不是一次性全部加载到内存,而是通过 Cursor 这个“迭代器”来按需访问。它封装了结果集的游标位置、列信息、数据值。

  • 核心方法:

    • moveToFirst()moveToNext()moveToPrevious()moveToPosition() - 移动游标。
    • isBeforeFirst()isAfterLast()isFirst()isLast()getPosition() - 判断游标位置。
    • getCount() - 获取结果总数。
    • getColumnIndex()getColumnName() - 列信息。
    • getString()getInt()getLong()getFloat()getBlob() - 按列索引或列名获取当前行的数据。
  • 重要!必须关闭:  Cursor 通常关联着底层资源(数据库连接、文件句柄等)。用完一定要调用 cursor.close() 来释放资源!  否则会导致内存泄漏和资源耗尽。推荐使用 try-with-resources (Java 7+) 或在 finally 块中关闭。

关键概念 4:MIME 类型 - 数据的“身份证”

  • 是什么?  类似互联网上的 MIME 类型 (如 text/htmlimage/jpeg)。

  • 干什么用?  ContentProvider 可以通过 getType(Uri uri) 方法返回给定 URI 所代表数据的 MIME 类型。这有助于客户端理解数据的格式(是单条联系人?还是联系人列表?是图片?还是视频?)。

  • 标准格式:

    • 指向集合的 URI: vnd.android.cursor.dir/vnd.<authority>.<path> 例如:vnd.android.cursor.dir/vnd.com.example.provider.books
    • 指向单条数据的 URI: vnd.android.cursor.item/vnd.<authority>.<path> 例如:vnd.android.cursor.item/vnd.com.example.provider.book

如何创建自己的 ContentProvider(提供者视角)

假设你的应用 (com.example.myapp) 想提供一个管理书籍的 CP:

  1. 定义 Contract 类 (推荐):

    public final class BookContract {
        // 防止实例化
        private BookContract() {}
        // Authority (唯一标识符)
        public static final String CONTENT_AUTHORITY = "com.example.myapp.provider";
        // Base URI: content://com.example.myapp.provider
        public static final Uri BASE_CONTENT_URI = Uri.parse("content://" + CONTENT_AUTHORITY);
        // Path for "books" table
        public static final String PATH_BOOKS = "books";
        // Books 表的完整 URI: content://com.example.myapp.provider/books
        public static final Uri CONTENT_URI = Uri.withAppendedPath(BASE_CONTENT_URI, PATH_BOOKS);
        // MIME Types
        public static final String CONTENT_LIST_TYPE =
                "vnd.android.cursor.dir/" + CONTENT_AUTHORITY + "/" + PATH_BOOKS;
        public static final String CONTENT_ITEM_TYPE =
                "vnd.android.cursor.item/" + CONTENT_AUTHORITY + "/" + PATH_BOOKS;
        // 定义表列常量 (假设你的 SQLite 表有这些列)
        public static final class BookEntry implements BaseColumns {
            public static final String TABLE_NAME = "books";
            public static final String COLUMN_TITLE = "title";
            public static final String COLUMN_AUTHOR = "author";
            public static final String COLUMN_ISBN = "isbn";
            public static final String COLUMN_PRICE = "price";
        }
    }
    
  2. 创建 ContentProvider 子类:  继承 android.content.ContentProvider,重写关键方法:

    public class BookProvider extends ContentProvider {
        // 数据库助手实例
        private BookDbHelper mDbHelper;
        // URI 匹配器,用于解析传入的 URI
        private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
        // 定义匹配码常量
        private static final int BOOKS = 100; // 匹配整个 books 表
        private static final int BOOK_ID = 101; // 匹配 books 表中的单行 (带 ID)
        // 静态初始化块,配置 UriMatcher
        static {
            sUriMatcher.addURI(BookContract.CONTENT_AUTHORITY, BookContract.PATH_BOOKS, BOOKS);
            sUriMatcher.addURI(BookContract.CONTENT_AUTHORITY, BookContract.PATH_BOOKS + "/#", BOOK_ID);
        }
        @Override
        public boolean onCreate() {
            // 初始化数据库助手。通常在这里创建数据库连接。
            mDbHelper = new BookDbHelper(getContext());
            return true; // 初始化成功
        }
        @Override
        public Cursor query(Uri uri, String[] projection, String selection,
                            String[] selectionArgs, String sortOrder) {
            SQLiteDatabase db = mDbHelper.getReadableDatabase();
            Cursor cursor;
            int match = sUriMatcher.match(uri);
            switch (match) {
                case BOOKS:
                    // 查询整个 books 表
                    cursor = db.query(BookContract.BookEntry.TABLE_NAME, projection, selection,
                            selectionArgs, null, null, sortOrder);
                    break;
                case BOOK_ID:
                    // 从 URI 中提取 ID (如 content://.../books/42 中的 42)
                    selection = BookContract.BookEntry._ID + "=?";
                    selectionArgs = new String[]{String.valueOf(ContentUris.parseId(uri))};
                    // 查询指定 ID 的书
                    cursor = db.query(BookContract.BookEntry.TABLE_NAME, projection, selection,
                            selectionArgs, null, null, sortOrder);
                    break;
                default:
                    throw new IllegalArgumentException("Cannot query unknown URI " + uri);
            }
            // 设置通知 URI (可选但重要,用于监听数据变化)
            cursor.setNotificationUri(getContext().getContentResolver(), uri);
            return cursor;
        }
        @Override
        public Uri insert(Uri uri, ContentValues values) {
            int match = sUriMatcher.match(uri);
            if (match != BOOKS) {
                throw new IllegalArgumentException("Insertion is only supported for " + BookContract.CONTENT_URI);
            }
            SQLiteDatabase db = mDbHelper.getWritableDatabase();
            long id = db.insert(BookContract.BookEntry.TABLE_NAME, null, values);
            if (id == -1) {
                return null; // 插入失败
            }
            // 构造新插入项的 URI (如 content://.../books/42)
            Uri newItemUri = ContentUris.withAppendedId(uri, id);
            // 通知监听者数据已改变 (在这个URI上)
            getContext().getContentResolver().notifyChange(newItemUri, null);
            return newItemUri;
        }
        @Override
        public int update(Uri uri, ContentValues values, String selection,
                          String[] selectionArgs) {
            int match = sUriMatcher.match(uri);
            SQLiteDatabase db = mDbHelper.getWritableDatabase();
            int rowsUpdated;
            switch (match) {
                case BOOKS:
                    rowsUpdated = db.update(BookContract.BookEntry.TABLE_NAME, values, selection, selectionArgs);
                    break;
                case BOOK_ID:
                    selection = BookContract.BookEntry._ID + "=?";
                    selectionArgs = new String[]{String.valueOf(ContentUris.parseId(uri))};
                    rowsUpdated = db.update(BookContract.BookEntry.TABLE_NAME, values, selection, selectionArgs);
                    break;
                default:
                    throw new IllegalArgumentException("Update is not supported for " + uri);
            }
            if (rowsUpdated > 0) {
                getContext().getContentResolver().notifyChange(uri, null); // 通知改变
            }
            return rowsUpdated;
        }
        @Override
        public int delete(Uri uri, String selection, String[] selectionArgs) {
            int match = sUriMatcher.match(uri);
            SQLiteDatabase db = mDbHelper.getWritableDatabase();
            int rowsDeleted;
            switch (match) {
                case BOOKS:
                    rowsDeleted = db.delete(BookContract.BookEntry.TABLE_NAME, selection, selectionArgs);
                    break;
                case BOOK_ID:
                    selection = BookContract.BookEntry._ID + "=?";
                    selectionArgs = new String[]{String.valueOf(ContentUris.parseId(uri))};
                    rowsDeleted = db.delete(BookContract.BookEntry.TABLE_NAME, selection, selectionArgs);
                    break;
                default:
                    throw new IllegalArgumentException("Deletion is not supported for " + uri);
            }
            if (rowsDeleted > 0) {
                getContext().getContentResolver().notifyChange(uri, null); // 通知改变
            }
            return rowsDeleted;
        }
        @Override
        public String getType(Uri uri) {
            int match = sUriMatcher.match(uri);
            switch (match) {
                case BOOKS:
                    return BookContract.CONTENT_LIST_TYPE; // vnd.android.cursor.dir/...
                case BOOK_ID:
                    return BookContract.CONTENT_ITEM_TYPE; // vnd.android.cursor.item/...
                default:
                    throw new IllegalStateException("Unknown URI " + uri + " with match " + match);
            }
        }
    }
    
  3. 在 AndroidManifest.xml 中声明 Provider:

    <application ...>
        ...
        <provider
            android:name=".BookProvider"             // 你的 ContentProvider 类全名
            android:authorities="com.example.myapp.provider" // 必须与 Contract 中定义的 AUTHORITY 完全一致!
            android:exported="true|false"            // 是否允许其他应用访问?true 允许,false 只允许本应用访问
            android:readPermission="..."             // (可选) 全局读权限
            android:writePermission="..."            // (可选) 全局写权限
            android:permission="..."                 // (可选) 读写统一权限
            android:grantUriPermissions="true|false"> // (可选) 是否允许临时 URI 授权
            <!-- 更细粒度的路径权限 (可选) -->
            <path-permission
                android:pathPrefix="/books"
                android:permission="com.example.myapp.READ_BOOKS"
                android:readPermission="..." />
        </provider>
    </application>
    
    • android:exported:  决定是否允许其他应用访问。如果只想给自己应用内部用,设为 false
    • 权限控制:  非常灵活,可以在 <provider> 级别设置全局权限,也可以在 <path-permission> 子元素中为特定 URI 路径设置精细权限。客户端应用必须在 AndroidManifest.xml 中声明相应的 <uses-permission> 才能访问。

关键概念 5:UriMatcher - URI 的“分类器”

  • 是什么?  一个工具类 (android.content.UriMatcher)。

  • 干什么用?  在 ContentProvider 的实现中,用来高效地解析传入的 URI,判断它指向的是哪个数据集或哪条具体数据(根据你预先注册的匹配规则)。

  • 如何使用:

    1. 创建实例:UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);

    2. 添加匹配规则:

      • matcher.addURI(authority, path, code); - 匹配集合 (如 "content://auth/books")
      • matcher.addURI(authority, path + "/#", code); - 匹配带数字 ID 的单条数据 (如 "content://auth/books/42")。# 匹配任意数字。
      • matcher.addURI(authority, path + "/*", code); - 匹配带任意字符串的单条数据 (如 "content://auth/books/fiction")。* 匹配任意字符串。
    3. 当收到请求时,调用 int matchCode = matcher.match(incomingUri);,根据返回的 matchCode 来决定如何处理请求。

关键概念 6:ContentObserver - 数据的“监听器”

  • 是什么?  一个抽象类 (android.database.ContentObserver)。

  • 干什么用?  允许客户端应用注册监听特定 URI 或 URI 子树下数据的变化(插入、更新、删除)。

  • 如何使用 (客户端):

    1. 创建一个 ContentObserver 的子类,重写 onChange(boolean selfChange) 或 onChange(boolean selfChange, Uri uri) 方法。

    2. 通过 ContentResolver 注册监听:

      getContentResolver().registerContentObserver(
              BookContract.CONTENT_URI, // 监听的 URI (可以是集合或单条)
              true,                     // 是否监听其子 URI 的变化 (true 表示监听整个子树)
              myObserver);              // 你的 ContentObserver 实例
      
    3. 当数据变化时,你的 onChange 方法会被调用。你可以在这里刷新 UI 或重新查询数据。

    4. 在不需要时(如 Activity onDestroy())取消注册:getContentResolver().unregisterContentObserver(myObserver);

  • 触发通知 (提供者端):  ContentProvider 在执行 insert()update()delete() 后,如果数据确实改变了,应该调用:

    getContext().getContentResolver().notifyChange(uri, observer);
    
    • uri: 发生变化的 URI(通常是操作的目标 URI)。
    • observer: 可以传入 null。如果传入一个特定的 ContentObserver,则该观察者不会收到自己触发的这次变化通知(避免循环)。

关键概念 7:CursorLoader / LoaderManager (旧) / ViewModel + LiveData + Room (新推荐)

  • 问题:  直接在 Activity/Fragment 中执行 ContentResolver.query() 是同步操作,如果查询耗时(尤其是跨进程时),会阻塞 UI 线程,导致 ANR。需要在后台线程执行查询,查询完再切回主线程更新 UI。

  • 旧方案 (已弃用但需理解):  CursorLoader + LoaderManager

    • CursorLoader: 一个 Loader,专门用于在后台线程通过 ContentResolver 异步加载 Cursor 数据。它会自动管理 Cursor 的生命周期(在配置变更如屏幕旋转时保持加载,避免重复查询;在不需要时关闭 Cursor)。它内部也自动注册了 ContentObserver,当数据变化时会自动重新加载。
    • LoaderManager: 负责管理 Loader 的生命周期(创建、启动、停止、销毁),使其与 Activity/Fragment 的生命周期同步。
  • 新方案 (强烈推荐):  ViewModel + LiveData + Room (或直接使用 ContentResolver 配合协程/RxJava)

    • ViewModel:  持有数据,在配置变更(如旋转)时存活。
    • LiveData:  可观察的数据持有者,自动感知生命周期,确保数据只在活跃的 UI 组件(如 Activity/Fragment)在前台时才更新 UI。
    • Room:  官方推荐的 SQLite ORM 库,它生成的 Dao 可以返回 LiveData<List<Entity>> 或 Flowable<List<Entity>> 等响应式类型。即使你的数据源是自定义 ContentProvider,也可以在 Repository 层使用协程或 RxJava 在后台线程调用 ContentResolver,然后将结果包装成 LiveData 或 Flow 暴露给 ViewModel/UI。
    • 协程 / RxJava:  在后台线程执行 ContentResolver 操作的标准异步工具。

核心原理:基于 Binder 的 IPC 与 AMS 管理

ContentProvider 的核心价值在于提供跨应用(进程)  的安全数据访问。这背后的基石是 Binder IPC 机制 和 Activity Manager Service (AMS)  的集中式管理。

1. 注册与发布:Provider 的“挂牌营业”

  • 启动时机:  当应用安装后首次启动时,或当首次有客户端通过 ContentResolver 请求访问该 Provider 时,系统会启动 Provider 所在的应用进程(如果还没运行)。

  • 安装公告:  应用安装时,系统解析其 AndroidManifest.xml,记录下所有 <provider> 标签的信息(authority, 类名, 权限等)。这些信息注册到系统的核心服务 Package Manager Service (PMS)  中。

  • 进程启动 & Provider 初始化:

    1. 系统(通常是 AMS)决定启动 Provider 所在的应用进程。
    2. 在该应用进程的主线程 (ActivityThread) 中,系统调用 Application.onCreate()(如果定义了)。
    3. 紧接着,系统为该进程中声明的每个 <provider> 调用其 ContentProvider.onCreate() 方法(在应用主线程执行! )。这是 Provider 初始化自己(如打开数据库)的地方。
    4. 应用进程通过 Binder 调用 ActivityManagerService,告知 AMS:“我的 ContentProvider 实例 X 已经创建好了,它负责的 Authority 是 Y”。这本质上是将本地的 ContentProvider 对象“发布”给系统服务 AMS。
    5. AMS 作为中央注册表:  AMS 维护着一个全局的映射关系:Authority -> ContentProviderRecordContentProviderRecord 包含关键信息:指向目标应用进程的 Binder 代理 (IApplicationThread),以及指向该进程内具体 ContentProvider 对象的 Binder 代理 (IContentProvider)。AMS 就是那个知道“哪个服务员在哪家店”的总管家。

2. 客户端请求:ContentResolver 的“跑腿代办”

当客户端调用 ContentResolver.query(uri, ...)

  1. 获取 ContentResolver:  context.getContentResolver() 返回一个 ApplicationContentResolver 实例(它是 ContentResolver 的子类)。

  2. 调用 query:  resolver.query(uri, ...)

  3. 内部调用 acquireProvider:  ApplicationContentResolver 调用其 acquireProvider(Context context, String auth) 方法(或其变种)。传入的 auth 是从 uri 中解析出来的 authority (如 com.android.contacts)。

  4. 请求 AMS 查找 Provider:

    • acquireProvider 内部最终会调用 ActivityThread 的 acquireProvider 方法。
    • ActivityThread 首先检查本地缓存(一个 Map<Auth, IContentProvider>)是否已有这个 authority 对应的 IContentProvider 代理。如果有缓存且有效,直接使用。
    • 关键步骤:缓存未命中!  ActivityThread 通过 Binder IPC 调用 ActivityManagerService 的 getContentProvider 方法。传入 authority 和客户端的 userId 等信息。
  5. AMS 查找并可能启动 Provider:

    • AMS 查找自己的 ContentProviderRecord 注册表。

    • 如果找到对应的记录且 Provider 所在进程已启动:

      • 检查客户端是否有权限访问。
      • 如果有权限,AMS 将 ContentProviderRecord 中保存的指向目标 Provider 的 IContentProvider Binder 代理返回给客户端进程。
    • 如果 Provider 所在进程未启动:

      • AMS 先启动目标应用进程。
      • 等待目标进程启动完成,并如上所述,在目标进程中将 Provider 初始化并发布给 AMS(步骤 1.4)。
      • AMS 拿到新发布的 IContentProvider 代理,再返回给客户端进程。
  6. 客户端获得代理对象:  客户端进程的 ActivityThread 收到 AMS 返回的 IContentProvider 代理对象(这是一个 Binder 代理对象,实现了 IContentProvider 接口)。它把这个代理对象缓存起来(避免下次再问 AMS),然后返回给 ApplicationContentResolver

  7. 跨进程调用 query:  ApplicationContentResolver 拿到 IContentProvider 代理后,调用其 query 方法(IContentProvider.query(callingPkg, uri, projection, selection, selectionArgs, sortOrder, cancellationSignal))。注意 callingPkg 是客户端的包名,用于 Provider 端做更精细的权限控制。

  8. Binder IPC 传递请求:  这个 query 调用通过 Binder IPC 机制,传递到 Provider 所在的应用进程。

  9. Provider 进程处理请求:

    • 在 Provider 进程端,Binder 线程池中的一个线程收到这个 IPC 调用。
    • 系统找到该进程内与这个 IContentProvider 代理对应的真正的 ContentProvider 对象(通常在 ActivityThread 中维护)。
    • 调用该 ContentProvider 对象的 query 方法(就是你重写的那个方法!)。
    • query 方法在 Binder 线程(不是主线程!)中执行。这就是为什么你在 query 实现里可以直接执行数据库操作(数据库操作通常允许在非主线程)。
    • 真正的 ContentProvider.query 方法执行数据库查询,得到一个 Cursor
  10. 跨进程返回结果:  查询得到的 Cursor 不能直接通过 Binder 传递(Binder 传输有大小限制,且 Cursor 是 Java 对象)。系统会将其包装成一个特殊的 Cursor 子类,通常是 CursorToBulkCursorAdaptor 或 CrossProcessCursorWrapper。核心是:

    • Provider 端:创建一个 BulkCursorDescriptor 对象,它包含了描述 Cursor 的元信息(列名、类型等)和一个指向 Cursor 所在进程的 IBulkCursor 接口的 Binder 代理 (BulkCursorToCursorAdaptor)。
    • 这个 BulkCursorDescriptor 对象通过 Binder 返回到客户端进程。
  11. 客户端接收 Cursor 代理:  客户端进程收到 BulkCursorDescriptor 后,利用它创建一个 CrossProcessCursor 的代理对象,通常是 CursorWrapperInner 或 BulkCursorToCursorAdaptor。这个代理对象内部持有指向 Provider 端真实 Cursor 的 IBulkCursor Binder 代理。

  12. 客户端使用代理 Cursor:  客户端代码拿到这个 Cursor 代理对象。当它调用 cursor.moveToFirst()cursor.getString() 等方法时:

    • 这些调用会通过 IBulkCursor Binder 代理,发送到 Provider 进程。
    • Provider 进程端的 BulkCursorToCursorAdaptor 接收到请求,调用其内部持有的真实 Cursor 对象的对应方法。
    • 操作结果(数据、是否成功)再通过 Binder 返回到客户端进程的 Cursor 代理对象。
    • 客户端感觉像在使用本地 Cursor,实际上每次数据获取都是跨进程的 IPC 调用!这就是为什么使用 ContentProvider 查询大量数据时性能需要特别注意,以及为什么推荐使用 Loader 或协程在后台处理。
  13. 关闭 Cursor:  当客户端调用 cursor.close() 时,这个关闭操作也会通过 Binder IPC 传递到 Provider 进程,关闭真正的 Cursor 并释放资源。客户端必须关闭!

图解调用链路

[Client App]                                                      [System Server (AMS)]                                         [Provider App]
     |                                                                   |                                                           |
     | 1. ContentResolver.query(uri, ...)                                |                                                           |
     |------------------------------------------------------------------>|                                                           |
     |                                                                   | 2. AMS: Lookup Provider by authority (from uri)           |
     |                                                                   |    - Check permissions                                     |
     |                                                                   |    - Start Provider App if needed & wait for publish       |
     |                                                                   |                                                           |
     |                                                                   | <------------------------(Provider process starts & calls AMS.publishContentProviders)
     |                                                                   |                                                           |
     |                                                                   | 3. AMS returns IContentProvider proxy (Binder) to client   |
     |<------------------------------------------------------------------|                                                           |
     |                                                                   |                                                           |
     | 4. Client calls IContentProvider.query(...) via Binder proxy      |                                                           |
     |------------------------------------------------------------------>|---------------------------------------------------------->|
     |                                                                   |                                                           | 5. Provider's Binder thread receives call
     |                                                                   |                                                           | 6. Call real ContentProvider.query() (your code!)
     |                                                                   |                                                           | 7. Get real Cursor
     |                                                                   |                                                           | 8. Wrap Cursor in BulkCursorDescriptor (with IBulkCursor proxy)
     |                                                                   |                                                           | 9. Return BulkCursorDescriptor via Binder
     |<------------------------------------------------------------------|<----------------------------------------------------------|
     |                                                                   |                                                           |
     | 10. Client wraps descriptor into a Cursor proxy (e.g., CursorWrapperInner) |                                                           |
     |                                                                   |                                                           |
     | 11. Client uses cursor proxy (e.g., moveToFirst, getString)      |                                                           |
     |     (Each call triggers IPC to Provider app)                     |                                                           |
     |------------------------------------------------------------------>|---------------------------------------------------------->|
     |                                                                   |                                                           | 12. Provider's BulkCursorAdaptor calls real Cursor
     |                                                                   |                                                           | 13. Results sent back via Binder
     |<------------------------------------------------------------------|<----------------------------------------------------------|
     |                                                                   |                                                           |
     | ...                                                               |                                                           |
     |                                                                   |                                                           |
     | 14. cursor.close()                                                |                                                           |
     |------------------------------------------------------------------>|---------------------------------------------------------->|
     |                                                                   |                                                           | 15. Close real Cursor & release resources
     |                                                                   |                                                           |

关键源码类解析

  1. ContentResolver (抽象类) / ApplicationContentResolver (实现类):

    • 客户端的主要入口。getContentResolver() 返回的是 ApplicationContentResolver
    • 核心方法 acquireProvider(String auth) (最终调用 ActivityThread.acquireProvider), queryinsertupdatedelete
  2. ActivityThread (核心类):

    • 代表应用的主线程。管理着应用的核心组件(Activity, Service, BroadcastReceiver, ContentProvider)的生命周期。
    • 维护一个 Map (mProviderMap) 缓存已获取的 IContentProvider 代理。
    • 关键方法 acquireProvider (处理 AMS 交互,获取 Provider 代理), publishContentProviders (在 Provider 启动后,将其发布给 AMS)。
  3. ActivityManagerService (AMS) (系统服务):

    • 系统核心服务,管理四大组件、进程、权限等。
    • 维护 ContentProvider 的全局注册表 (mProviderMap - ProviderMap 类)。
    • 关键方法 getContentProvider (响应客户端请求,查找或启动 Provider), publishContentProviders (接收 Provider 进程的发布通知)。
  4. IContentProvider (Binder 接口):

    • 定义了 ContentProvider 核心操作(queryinsertupdatedeletegetTypecall 等)的 Binder 接口。
    • 客户端持有的是 IContentProvider.Stub.Proxy (代理对象)。
    • Provider 端实现的是 ContentProvider.Transport (内部类,继承 IContentProvider.Stub)。
  5. ContentProvider (抽象类):

    • 开发者继承和实现的核心类。
    • 包含内部类 Transport,它是 IContentProvider.Stub 的实现,负责接收来自客户端的 Binder IPC 调用,并转发给外部的 ContentProvider 实例方法。
    • 关键生命周期方法 onCreate (初始化), queryinsertupdatedeletegetType
  6. UriMatcher:  如前所述,用于解析 URI。

  7. Cursor / ICursor / IBulkCursor / BulkCursorDescriptor / BulkCursorToCursorAdaptor / CursorToBulkCursorAdaptor / CursorWrapperInner:

    • 这一系列类和接口共同实现了 Cursor 数据的跨进程访问代理机制,是 ContentProvider 查询性能的关键点。

总结与注意事项

  1. 核心价值:  安全、标准化的跨应用数据共享机制。是 Android 沙箱模型的必要补充。

  2. 关键角色:  URI (门牌号), ContentResolver (客户端代理), ContentProvider (服务端实现), AMS (中央管理器), Binder (通信管道)。

  3. 性能考量:  跨进程通信 (IPC) 有开销。通过 ContentProvider 访问数据,尤其是大量数据或频繁操作,性能远低于直接访问本地数据库。设计时需注意:

    • 避免在 UI 线程执行耗时 CP 操作。
    • 使用 Projection 只查询需要的列。
    • 使用 Selection 精确过滤数据。
    • 考虑使用 Loader / LiveData / 协程 / RxJava 进行异步加载。
    • 对于仅限本应用使用的数据,优先考虑 SQLiteOpenHelper 或 Room 直接访问数据库。
  4. 权限控制:  是安全的核心。务必在 Manifest 中正确声明权限 (exportedreadPermissionwritePermissionpath-permission),并在运行时检查(Android 6.0+)。

  5. 线程模型:

    • ContentProvider.onCreate() 在主线程调用。初始化操作要快。
    • queryinsertupdatedelete 方法在 Binder 线程池 中调用。必须保证线程安全(如数据库操作本身是线程安全的,或者使用同步机制)。避免在这些方法中直接操作 UI。
  6. 现代替代:  对于应用内部的数据访问,Room + LiveData/Flow + ViewModel 是 Google 官方推荐的最佳实践,它提供了更简洁、类型安全、响应式且生命周期感知的 API。ContentProvider 的主要场景仍然是向其他应用安全地暴露数据(如系统联系人、日历、自定义 SDK 提供数据)或访问系统提供的数据。

  7. ContentProvider 与 FileProvider:  FileProvider 是 ContentProvider 的一个特殊子类,专门用于安全地共享应用私有文件(通过 content:// URI 和 Intent.FLAG_GRANT_READ_URI_PERMISSION / FLAG_GRANT_WRITE_URI_PERMISSION 机制)。它避免了使用 file:// URI 的安全风险。