数据库是Android开发中最基本的数据保存方式,但由于数据库的私有性,我们无法对外提供或获取信息,当两个应用需要实现数据共享时,此时就需要本篇文章的主题——ContentProvider
在使用ContentProvider之前,先介绍下Uri基础,Uri的对于开发者来说应该并不陌生,开发中使用Uri之处有很多,如:AppLink、FileProvider等,他们的作用相同都是定位资源位置,不同的是此处定义的是数据库中的信息;
Uri的匹配表示要查询的数据,对于单个数据查询,可直接使用Uri定位具体的资源位置,但当范围查询时就需要结合通配符的使用,Uri提供以下两种通配符:
content://com.example.app.provider/table2/* //多数据查询
content://com.example.app.provider/table3/#
content://com.example.app.provider/table3/6 //单数据查询
Uri uri = Uri.parse(“content://contacts/people/5")
//通过将 ID 值追加到 URI 末尾来访问表中的单个行
Uri singleUri = ContentUris.withAppendedId(UserDictionary.Words.CONTENT_URI,4);
ContentProvider一般配合数据库共同使用,实现对外共享数据的目的,所以它需要对数据库的增删改查操作,ContentProvider也为我们提供了相应的操作方法,使用时只需实现即可,下面按照使用步骤实现一个ContentProvider:
UriMatcher的作用是在使用Uri操作数据库时,根据发起请求的Uri和配置好的uriMatcher确定本次操作的数据表
static UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH)
Static{
uriMatcher.addURI(AUTHORIY,”userinfo”,1) //添加userinfo表映射
uriMatcher.addURI(AUTHORIY,”userinfo/*”,2) //*表示匹配任意长度任意字符
uriMatcher.addURI(AUTHORIY,”userinfo/#”,3) //#匹配任意长度的的数字
}
public Uri insert(Uri uri,ContentValues contentValues){
long newId = 0;
Uri newUri = null;
switch (uriMatcher.match(uri)){
newId = dataBase.insert(…) //此处的newId表示插入数据的id
newUri = Uri.parse(content://authoriy/**table/newId)
}
return newUri;
}
使用细节:
ContentProvider的查询和数据库查询一样,支持条件查询和多数据查询,返回结果为查询Cursor实例
//当查询整个数据表时. Uri.parse(”content://com.book.jtm/userinfo")
dataBase.query(….)
//当查询具体一个数据. Uri.parse(”content://com.book.jtm/userinfo/123456”)
String id = uri.getPathSegments().get(1)
//调价查询时
dataBase.query(table, projection,”tel_number = ?”,new String[]{id},null, null,sortOrder)
提到ContentProvider的使用就会想到ContentObserver,这里一起介绍下ContentObserver,采用观察者模式在存储的数据发生修改时自动触发回调,使用起来也很简单创建ContentObserver的实例完成注册即可:
val contentObservable = object : ContentObserver(handler){
override fun onChange(selfChange: Boolean, uri: Uri?) {
super.onChange(selfChange, uri)
val cursor = contentResolver.query(uri, arrayOf("_id","name"),null,null,null)
if (cursor != null && cursor.moveToFirst()) {
do {
Log.e("========", cursor.getInt(cursor.getColumnIndex("_id")).toString())
Log.e("========", cursor.getString(cursor.getColumnIndex("name")).toString())
}while (cursor.moveToNext())
}
}
}
<intent-filter>
<action android:name="com.alex.kotlin.job.JobScheduleActivity"/>
<category android:name="android.intent.category.DEFAULT"/>
<data android:mimeType="text/plain”/> //生明匹配的数据类型
</intent-filter>
val intent = Intent("com.alex.kotlin.job.JobScheduleActivity")
intent.type = "text/plain” // 设置数据库类型,只有与清单文件中设置的一致才能启动
startActivity(intent)
<data android:mimeType="vnd.android.cursor.dir/test"/>
<provider
android:authorities="com.alex.kotlin.job.provider"
android:name=".Provider">
<intent-filter>
<category android:name="android.intent.category.DEFAULT" />
<data
android:host="com.alex.kotlin.job.provider"
android:pathPrefix="/path"
android:scheme="content" />
</intent-filter>
</provider>
usrSwitch.addURI(AUTHORITY, "path", 1)
const val data = "vnd.android.cursor.dir/test” //声明要返回的MIMEType类型
override fun getType(uri: Uri): String? {
when(usrSwitch.match(uri)){
1 -> { return data}
}
return null
}
val intent = Intent("com.alex.kotlin.job.JobScheduleActivity”) //配置action
intent.data = Uri.parse("content://com.alex.kotlin.job.provider/path”) //设置Uri
startActivity(intent)
有没有想过为什么可以启动活动呢?静态启动Activity的两个条件:必须匹配意图过滤的action和mimeType;
由上面的代码看出创建Intent时设置了意图过滤action,那么mimeType呢?其实在使用Uri.parse("content://com.alex.kotlin.job.provider/path”) 设置intent.data时会启动上面配置的ContentProvider,在ContentProvider返回Uri模式"vnd.android.cursor.dir/test”正好匹配清单中的data数据类型,所以会直接启动JobScheduleActivity
//该属性的值决定可访问的提供程序范围,如果设置为true,系统会像整个系统授予临时权限,并替代其他设置的权限
android:grantUriPermissions=“true"
//设置为false,则需添加并表明可以授权临时权限所对应的URI
android:grantUriPermissions=“false"
<grant-uri-permission android:path=“string” // path表示绝对路径Uri
android:pathPattern=“string” // 表示限定完整的路径但可以使用./*通配符匹配
android:pathPrefix="string" /> //限定路径的初始部分后面可以变化,只要初始部分符合即可授权
android:readPermission="com.alex.kotlin.job.provider.permission.READ_PERMISSION"
android:writePermission="com.alex.kotlin.job.provider.permission.WRITE_PERMISSION"
android:permission="com.alex.kotlin.job.provider.permission.PERMISSION"
使用一个实例验证权限的使用,创建两个程序A和B,在程序A中使用ContentProvider保存数据,在程序B中进行查询,在开始A程序中不设置任何权限,B程序进行访问数据,系统直接报错:
报错原因也很直接,没有权限访问,此时是因为A程序中的Privider没有支持其他进程使用,修改A程序清单文件添加android:exported=“true”,再次访问数据访问成功:
从Log中可以看出获取的进程包为“baselibrary",而提供数据的包为“job.provider”,可见二者并不是同一个程序;
android:writePermission="com.alex.kotlin.job.provider.WRITE"
android:readPermission="com.alex.kotlin.job.provider.READ"
在A程序的清单文件中,为Provider添加两个读写权限,添加完权限后再次在B程序中获取数据,还是会报错,也很正常因为已经对数据的访问设置了门槛,所以在B程序中声明读写权限即可:
<uses-permission android:name="com.alex.kotlin.job.provider.READ"/>
<uses-permission android:name="com.alex.kotlin.job.provider.WRITE"/>
contentResolver.query(......)
ContentProvider的使用是通过ContentResolver实例进行操作的,所以工作原理分析从调用getContentResolver()获取ContentResolver实例
@Override
public ContentResolver getContentResolver() {
return mBase.getContentResolver();
}
getContentResolver() 获取的是ContextImpl.ApplicationContentResolver()实例,而ApplicationContentResolver继承了ContentResolver,本次对ContentProvider的分析以query()为例,contentResolver.query(…)调用的是ContentResolver.query()
public final @Nullable Cursor query(final @RequiresPermission.Read @NonNull Uri uri) {
IContentProvider unstableProvider = acquireUnstableProvider(uri);
qCursor = unstableProvider.query(mPackageName, uri, projection,
queryArgs, remoteCancellationSignal);
}
query()中首先调用acquireUnstableProvider(uri)获取IContentProvider实例,acquireUnstableProvider中调用了ContentResolver.acquireUnstableProvider(),ApplicationContentResolver继承了ContentResolver,此处实际执行的是ApplicationContentResolver.acquireUnstableProvider(),acquireUnstableProvider()中又调用ActivityThread.acquireProvider()
@Override
protected IContentProvider acquireUnstableProvider(Context c, String auth) {
return mMainThread.acquireProvider(c,
ContentProvider.getAuthorityWithoutUserId(auth),
resolveUserIdFromAuthority(auth), false);
}
Context c, String auth, int userId, boolean stable) {
final IContentProvider provider = acquireExistingProvider(c, auth, userId, stable);
if (provider != null) {
return provider;
}
synchronized (getGetProviderLock(auth, userId)) {
holder = ActivityManager.getService().getContentProvider(
getApplicationThread(), auth, userId, stable);
}
acquireProvider中,首先从ArrayMap中获取IContentProvider,如果获取成功则直接返回,若ArrayMap中不存在则ActivityManagerService.getContentProvider启动Provider,getContentProvide()中调用getContentProviderImpl()
if (proc != null && proc.thread != null && !proc.killed) {
if (!proc.pubProviders.containsKey(cpi.name)) {
proc.pubProviders.put(cpi.name, cpr);
try {
proc.thread.scheduleInstallProvider(cpi);
} catch (RemoteException e) {
}
}
} else {
proc = startProcessLocked(cpi.processName,
cpr.appInfo, false, 0, "content provider",
new ComponentName(cpi.applicationInfo.packageName,
cpi.name), false, false, false);
return null;
}
}
getContentProviderImpl()执行过程分两步:
对于已启动的进程直接调用Application.scheduleInstallProvider()启动ContentProvider
public void scheduleInstallProvider(ProviderInfo provider) {
sendMessage(H.INSTALL_PROVIDER, provider); //发送Message信息,执行handleInstallProvider()
}
handleInstallProvider()中又调用installContentProviders()方法,对于应用进程已启动的分析,先暂停此处,下面分析以下应用进程未启动的状况,首先执行startProcessLocked(),启动应用进程并初始化ContentProvider
应用进程启动后调用ActivityThread.main(),初始化消息队列,创建ActivityThread实例并调用attach()方法
ActivityThread thread = new ActivityThread();
thread.attach(false);
main方法中执行thread.attach()方法,attach()中又调用了IActivityManager.attachApplication(),ActivityManagerService 是IActivityManager的代理类,此处执行的ActivityManagerService.attachApplication(),attachApplication()中又调用attachApplicationLocked(),attachApplicationLocked中调用I Application.bindApplication()
thread.bindApplication(processName, appInfo, providers,
app.instr.mClass,
......
buildSerial, isAutofillCompatEnabled);
sendMessage(H.BIND_APPLICATION, data);
bindApplication()中发送Message消息,Handler接收消息后执行handleBindApplication()
private void handleBindApplication(AppBindData data) {
final ContextImpl appContext = ContextImpl.createAppContext(this, data.info);//1
try {
final ClassLoader cl = instrContext.getClassLoader();
mInstrumentation = (Instrumentation)
cl.loadClass(data.instrumentationName.getClassName()).newInstance();//2
} catch (Exception e) {...}
final ComponentName component = new ComponentName(ii.packageName, ii.name);
mInstrumentation.init(this, instrContext, appContext, component,
data.instrumentationWatcher, data.instrumentationUiAutomationConnection);//3
Application app = data.info.makeApplication(data.restrictedBackupMode, null);//4
mInitialApplication = app;
if (!data.restrictedBackupMode) {
if (!ArrayUtils.isEmpty(data.providers)) {
installContentProviders(app, data.providers);//5
mH.sendEmptyMessageDelayed(H.ENABLE_JIT, 10*1000);
}
}
mInstrumentation.callApplicationOnCreate(app);//6
}
执行工作过程:
上述过程执行启动应用程序和初始化Application之后,调用 installContentProviders(),这里和上面第一种情况一样都执行到installContentProviders方法,所以此处就接着第一种情况一起分析,在installContentProviders方法中回调用nstallProvider()
final java.lang.ClassLoader cl = c.getClassLoader(); //
LoadedApk packageInfo = peekPackageInfo(ai.packageName, true);
if (packageInfo == null) {
packageInfo = getSystemContext().mPackageInfo;
}
localProvider = packageInfo.getAppFactory()。//
.instantiateProvider(cl, info.name);
provider = localProvider.getIContentProvider();
localProvider.attachInfo(c, info); //
//localProvider.attachInfo
ContentProvider.this.onCreate(); //
执行过程:
到此ContentProvider的整个使用和工作过程就分析完了,较四大组件中其他三个而言,ContentProvider的启动情况略微复杂,这也符合它跨进程跨程序的功能,之前很早就分析过它的工作过程,但没有整理和输出,通过此篇文章的编写和分析,加深了对ContentProvider的使用和原理的理解。