实战项目:完整通讯录应用、数据备份恢复、多模块数据共享

讲到这里,Content Provider 的理论知识基本都覆盖了。但说实话,光看理论是不够的。我当年刚学的时候,看完文档觉得「哦,原来就这么回事」,结果真动手写项目,各种坑全冒出来了。

所以这一章,咱们直接上实战。我会带你做一个完整的通讯录应用,顺便把数据备份恢复、多模块数据共享这两个高频场景也一并搞定。你想想看,这三个场景几乎覆盖了 Content Provider 90% 的实际用途。

项目一:完整通讯录应用

通讯录是 Content Provider 最经典的应用场景。Android 系统自带的 Contacts Provider 已经封装好了,但我们今天要做的,是自己实现一个通讯录 Provider,然后暴露给其他应用使用。

1. 定义数据库结构

先建表。我习惯把联系人信息和电话号码分开存,这样查询效率更高。

public class ContactContract {
    public static final String AUTHORITY = "com.example.contacts.provider";

    // 联系人表
    public static final class ContactEntry implements BaseColumns {
        public static final Uri CONTENT_URI = 
            Uri.parse("content://" + AUTHORITY + "/contacts");
        public static final String TABLE_NAME = "contacts";
        public static final String COLUMN_NAME = "name";
        public static final String COLUMN_EMAIL = "email";
    }

    // 电话号码表
    public static final class PhoneEntry implements BaseColumns {
        public static final Uri CONTENT_URI = 
            Uri.parse("content://" + AUTHORITY + "/phones");
        public static final String TABLE_NAME = "phones";
        public static final String COLUMN_CONTACT_ID = "contact_id";
        public static final String COLUMN_NUMBER = "number";
        public static final String COLUMN_TYPE = "type"; // HOME, WORK, MOBILE
    }
}

小提示:BaseColumns 接口自带 _ID 字段,省得你每次都要手动定义。我早期项目没注意这个,每个表都自己写 _ID,后来重构时才发现有多麻烦。

2. 实现 ContentProvider 类

这里有个关键点:多表查询时的 URI 匹配。我见过不少新手把 URI 匹配写成一团乱麻,最后 debug 到崩溃。

public class ContactProvider extends ContentProvider {
    private static final int CONTACTS = 1;
    private static final int CONTACT_ID = 2;
    private static final int PHONES = 3;
    private static final int PHONE_ID = 4;

    private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);

    static {
        sUriMatcher.addURI(ContactContract.AUTHORITY, "contacts", CONTACTS);
        sUriMatcher.addURI(ContactContract.AUTHORITY, "contacts/#", CONTACT_ID);
        sUriMatcher.addURI(ContactContract.AUTHORITY, "phones", PHONES);
        sUriMatcher.addURI(ContactContract.AUTHORITY, "phones/#", PHONE_ID);
    }

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

        switch (sUriMatcher.match(uri)) {
            case CONTACTS:
                cursor = db.query(ContactEntry.TABLE_NAME, projection, 
                    selection, selectionArgs, null, null, sortOrder);
                break;
            case CONTACT_ID:
                String id = uri.getLastPathSegment();
                cursor = db.query(ContactEntry.TABLE_NAME, projection,
                    "_ID = ?", new String[]{id}, null, null, sortOrder);
                break;
            // ... 其他 case 类似
            default:
                throw new IllegalArgumentException("Unknown URI: " + uri);
        }
        cursor.setNotificationUri(getContext().getContentResolver(), uri);
        return cursor;
    }
}

注意:别忘了调用 setNotificationUri()。我曾经有个项目,数据更新了但 UI 不刷新,查了半天才发现是忘了这行代码。没有它,ContentObserver 收不到通知。

项目二:数据备份与恢复

数据备份恢复,说白了就是把 Provider 里的数据导出成文件,或者从文件导入。这个场景在用户换手机、重置系统时特别常见。

我个人习惯用 JSON 格式做备份,因为可读性好,而且 Android 原生就支持 JSON 解析。

1. 备份逻辑

