25、ContentProvider:跨进程数据共享,读取系统联系人、自定义ContentProvider

ContentProvider,说白了就是Android给不同App之间准备的“数据管道”。

我刚开始做Android开发时,总觉得这玩意儿有点多余。自己App的数据自己用不就行了?直到有一次我需要读取系统通讯录,才发现没有ContentProvider,你根本拿不到别的App的数据。嗯,这就是它的核心价值——跨进程数据共享。

为什么需要ContentProvider?

Android的每个App都运行在自己的进程里,有自己的沙箱。正常情况下,App A访问不了App B的数据库文件。那系统通讯录、短信、相册这些数据,怎么让第三方App读取呢?

ContentProvider就是官方提供的标准方案。它把数据暴露出来,其他App通过URI去访问。你想想看,如果没有这个机制,每个App都自己去读别的App的数据库文件,那安全性和稳定性就全乱套了。

核心要点:ContentProvider是Android四大组件之一,用于跨进程数据共享。它提供了一套统一的接口,让数据访问变得标准化、安全化。

ContentProvider的工作原理

我画了一张图,帮你理解ContentProvider在整个系统中的位置:

ContentProvider 跨进程数据共享架构 App A(调用方) ContentResolver query() / insert() update() / delete() URI匹配 content:// authority 路径匹配 App B(提供方) ContentProvider onCreate() query / insert / update / delete 数据源 SQLite / 文件 / 网络 权限控制 AndroidManifest.xml readPermission / writePermission

流程其实很简单:App A通过ContentResolver发起请求,系统根据URI找到对应的ContentProvider,然后调用App B中对应的方法。整个过程对开发者来说,就像在调用本地方法一样。

读取系统联系人

读取系统通讯录,是ContentProvider最经典的应用场景。我在项目中做过好几次联系人同步功能,这里给你一个完整的示例:

// 读取系统联系人
public List<String> readContacts(Context context) {
    List<String> contacts = new ArrayList<>();
    ContentResolver resolver = context.getContentResolver();
    
    // 系统联系人的URI
    Uri uri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI;
    
    // 要查询的字段
    String[] projection = new String[]{
        ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,
        ContactsContract.CommonDataKinds.Phone.NUMBER
    };
    
    // 查询,返回Cursor
    Cursor cursor = resolver.query(uri, projection, null, null, null);
    
    if (cursor != null) {
        while (cursor.moveToNext()) {
            String name = cursor.getString(cursor.getColumnIndex(
                ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
            String number = cursor.getString(cursor.getColumnIndex(
                ContactsContract.CommonDataKinds.Phone.NUMBER));
            contacts.add(name + " : " + number);
        }
        cursor.close(); // 别忘了关闭Cursor
    }
    return contacts;
}

注意:读取联系人需要声明权限。Android 6.0以上需要动态申请READ_CONTACTS权限。我曾经遇到过用户拒绝权限后App直接崩溃的情况,所以一定要做权限检查。

权限声明在AndroidManifest.xml中:

<uses-permission android:name="android.permission.READ_CONTACTS" />

动态权限申请代码:

if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) 
        != PackageManager.PERMISSION_GRANTED) {
    ActivityCompat.requestPermissions(this, 
        new String[]{Manifest.permission.READ_CONTACTS}, REQUEST_CODE);
}

自定义ContentProvider

有时候我们需要把自己的数据暴露给其他App。比如我做过一个笔记App,想让其他App能读取笔记内容。这时候就需要自定义ContentProvider。

实现一个ContentProvider,需要继承ContentProvider类,并实现六个方法:

方法 说明 调用时机
onCreate() 初始化Provider App启动时
query() 查询数据 外部调用query时
insert() 插入数据 外部调用insert时
update() 更新数据 外部调用update时
delete() 删除数据 外部调用delete时
getType() 返回MIME类型 外部调用getType时

下面是一个简单的自定义ContentProvider示例:

public class MyContentProvider extends ContentProvider {
    
    private static final String AUTHORITY = "com.example.myapp.provider";
    private static final String TABLE_NAME = "notes";
    
