前言
資料庫是Android開發中最基本的資料儲存方式,但由於資料庫的私有性,我們無法對外提供或獲取資訊,當兩個應用需要實現資料共享時,此時就需要本篇文章的主題——ContentProvider
1、Uri基礎
在使用ContentProvider之前,先介紹下Uri基礎,Uri的對於開發者來說應該並不陌生,開發中使用Uri之處有很多,如:AppLink、FileProvider等,他們的作用相同都是定位資源位置,不同的是此處定義的是資料庫中的資訊;
- Uri的四個組成部分:content://contacts/people/5
- schema:已由Android固定設定為content://
- authority:ContentProvider許可權,在AndroidMenifest中設定許可權
- path:要操作的資料庫表
- Id:查詢的關鍵字(可選欄位)
- Uri匹配模式
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 = Uri.parse(“content://contacts/people/5")複製程式碼
- Uri建立
//通過將 ID 值追加到 URI 末尾來訪問表中的單個行Uri singleUri = ContentUris.withAppendedId(UserDictionary.Words.CONTENT_URI,4);
複製程式碼
2、ContentProvider使用
ContentProvider一般配合資料庫共同使用,實現對外共享資料的目的,所以它需要對資料庫的增刪改查操作,ContentProvider也為我們提供了相應的操作方法,使用時只需實現即可,下面按照使用步驟實現一個ContentProvider:
- 建立ContentProvider的子類,重寫insert、update、query、delete、getType
- 新增UriMatcher 對映資料表
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) //#匹配任意長度的的數字
}複製程式碼
- insert ():新增資料
public Uri insert(Uri uri,ContentValues contentValues){long newId = 0;
Uri newUri = null;
switch (uriMatcher.match(uri)){newId = dataBase.insert(…) //此處的newId表示插入資料的idnewUri = Uri.parse(content://authoriy/**table/newId)
}return newUri;
}複製程式碼
使用細節:
- dataBase新增後返回新增的id,此時使用withAppendedId ()和id建立新增的Uri
- insert()最終返回的是本次新新增資料的newUri
- 使用 ContentUris.parseId() 可以從newUri中獲取本次新增資料的_ID
- query():查詢方法
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)複製程式碼
- ContentObserver
提到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())
}
}
}複製程式碼
當資料新增時自動查詢所有資料:
3、MimeType理解和使用
3.1、簡介
- 作用:用來標識當前所能開啟的檔案型別
- 使用場景:隱式啟動Activity
- 在清單檔案中配置隱式啟動的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>
複製程式碼
- 建立Intent例項配置啟動Action並設定資料型別
val intent = Intent("com.alex.kotlin.job.JobScheduleActivity")intent.type = "text/plain” // 設定資料庫型別,只有與清單檔案中設定的一致才能啟動startActivity(intent)複製程式碼
3.2、ContentProvider中的MimeType
- ContentProvider根據操作的物件不同提供兩種MimeType
- getType():必須實現的查詢資料的型別
- getStreamTypes():對外提供檔案時需要實現方法,根據查詢的Uri返回匹配的型別陣列
- Table MimeType系統提供兩種型別:
- vnd.android.cursor.item/***:用於單個行操作的URI模式
- vnd.android.cursor.dir/***:用於多行操作的URI模式
- 檔案MIME型別(例如程式對外提供 .jpg、.png、.gif 形式對外提供照片)
- 如果在獲取時使用“image/*”過濾,則getStreamTypes()返回 陣列 {
“image/jpg”, “image/png”, “image/gif”
} - 如果獲取時使用“/jpg”過濾,則返回陣列{“image/jpg”
} - 如果獲取時的Uri不匹配提供內容,則返回null
3.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”) //配置actionintent.data = Uri.parse("content://com.alex.kotlin.job.provider/path”) //設定UristartActivity(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
4、許可權
- 指定其他應用訪問提供程式的資料所必須具備許可權的屬性:
- android:grantUriPermssions:表示是否可以通過臨時許可權訪問資料,預設為false,在開發中可以只對限定的內容提供臨時許可權,如照片的內容 URI 設定臨時許可權
//該屬性的值決定可訪問的提供程式範圍,如果設定為true,系統會像整個系統授予臨時許可權,並替代其他設定的許可權android:grantUriPermissions=“true"//設定為false,則需新增<
grant-uri-permission>
並表明可以授權臨時許可權所對應的URIandroid:grantUriPermissions=“false"<
grant-uri-permission android:path=“string” // path表示絕對路徑Uri android:pathPattern=“string” // 表示限定完整的路徑但可以使用./*萬用字元匹配 android:pathPrefix="string" />
//限定路徑的初始部分後面可以變化,只要初始部分符合即可授權複製程式碼
- android:permission:統一提供程式範圍讀取/寫入許可權
- android:readPermission:提供程式範圍讀取許可權,優先於permission許可權
- 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"/>
複製程式碼
5、ContentProvider工作過程
- 使用
contentResolver.query(......)複製程式碼
ContentProvider的使用是通過ContentResolver例項進行操作的,所以工作原理分析從呼叫getContentResolver()獲取ContentResolver例項
- getContentResolver()
@Overridepublic 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()執行過程分兩步:
- 如果應用程式已經啟動則呼叫proc.thread.scheduleInstallProvider(cpi)啟動ContentProvider
- 如果應用程式未啟動則執行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
}複製程式碼
執行工作過程:
- 建立ContextImpl的例項
- 使用反射建立Instrumentation例項
- 呼叫makeApplication()建立Application例項
- installContentProviders()執行建立和初始化ContentProvider
- 呼叫 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.attachInfoContentProvider.this.onCreate();
//複製程式碼
執行過程:
- 反射獲取例項ClassLoader
- 建立ContentProvider例項
- 呼叫ContentProvider的attachInfo()
- attachInfo()中呼叫ContentProvider.this.onCreate(),初始化ContentProviderContentProvider.this.onCreate()執行後,ContentProvider就完成了整個啟動過程,後面就可以呼叫每個方法執行相應的操作了
6、總結
到此ContentProvider的整個使用和工作過程就分析完了,較四大元件中其他三個而言,ContentProvider的啟動情況略微複雜,這也符合它跨程式跨程式的功能,之前很早就分析過它的工作過程,但沒有整理和輸出,通過此篇文章的編寫和分析,加深了對ContentProvider的使用和原理的理解。