public class BackupManager {
    public static void backupContacts(Context context, File outputFile) {
        ContentResolver resolver = context.getContentResolver();
        Uri uri = ContactContract.ContactEntry.CONTENT_URI;

        Cursor cursor = resolver.query(uri, null, null, null, null);
        if (cursor == null) return;

        JSONArray jsonArray = new JSONArray();
        while (cursor.moveToNext()) {
            JSONObject obj = new JSONObject();
            try {
                obj.put("name", cursor.getString(cursor.getColumnIndex("name")));
                obj.put("email", cursor.getString(cursor.getColumnIndex("email")));
                // 这里还可以查电话号码,关联查询
                jsonArray.put(obj);
            } catch (JSONException e) {
                e.printStackTrace();
            }
        }
        cursor.close();

        // 写入文件
        try (FileWriter writer = new FileWriter(outputFile)) {
            writer.write(jsonArray.toString(2));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

核心思路:备份就是「读 Provider → 转 JSON → 写文件」。恢复就是反过来:「读文件 → 解析 JSON → 写 Provider」。流程清晰,但要注意事务处理。

2. 恢复时的批量插入

恢复数据时,如果一条一条插入,几百条联系人可能要等好几秒。我建议用批量插入,配合事务,速度能快 10 倍以上。

public static void restoreContacts(Context context, File inputFile) {
    ContentResolver resolver = context.getContentResolver();
    Uri uri = ContactContract.ContactEntry.CONTENT_URI;

    try {
        String jsonStr = new String(FileInputStream(inputFile).readAllBytes());
        JSONArray jsonArray = new JSONArray(jsonStr);

        ArrayList<ContentProviderOperation> operations = new ArrayList<>();
        for (int i = 0; i < jsonArray.length(); i++) {
            JSONObject obj = jsonArray.getJSONObject(i);
            ContentValues values = new ContentValues();
            values.put("name", obj.getString("name"));
            values.put("email", obj.getString("email"));

            operations.add(ContentProviderOperation.newInsert(uri)
                .withValues(values)
                .build());
        }

        // 批量执行
        resolver.applyBatch(ContactContract.AUTHORITY, operations);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

避坑指南:我曾经在恢复数据时没加事务,结果插到一半崩溃了,数据丢了一半。applyBatch 默认就在事务里执行,但如果你自己写循环插入,记得手动开启事务。

项目三:多模块数据共享

现在的大型 App 基本都是多模块架构。比如一个电商 App,有用户模块、订单模块、商品模块。每个模块都有自己的数据库,但有时候需要共享数据。

举个例子:订单模块需要读取用户的收货地址。如果直接访问用户模块的数据库,就破坏了模块独立性。这时候 Content Provider 就派上用场了。

1. 模块间通信架构

多模块数据共享架构 用户模块 UserProvider content://com.app.user/ 订单模块 OrderProvider content://com.app.order/ 商品模块 ProductProvider content://com.app.product/ 读取地址 查询价格 关键设计原则: 1. 每个模块独立维护自己的 Provider 2. 模块间通过 Content URI 访问,不直接操作数据库 3. 权限控制:读/写分离,按需授权 4. 数据变更通过 ContentObserver 通知,避免轮询

2. 订单模块读取用户地址

// 在订单模块中,通过 ContentResolver 读取用户模块的数据
public class OrderRepository {
    private final ContentResolver contentResolver;

    public OrderRepository(Context context) {
        this.contentResolver = context.getContentResolver();
    }

    public String getUserDefaultAddress(int userId) {
        Uri uri = Uri.parse("content://com.app.user/address/" + userId);
        Cursor cursor = contentResolver.query(uri, 
            new String[]{"address"}, null, null, null);
        
        if (cursor != null && cursor.moveToFirst()) {
            String address = cursor.getString(0);
            cursor.close();
            return address;
        }
        return null;
    }
}

重点:模块间共享数据时,一定要定义好接口契约。我见过最坑的情况是,两个模块的团队各自改了数据库字段名,结果联调时数据全乱了。建议把 Contract 类放在公共的 base 模块里。

权限控制与安全

多模块共享数据,权限控制是重中之重。你不能让订单模块随便删用户数据吧?

权限类型 作用 示例
读权限 允许查询数据 android.permission.READ_CONTACTS
写权限 允许增删改数据 android.permission.WRITE_CONTACTS
签名权限 仅同签名应用可访问 protectionLevel="signature"
临时权限 单次授权,用完即废 grantUriPermission

我个人建议,内部模块之间用签名权限就够了。对外暴露的 Provider,才考虑用普通权限加运行时申请。

性能优化:批量操作与缓存

多模块共享数据时,频繁的跨进程查询会带来性能开销。我常用的优化手段有两个:

  • 批量操作:能用 applyBatch 就别一条一条 insert。减少 Binder 调用次数,效果立竿见影。
  • 内存缓存:对于不常变的数据(比如用户地址),在模块内做一层 LruCache。数据变更时通过 ContentObserver 失效缓存。
public class AddressCache {
    private static final int MAX_SIZE = 100;
    private final LruCache<Integer, String> cache = new LruCache<>(MAX_SIZE);
    private final ContentResolver resolver;

    public AddressCache(Context context) {
        this.resolver = context.getContentResolver();
        // 注册观察者,数据变化时清缓存
        resolver.registerContentObserver(
            Uri.parse("content://com.app.user/address"), 
            true, 
            new ContentObserver(null) {
                @Override
                public void onChange(boolean selfChange) {
                    cache.evictAll();
                }
            });
    }

    public String getAddress(int userId) {
        String cached = cache.get(userId);
        if (cached != null) return cached;

        // 缓存未命中,查询 Provider
        String address = queryFromProvider(userId);
        cache.put(userId, address);
        return address;
    }
}

经验之谈:缓存虽好,但别忘了失效策略。我有个项目就是因为缓存没及时清,用户改了地址,订单还是发到老地址去了。后来加了 ContentObserver,问题解决。

总结

这三个实战项目,覆盖了 Content Provider 最核心的使用场景:

  • 通讯录应用让你掌握 Provider 的完整实现流程
  • 数据备份恢复让你学会数据的导入导出和批量操作
  • 多模块共享让你理解大型项目的架构设计思路

说白了,Content Provider 就是个数据中间层。你把它理解成「数据 API 网关」,一切就都顺了。模块之间不直接操作数据库,而是通过 URI 访问,这样耦合度低,扩展性也强。

嗯,这一章的内容就到这里。代码我都跑过,你可以直接拿去用。如果遇到问题,多看看 Logcat,Provider 相关的错误通常会在日志里写得很清楚。


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