    // 定义URI匹配器
    private static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH);
    private static final int NOTES_ALL = 1;
    private static final int NOTES_ID = 2;
    
    static {
        URI_MATCHER.addURI(AUTHORITY, TABLE_NAME, NOTES_ALL);
        URI_MATCHER.addURI(AUTHORITY, TABLE_NAME + "/#", NOTES_ID);
    }
    
    private SQLiteDatabase db;
    
    @Override
    public boolean onCreate() {
        // 初始化数据库
        MyDatabaseHelper helper = new MyDatabaseHelper(getContext());
        db = helper.getWritableDatabase();
        return db != null;
    }
    
    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
                        String[] selectionArgs, String sortOrder) {
        Cursor cursor;
        switch (URI_MATCHER.match(uri)) {
            case NOTES_ALL:
                cursor = db.query(TABLE_NAME, projection, selection, 
                    selectionArgs, null, null, sortOrder);
                break;
            case NOTES_ID:
                String id = uri.getLastPathSegment();
                cursor = db.query(TABLE_NAME, projection, "_id=?", 
                    new String[]{id}, null, null, sortOrder);
                break;
            default:
                throw new IllegalArgumentException("Unknown URI: " + uri);
        }
        // 设置通知URI,让Cursor能感知数据变化
        cursor.setNotificationUri(getContext().getContentResolver(), uri);
        return cursor;
    }
    
    @Override
    public Uri insert(Uri uri, ContentValues values) {
        long rowId = db.insert(TABLE_NAME, null, values);
        if (rowId > 0) {
            Uri newUri = ContentUris.withAppendedId(uri, rowId);
            // 通知数据变化
            getContext().getContentResolver().notifyChange(newUri, null);
            return newUri;
        }
        throw new SQLException("Failed to insert row into " + uri);
    }
    
    @Override
    public int update(Uri uri, ContentValues values, String selection,
                      String[] selectionArgs) {
        int count = db.update(TABLE_NAME, values, selection, selectionArgs);
        if (count > 0) {
            getContext().getContentResolver().notifyChange(uri, null);
        }
        return count;
    }
    
    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        int count = db.delete(TABLE_NAME, selection, selectionArgs);
        if (count > 0) {
            getContext().getContentResolver().notifyChange(uri, null);
        }
        return count;
    }
    
    @Override
    public String getType(Uri uri) {
        switch (URI_MATCHER.match(uri)) {
            case NOTES_ALL:
                return "vnd.android.cursor.dir/vnd.com.example.notes";
            case NOTES_ID:
                return "vnd.android.cursor.item/vnd.com.example.notes";
            default:
                throw new IllegalArgumentException("Unknown URI: " + uri);
        }
    }
}

别忘了在AndroidManifest.xml中注册:

<provider
    android:name=".MyContentProvider"
    android:authorities="com.example.myapp.provider"
    android:exported="true" />

个人经验:我建议把exported属性设为true时一定要谨慎。如果只是给自己App的另一个进程用,可以设为false。暴露给外部App时,最好加上权限控制,防止数据被随意访问。

ContentProvider的避坑指南

我在项目中踩过不少ContentProvider的坑,这里分享几个:

  • Cursor没关闭导致内存泄漏——我曾经在循环中忘了close Cursor,结果内存越占越多,最后App直接OOM。一定要用try-finally或者try-with-resources确保Cursor被关闭。
  • URI匹配错误——UriMatcher的匹配规则很容易写错。我建议把所有的URI常量集中定义,不要散落在代码里。
  • 跨进程数据量大时ANR——ContentProvider的方法默认运行在调用方的Binder线程池中。如果查询很耗时,比如一次查几千条数据,很容易触发ANR。我建议大数据量查询时用分页,或者用ContentProvider的异步查询接口。
  • 权限声明遗漏——自定义Provider如果忘了声明权限,外部App调用时会直接抛SecurityException。记得在provider标签里加上readPermission和writePermission。

ContentProvider vs 其他方案

你可能会问,跨进程通信不是还有AIDL、BroadcastReceiver、Messenger吗?为什么非要用ContentProvider?

我的看法是:

  • 数据共享场景——比如通讯录、媒体库,用ContentProvider最合适。它天然支持CRUD操作,还有数据变化通知机制。
  • 方法调用场景——比如让另一个App执行某个计算任务,用AIDL更直接。
  • 事件通知场景——比如某个状态变化了,用BroadcastReceiver更轻量。

说白了,ContentProvider是专门为“数据”设计的。如果你的核心需求是共享结构化数据,那就选它准没错。

总结一下:ContentProvider是Android跨进程数据共享的标准方案。读取系统联系人时,用ContentResolver配合ContactsContract即可。自定义ContentProvider时,注意URI匹配、权限控制和Cursor管理。记住,数据变化后要调用notifyChange,这样监听者才能及时收到更新。


公众号:蓝海资料掘金营,微信deep3321