第十章:ContentProvider与数据共享

ContentProvider,说白了就是Android系统给应用之间共享数据开的一扇门。我刚开始接触Framework时,总觉得它就是个「高级数据库封装」,后来踩过几次坑才明白——它真正的价值在于跨进程通信的标准化。

10.1 ContentProvider原理

ContentProvider的核心,是Binder驱动的跨进程数据通道。每个Provider在AMS(ActivityManagerService)中注册时,会分配一个唯一的authority。你想想看,这就像给每个数据源发了个门牌号。

关键点:ContentProvider本身不存储数据,它只是数据的「代理人」。真正的数据可能在SQLite、文件、网络,甚至内存里。

我在项目中遇到过一个问题:某个Provider的query方法返回了空Cursor,但数据库里明明有数据。后来发现是Binder传输的Cursor对象超过了1MB的Binder事务缓冲区限制。嗯,这里要注意——大数据量一定要分页。

10.2 跨进程数据访问

跨进程访问ContentProvider,本质上是客户端通过ContentResolver发起Binder调用。流程是这样的:

  1. 客户端调用ContentResolver.query()
  2. 内部通过Binder调用到Provider所在进程
  3. Provider执行query,返回Cursor
  4. Cursor通过Binder跨进程传递(实际是CursorWindow的共享内存机制)

我个人习惯在Provider的query方法里加日志,方便排查调用来源。曾经有个第三方SDK频繁调用我的Provider,导致主线程卡顿——加个线程检查就解决了。

避坑指南:我曾经在ContentProvider的onCreate()里做了耗时操作,结果应用启动直接ANR。记住,Provider的onCreate()运行在主线程,千万别干重活。

10.3 Uri匹配机制

Uri匹配是ContentProvider的「路由系统」。每个Provider会注册一组Uri规则,匹配到对应操作。我习惯用UriMatcher来实现:

public class MyProvider extends ContentProvider {
    private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
    
    private static final int CODE_USERS = 1;
    private static final int CODE_USER_ID = 2;
    
    static {
        sUriMatcher.addURI("com.example.provider", "users", CODE_USERS);
        sUriMatcher.addURI("com.example.provider", "users/#", CODE_USER_ID);
    }
    
    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
                        String[] selectionArgs, String sortOrder) {
        switch (sUriMatcher.match(uri)) {
            case CODE_USERS:
                // 查询所有用户
                break;
            case CODE_USER_ID:
                // 查询单个用户
                String userId = uri.getLastPathSegment();
                break;
            default:
                throw new IllegalArgumentException("Unknown URI: " + uri);
        }
    }
}

为什么用#而不是*?#匹配数字,*匹配任意文本。我建议严格区分,避免Uri解析出错。

小技巧:UriMatcher的匹配是从上到下的,把更具体的规则放在前面。我曾经把通配符规则放在前面,导致精确匹配永远走不到。

10.4 Cursor与ContentObserver

Cursor是数据查询的结果集,但跨进程传递时有个特殊机制——它背后是CursorWindow,一块共享内存。客户端和Provider进程通过这块内存直接读写数据,避免了数据拷贝。

ContentObserver则是数据变化的「监听器」。当Provider的数据发生变化时,会通知所有注册的Observer。我习惯在数据变更后调用:

getContext().getContentResolver().notifyChange(uri, null);

这样所有监听这个Uri的Observer都会收到回调。曾经有个同事忘了调用notifyChange,导致UI一直显示旧数据——排查了半天才发现。

核心要点:ContentObserver的回调发生在Provider所在进程,但Observer本身是跨进程注册的。系统通过Binder把通知跨进程传递回去。

10.5 知识体系总览

下面这张图,是我梳理的ContentProvider核心知识结构。你看,它其实就围绕三个问题:数据怎么暴露?怎么访问?怎么通知变化?

ContentProvider 原理 Binder驱动跨进程通信 AMS注册与authority分配 数据存储无关(SQLite/文件/网络) 跨进程访问 ContentResolver发起Binder调用 UriMatcher路由匹配 CursorWindow共享内存 变化通知 ContentObserver注册 notifyChange触发回调 跨进程Binder通知 核心:标准化跨进程数据共享 暴露 → 访问 → 通知

10.6 实战经验总结

最后,分享几个我在项目中积累的经验:

  • 权限控制:Provider的readPermission和writePermission一定要配,别裸奔。我曾经见过一个Provider没配权限,任何应用都能读用户数据。
  • 批量操作:用ContentProviderOperation实现批量事务,比逐条调用高效得多。我优化过一个同步模块,批量操作后耗时从30秒降到2秒。
  • 临时文件:大文件共享用FileProvider,别自己写文件传输逻辑。Android官方已经封装好了,直接用就行。
  • 生命周期:Provider所在进程被杀后,所有Cursor都会失效。记得在客户端做重连机制。

个人建议:写Provider时,一定要考虑多线程并发。我习惯用ReadWriteLock来保护数据,读多写少的场景性能提升很明显。

ContentProvider这套机制,说白了就是Android系统对「数据共享」这个需求的标准化解决方案。理解它的原理,你就能写出更健壮、更高效的数据共享代码。嗯,这部分内容就到这里。

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