Android进阶知识树——ContentProvider使用和工做过程详解

前言

数据库是Android开发中最基本的数据保存方式,但因为数据库的私有性,咱们没法对外提供或获取信息,当两个应用须要实现数据共享时,此时就须要本篇文章的主题——ContentProviderjava

一、Uri基础

在使用ContentProvider以前,先介绍下Uri基础,Uri的对于开发者来讲应该并不陌生,开发中使用Uri之处有不少,如:AppLink、FileProvider等,他们的做用相同都是定位资源位置,不一样的是此处定义的是数据库中的信息;android

  • Uri的四个组成部分:content://contacts/people/5
  1. schema:已由Android固定设置为content://
  2. authority:ContentProvider权限,在AndroidMenifest中设置权限
  3. path:要操做的数据库表
  4. Id:查询的关键字(可选字段)
  • Uri匹配模式

Uri的匹配表示要查询的数据,对于单个数据查询,可直接使用Uri定位具体的资源位置,但当范围查询时就须要结合通配符的使用,Uri提供如下两种通配符:数据库

  1. *:匹配由任意长度的任何有效字符组成的字符串
  2. #:匹配由任意长度的数字字符组成的字符串
content://com.example.app.provider/table2/*  //多数据查询
content://com.example.app.provider/table3/#
content://com.example.app.provider/table3/6  //单数据查询
复制代码
  • Uri的转换
Uri uri = Uri.parse(“content://contacts/people/5") 复制代码
  • Uri建立
//经过将 ID 值追加到 URI 末尾来访问表中的单个行
Uri singleUri = ContentUris.withAppendedId(UserDictionary.Words.CONTENT_URI,4);
复制代码

二、ContentProvider使用

ContentProvider通常配合数据库共同使用,实现对外共享数据的目的,因此它须要对数据库的增删改查操做,ContentProvider也为咱们提供了相应的操做方法,使用时只需实现便可,下面按照使用步骤实现一个ContentProvider:数组

  • 建立ContentProvider的子类,重写insert、update、query、delete、getType
  • 添加UriMatcher 映射数据表

UriMatcher的做用是在使用Uri操做数据库时,根据发起请求的Uri和配置好的uriMatcher肯定本次操做的数据表bash

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) //#匹配任意长度的的数字
}
复制代码
  • insert ():添加数据
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;
}
复制代码

使用细节:app

  1. dataBase添加后返回添加的id,此时使用withAppendedId ()和id建立添加的Uri
  2. insert()最终返回的是本次新添加数据的newUri
  3. 使用 ContentUris.parseId() 能够从newUri中获取本次添加数据的_ID
  • query():查询方法

ContentProvider的查询和数据库查询同样,支持条件查询和多数据查询,返回结果为查询Cursor实例ide

//当查询整个数据表时. 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) 复制代码
  • ContentObserver

提到ContentProvider的使用就会想到ContentObserver,这里一块儿介绍下ContentObserver,采用观察者模式在存储的数据发生修改时自动触发回调,使用起来也很简单建立ContentObserver的实例完成注册便可:ui

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())
            }
        }
    }
复制代码

当数据添加时自动查询全部数据: this

在这里插入图片描述

三、MimeType理解和使用

3.一、简介
  • 做用:用来标识当前所能打开的文件类型
  • 使用场景:隐式启动Activity
  1. 在清单文件中配置隐式启动的action
<intent-filter>
    <action android:name="com.alex.kotlin.job.JobScheduleActivity"/>
    <category android:name="android.intent.category.DEFAULT"/>
    <data android:mimeType="text/plain”/> //生明匹配的数据类型 </intent-filter> 复制代码
  1. 建立Intent实例配置启动Action并设置数据类型
