11、ContentProvider原理:跨进程数据共享、URI匹配、CRUD操作与权限校验
ContentProvider,说白了就是Android系统给应用之间搭的一座桥。你想让别的App读写你的数据,或者你想读写别的App的数据,都得走这座桥。我刚开始接触Android时,觉得这玩意儿挺绕的,后来在做一个联系人同步项目时,才真正体会到它的设计精妙之处。
11.1 跨进程数据共享的核心机制
Android的每个App默认跑在自己的进程里,数据是隔离的。那怎么让A应用读到B应用的数据呢?ContentProvider就是干这个的。
它底层依赖Binder机制。你想想看,A应用发起一个查询请求,这个请求会通过Binder跑到B应用的进程里,B应用处理完再把结果返回。整个过程对开发者来说是透明的,你只需要调用ContentResolver的接口就行。
核心要点:ContentProvider是Android四大组件之一,它封装了数据存取逻辑,对外提供统一的接口。调用方通过ContentResolver来操作,不需要关心数据源是SQLite、文件还是网络。
我个人习惯把ContentProvider理解成一个"数据服务员"。你告诉它你想干什么(查、增、改、删),它帮你搞定,然后把结果给你。至于它背后用的是数据库还是别的什么,你不用管。
11.2 URI匹配机制
URI是ContentProvider的"门牌号"。每个ContentProvider都有一个唯一的authority,比如com.example.provider。调用方通过URI来定位要操作的数据。
一个典型的URI长这样:
content://com.example.provider/user/1
拆开来看:
content://:协议头,固定写法com.example.provider:authority,标识哪个Provideruser:路径,表示操作哪张表或哪类数据1:ID,表示具体某条记录
Provider内部怎么知道这个URI对应什么操作呢?靠的是UriMatcher。我在项目中见过不少新手直接写if-else去判断URI,那代码简直没法看。
我的习惯:在Provider的静态代码块里初始化UriMatcher,把所有支持的URI模式都注册好。然后在query、insert等方法里直接switch匹配结果,代码干净又高效。
举个例子:
private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
private static final int USERS = 1;
private static final int USER_ID = 2;
static {
sUriMatcher.addURI("com.example.provider", "user", USERS);
sUriMatcher.addURI("com.example.provider", "user/#", USER_ID);
}
#是通配符,匹配任意数字。*匹配任意文本。嗯,这里要注意,通配符别用反了。
11.3 CRUD操作详解
CRUD就是增删改查。ContentProvider提供了四个核心方法:query、insert、update、delete。调用方通过ContentResolver来调用。
我曾经遇到过一个坑:在query方法里直接返回了数据库的Cursor,结果调用方那边一关闭Cursor,Provider这边就崩溃了。后来才意识到,必须用ContentResolver来管理Cursor的生命周期。
来看一个标准的query实现:
@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 USERS:
cursor = db.query("user", projection, selection, selectionArgs, null, null, sortOrder);
break;
case USER_ID:
String id = uri.getLastPathSegment();
cursor = db.query("user", projection, "_id=?", new String[]{id}, null, null, sortOrder);
break;
default:
throw new IllegalArgumentException("Unknown URI: " + uri);
}
// 设置通知URI,让调用方能感知数据变化
cursor.setNotificationUri(getContext().getContentResolver(), uri);
return cursor;
}
注意最后那行setNotificationUri,这个很关键。调用方可以通过ContentResolver.registerContentObserver来监听数据变化。我在做即时通讯项目时,就用这个机制实现了消息的实时刷新,比轮询优雅多了。
避坑指南:我曾经在insert方法里忘记调用getContext().getContentResolver().notifyChange(uri, null),结果调用方那边怎么都收不到数据变化的通知。折腾了半天才发现是少了这行代码。切记,数据变化后一定要通知。
11.4 权限校验
数据共享不是无条件的。ContentProvider支持细粒度的权限控制。你可以在AndroidManifest.xml里声明读权限和写权限。
<provider
android:name=".MyProvider"
android:authorities="com.example.provider"
android:readPermission="com.example.permission.READ_DATA"
android:writePermission="com.example.permission.WRITE_DATA" />
调用方想读写你的数据,必须在自己的manifest里声明对应的权限:
<uses-permission android:name="com.example.permission.READ_DATA" />
除了静态权限,你还可以在Provider内部做动态校验。比如根据不同的URI路径,要求不同的权限。我做过一个项目,普通用户只能查自己的数据,管理员才能查全部。就是在query方法里通过checkCallingPermission来做的。
权限校验的两种方式:
- 静态声明:在manifest里声明readPermission和writePermission,系统自动校验
- 动态校验:在代码里调用
checkCallingPermission或enforceCallingPermission,灵活控制
嗯,这里要提醒一下:如果你的Provider只给自己用,或者只在同一个App的不同进程间共享,可以考虑用android:exported="false"来禁止外部访问。别图省事不写,安全无小事。
11.5 知识体系总览
下面这张图是我自己整理的ContentProvider知识体系,涵盖了核心流程和关键节点。你把它理清楚了,面试和实战都不怕。
ContentProvider看起来复杂,其实核心就这几块:跨进程通信靠Binder,URI匹配靠UriMatcher,CRUD操作要记得通知变化,权限校验要内外结合。把这些搞明白了,你就能写出安全又高效的数据共享组件。
最后说一句:如果你只是想在同一个App的不同进程间共享数据,可以考虑用android:multiprocess="true"或者直接用ContentProvider的call方法。但如果是跨App共享,老老实实把权限声明好,别偷懒。
公众号:蓝海资料掘金营,微信deep3321