val intent = Intent("com.alex.kotlin.job.JobScheduleActivity")
intent.type = "text/plain” // 设置数据库类型,只有与清单文件中设置的一致才能启动 startActivity(intent) 复制代码
3.二、ContentProvider中的MimeType
  • ContentProvider根据操做的对象不一样提供两种MimeType
  1. getType():必须实现的查询数据的类型
  2. getStreamTypes():对外提供文件时须要实现方法,根据查询的Uri返回匹配的类型数组
  • Table MimeType系统提供两种类型:
  1. vnd.android.cursor.item/***:用于单个行操做的URI模式
  2. vnd.android.cursor.dir/***:用于多行操做的URI模式
  • 文件MIME类型(例如程序对外提供 .jpg、.png、.gif 形式对外提供照片)
  1. 若是在获取时使用“image/*”过滤,则getStreamTypes()返回 数组 { "image/jpg", "image/png", "image/gif"}
  2. 若是获取时使用“/jpg”过滤,则返回数组{"image/jpg”}
  3. 若是获取时的Uri不匹配提供内容,则返回null
3.三、使用ContentProvider隐式启动上面的Activity
  • 修改上面清单文件中的
<data android:mimeType="vnd.android.cursor.dir/test"/>
复制代码
  • 修改ContentProvider的注册文件,设置匹配的意图
<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>
复制代码
  • 建立ContentProvider的实现类,并配置UriMatcher,在getType中根据Uri返回MimeType
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 } 复制代码
  • 使用Uri访问,便会直接打开JobScheduleActivity
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;spa

由上面的代码看出建立Intent时设置了意图过滤action,那么mimeType呢?其实在使用Uri.parse("content://com.alex.kotlin.job.provider/path”) 设置intent.data时会启动上面配置的ContentProvider,在ContentProvider返回Uri模式"vnd.android.cursor.dir/test”正好匹配清单中的data数据类型,因此会直接启动JobScheduleActivity

四、权限

  • 指定其余应用访问提供程序的数据所必须具有权限的属性:
  1. android:grantUriPermssions:表示是否能够经过临时权限访问数据,默认为false,在开发中能够只对限定的内容提供临时权限,如照片的内容 URI 设置临时权限
//该属性的值决定可访问的提供程序范围,若是设置为true,系统会像整个系统授予临时权限,并替代其余设置的权限
android:grantUriPermissions=“true" //设置为false,则需添加<grant-uri-permission>并代表能够受权临时权限所对应的URI android:grantUriPermissions=“false"

<grant-uri-permission android:path=“string” // path表示绝对路径Uri
    android:pathPattern=“string” // 表示限定完整的路径但可使用./*通配符匹配
    android:pathPrefix="string" /> //限定路径的初始部分后面能够变化,只要初始部分符合便可受权
复制代码
  1. android:permission:统一提供程序范围读取/写入权限
  2. android:readPermission:提供程序范围读取权限,优先于permission权限
  3. android:writePermission:提供程序范围写入权限,优先于permission权限
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"/>
复制代码

五、ContentProvider工做过程

  • 使用
contentResolver.query(......)
复制代码

ContentProvider的使用是经过ContentResolver实例进行操做的,因此工做原理分析从调用getContentResolver()获取ContentResolver实例

  • getContentResolver()
@Override
public ContentResolver getContentResolver() {
    return mBase.getContentResolver(); 
}
复制代码

getContentResolver() 获取的是ContextImpl.ApplicationContentResolver()实例,而ApplicationContentResolver继承了ContentResolver,本次对ContentProvider的分析以query()为例,contentResolver.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);
  }
复制代码
  • ActivityThread.acquireProvider()
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()

  • ActivityManagerService.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()执行过程分两步:

  1. 若是应用进程已经启动则调用proc.thread.scheduleInstallProvider(cpi)启动ContentProvider
  2. 若是应用进程未启动则执行startProcessLocked(),而后启动ContentProvider

对于已启动的进程直接调用Application.scheduleInstallProvider()启动ContentProvider

  • Application.scheduleInstallProvider()
public void scheduleInstallProvider(ProviderInfo provider) {
 sendMessage(H.INSTALL_PROVIDER, provider); //发送Message信息,执行handleInstallProvider()
}
复制代码

handleInstallProvider()中又调用installContentProviders()方法,对于应用进程已启动的分析,先暂停此处,下面分析如下应用进程未启动的情况,首先执行startProcessLocked(),启动应用进程并初始化ContentProvider

  • 启动应用进程 ()

应用进程启动后调用ActivityThread.main(),初始化消息队列,建立ActivityThread实例并调用attach()方法

  • ActivityThread.main()
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);
复制代码
  • bindApplication()
sendMessage(H.BIND_APPLICATION, data);
复制代码

bindApplication()中发送Message消息,Handler接收消息后执行handleBindApplication()

  • 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
}
复制代码

执行工做过程:

  1. 建立ContextImpl的实例
  2. 使用反射建立Instrumentation实例
  3. 调用makeApplication()建立Application实例
  4. installContentProviders()执行建立和初始化ContentProvider
  5. 调用 mInstrumentation.callApplicationOnCreate(app)执行Appliocation的onCreate()

上述过程执行启动应用程序和初始化Application以后,调用 installContentProviders(),这里和上面第一种状况同样都执行到installContentProviders方法,因此此处就接着第一种状况一块儿分析,在installContentProviders方法中回调用nstallProvider()

  • installProvider(ProviderInfo info)
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(); //
复制代码

执行过程:

  1. 反射获取实例ClassLoader
  2. 建立ContentProvider实例
  3. 调用ContentProvider的attachInfo()
  4. attachInfo()中调用ContentProvider.this.onCreate(),初始化ContentProvider ContentProvider.this.onCreate()执行后,ContentProvider就完成了整个启动过程,后面就能够调用每一个方法执行相应的操做了

六、总结

到此ContentProvider的整个使用和工做过程就分析完了,较四大组件中其余三个而言,ContentProvider的启动状况略微复杂,这也符合它跨进程跨程序的功能,以前很早就分析过它的工做过程,但没有整理和输出,经过此篇文章的编写和分析,加深了对ContentProvider的使用和原理的理解。

相关文章
相关标签/搜索