目錄

前置知識
這篇文章的內(nèi)容會涉及以下前置 / 相關(guān)知識,貼心的我都幫你準(zhǔn)備好了,請享用~
- Binder 機(jī)制
1. 概述
1.1 作用
ContentProvider 是進(jìn)程間內(nèi)容共享的統(tǒng)一接口。注意:ContentProvider 的作用不是實現(xiàn)進(jìn)程間通信,它只是為進(jìn)程間通信提供了一套統(tǒng)一接口,真正實現(xiàn)進(jìn)程間通信的是底層的 Binder 機(jī)制。

1.2 優(yōu)點:透明地提供內(nèi)容
使用 ContentProvider 允許應(yīng)用透明地將數(shù)據(jù)開放給其它應(yīng)用,無論底層數(shù)據(jù)采用何種實現(xiàn)方式(網(wǎng)絡(luò)、內(nèi)存、文件或數(shù)據(jù)庫),外界對于數(shù)據(jù)的訪問方式都是統(tǒng)一的 & 固定的。外界只關(guān)心采用 CURD 來訪問 ContentProvider 的數(shù)據(jù),至于其內(nèi)部數(shù)據(jù)的實現(xiàn)是采用文件存儲還是數(shù)據(jù)庫存儲,外界是不感知的。

1.3 ContentProvider 是單例嗎?
通常來說,ContentProvider 是單例的,特殊情況可以設(shè)置android:multiprocess屬性來決定是不是單例:當(dāng)屬性值為 true 時,每個調(diào)用者進(jìn)程都會存在一個 ContentProvider 實例,官方的解釋是可以避免進(jìn)程間通訊的開銷,但是這種方式在實際開發(fā)中很少運(yùn)用。因此我們說一般情況下 ContentProvider 是單例的,只在服務(wù)提供進(jìn)程創(chuàng)建實例。
2. 相關(guān)概念
2.1 統(tǒng)一資源標(biāo)識符(URI)
統(tǒng)一資源標(biāo)識符(Uniform Resource Indentifier)的作用是 唯一標(biāo)識 ContentProvider 的數(shù)據(jù)。在通過 ContentResolver 解析數(shù)據(jù)時,URI 是必要的參數(shù),其遵循的格式體現(xiàn)在ContentUris.java:
Content URIs have the syntax:content://authority/path/id
可以看到,URI 遵循固定的格式,一共分為四個部分:[圖片上傳失敗...(image-df2b78-1666750926778)]
| 元素 | 描述 |
|---|---|
| schema(方案) | 固定為 content:// |
| authority(權(quán)威) | 標(biāo)識 ContentProvider 的唯一字符串,對應(yīng)于注冊時指定的 android:authority 屬性 |
| path(路徑) | 標(biāo)識 authority 數(shù)據(jù)的某些子集 |
| id(記錄 id) | 標(biāo)識 path 子集中的某個記錄(不指定是標(biāo)識全部記錄) |
系統(tǒng)預(yù)置了一些 ContentProvider,例如通訊錄、媒體資源等,這里舉出一些常用的系統(tǒng) ContentProvider 的 Authority,它們的接口約定定義在目錄/android.provider:
| Authority | 描述 |
|---|---|
| com.android.contacts | 通訊錄 |
| media | 媒體 |
| com.android.calendar | 日歷 |
| user_dictionary | 用戶詞典 |
2.2 MIME 數(shù)據(jù)類型
MIME類型(Multipurpose Internal Mail Extensions,多用途互聯(lián)網(wǎng)郵件擴(kuò)展類型)是一種互聯(lián)網(wǎng)標(biāo)準(zhǔn),用于指定某種擴(kuò)展名的文件與應(yīng)用程序的對應(yīng)關(guān)系。一個 MIME 類型分為「主類型」+「子類型」,例如 .html 文件對應(yīng)的 MIME 類型為 text/html,其中 text 為主類型,html 為子類型。
在 ContentProvider 中,通過 getType(Uri) 方法來確定 URI 對應(yīng)的 MIME 類型,返回值可以返回 標(biāo)準(zhǔn) MIME 類型或者自定義 MIME 類型,這是一個抽象方法,需要由子類實現(xiàn):
ContentProvider.java
public abstract String getType(Uri uri);
2.2.1 標(biāo)準(zhǔn) MIME 類型
標(biāo)準(zhǔn) MIME 類型中常見的主類型有:
- 聲音:audio
- 視頻:video
- 圖像:image
- 文本:text
對應(yīng)的 MIMIE 類型舉例:
| 擴(kuò)展名 | MIME |
|---|---|
| .html | text/html |
| .txt | text/plain |
| .png | image/png |
| .jpeg | image/jpeg |
2.2.2 自定義 MIME 類型
在 Android 中,自定義 MIME 類型的主類型只有兩種:
-
vnd.android.cursor.item:單行記錄 -
vnd.android.cursor.dir:多行記錄(集合)
例如通訊錄 ContentProvider 定義了兩種 MIME 類型,分別表示多條記錄和單條記錄:
ContactsContract.java
/**
* The MIME type of {@link #CONTENT_URI} providing a directory of contact directories.
*/
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/contact_directories";
/**
* The MIME type of a {@link #CONTENT_URI} item.
*/
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/contact_directory";
3. 主要方法
ContentProvider 使用表格的形式管理數(shù)據(jù),對外暴露四個操作方法,分別是:添加、刪除、更新、查詢(insert、delete、update、query):
添加數(shù)據(jù)(Binder 線程)
public abstract Uri insert(Uri uri, ContentValues values);
刪除數(shù)據(jù)(Binder 線程)
public abstract int delete(Uri uri, String selection, String[] selectionArgs);
更新數(shù)據(jù)(Binder 線程)
public abstract int update(Uri uri, ContentValues values, String selection, String[] selectionArgs);
查詢數(shù)據(jù)(Binder 線程)
public abstract Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder);
除了 4 個核心方法外,ContentProvider 還有其他比較重要的方法,例如:
啟動回調(diào)(主線程)
public abstract boolean onCreate();
返回 Uri 對應(yīng)的 MIME 類型(調(diào)用線程)
public abstract String getType(Uri uri);
需要注意:四個核心方法執(zhí)行在 ContentProvider 注冊進(jìn)程,并在 Binder 線程池中執(zhí)行,而不是主線程??紤]到存在多線程并發(fā)訪問,為了保證數(shù)據(jù)安全在實現(xiàn) ContentProvider 是還需要保證線程同步。而 onCreate() 方法執(zhí)行在 ContentProvider 注冊進(jìn)程的主線程,因此不能執(zhí)行耗時操作。關(guān)于 onCreate() 方法的調(diào)用我在 第 4 節(jié) ContentProvider 的啟動過程 中會詳細(xì)介紹。
| 主要方法 | 執(zhí)行線程 |
|---|---|
| insert() | Binder 線程 |
| delete() | Binder 線程 |
| update() | Binder 線程 |
| query() | Binder 線程 |
| onCreate() | 主線程 |
3.1 插入數(shù)據(jù)
要插入一行新數(shù)據(jù),需要使用 ContentProvider#insert(...)。例如,下面程序?qū)⒁粭l日程數(shù)據(jù)插入的系統(tǒng)日歷中:
ContentValues eventValues = new ContentValues();
eventValues.put(CalendarContract.Events.CALENDAR_ID, catId); // 日歷賬號 ID
eventValues.put(CalendarContract.Events.EVENT_TIMEZONE, TimeZone.getDefault().getID()); // 時區(qū)
eventValues.put(CalendarContract.Events.DTSTART, beginTimeMillis); // 開始時間
eventValues.put(CalendarContract.Events.DTEND, endTimeMillis); // 結(jié)束時間
eventValues.put(CalendarContract.Events.TITLE, title); // 標(biāo)題
eventValues.put(CalendarContract.Events.DESCRIPTION, description); // 描述
eventValues.put(CalendarContract.Events.EVENT_LOCATION, location); // 地點
Uri resultUri = context.getContentResolver().insert(CalendarContract.Events.CONTENT_URI, eventValues);
if(null == resultUri) {
// 插入失敗
return;
}
插入成功后會返回該行的 Uri,格式如下:
content://com.android.calendar/events<id_value>
URI 中的 <id_value> 就是該行 _ID 列的值,而前綴 content://com.android.calendar/events 正好就是插入數(shù)據(jù)時使用的 URI。需要注意的是,你不需要指定數(shù)據(jù)的 _ID列,該列是表的主鍵,ContentProvider 會自動維護(hù)該列并分配一個唯一值。而要從 Uri 中提取 _ID 列的值,可以調(diào)用 ContentUris.parseId(...):
ContentUris.java
public static long parseId(Uri contentUri) {
String last = contentUri.getLastPathSegment();
return last == null ? -1 : Long.parseLong(last);
}
提示: 客戶端程序并非直接調(diào)用 ContentProvider#insert(),而是通過 ContentResolver#insert() 間接調(diào)用,下文會提到。
3.2 查詢數(shù)據(jù)
從 ContentProvider 中查詢數(shù)據(jù)的流程主要分為三個步驟:
3.2.1 請求訪問權(quán)限
ContentProvider 程序可以指定其他應(yīng)用程序必須具備的權(quán)限,例如讀取用戶詞典需要android.permission.READ_USER_DICTIONARY,寫入用戶詞典需要android.permission.WRITE_USER_DICTIONARY。
為了獲取 ContentProvider 程序所需的權(quán)限,你的應(yīng)用需要在 Manifest 文件中使用 <uses-permission> 來請求它們。當(dāng) Android Package Manager 安裝 APK 時,會提示用戶應(yīng)用所需要的權(quán)限,用戶繼續(xù)安裝相當(dāng)于隱式授予權(quán)限。當(dāng)然了,在 Android 6.0 以后部分權(quán)限還需要動態(tài)申請。
<uses-permission android:name = “ android.permission.READ_USER_DICTIONARY” >
3.2.2 構(gòu)造查詢條件
ContentProvider 查詢和 SQL 查詢是相似的,如下表對比:
| ContentProvider 查詢 | SQL 查詢 | 作用 |
|---|---|---|
| Uri | FROM table_name | 查詢的數(shù)據(jù)集合 |
| projection | col,col,col... | 查詢結(jié)果所需的列 |
| selectionClause | WHERE col = value | 選擇條件 |
| selectionArgs | (沒有確切地等效項) | 選擇條件參數(shù)(如果 selection )中使用了 ? 占位符 |
| sortOrder | ORDER BY col,col,... | 結(jié)果集 Cursor 的排序規(guī)則 |
cursor = context .getContentResolver().query(
UserDictionary.Words.CONTENT_URI,
projection,
selectionClause,
selectionArgs,
sortOrder);
例如查詢手機(jī)通訊錄:
Uri uri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI;
String[] projection = {
ContactsContract.Contacts._ID,
ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,
ContactsContract.CommonDataKinds.Phone.NUMBER
};
String selectionClause = ContactsContract.CommonDataKinds.Phone.NUMBER + " = ?";
String[] selectionArgs = {"123456"};
Cursor cursor = getContentResolver().query(uri, projection, selectionClause, selectionArgs, "sort_key COLLATE LOCALIZED asc");
此查詢類似于 SQL 查詢:
SELECT _ID, displayName, data1 FROM content://com.android.contacts/data/phones WHERE data1 = "123456" ORDER BY sort_key COLLATE LOCALIZED asc
3.2.3 處理結(jié)果集
查詢結(jié)果是一個 Cursor 對象,處理范例如下:
if (null == mCursor) {
// 失敗
} else if (mCursor.getCount() < 1) {
// 查詢結(jié)果為空
} else {
// 查詢結(jié)果非空
while (cursor.moveToNext()) {
// 聯(lián)系人名稱
String contractName = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
// 聯(lián)系人電話
String phone = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
...
}
cursor.close(); // 記得關(guān)閉結(jié)果集
}
3.3 刪除數(shù)據(jù)
刪除數(shù)據(jù)與查詢類似,需要構(gòu)造查詢條件,刪除操作結(jié)束會返回成功刪除的行數(shù)。
int rowsDeleted = context.getContentResolver().delete(...);
3.4 更新數(shù)據(jù)
更新操作類似于查詢操作和插入操作的結(jié)合體,既需要構(gòu)造 ContentValues 對象,也需要構(gòu)造查詢條件,刪除操作結(jié)束后返回成功修改的行數(shù)。
int rowsUpdated = context.getContentResolver().update(
UserDictionary.Words.CONTENT_URI,
updateValues,
selectionClause,
selectionArgs
);
4. ContentProvider 核心類
4.1 ContentResolver
外界(包括當(dāng)前進(jìn)程的其他組件)無法直接訪問 ContentProvider 的,而是需要通過 ContentResolver 來間接訪問。這種設(shè)計的優(yōu)點是 統(tǒng)一管理應(yīng)用依賴的 ContentProvider,而不需要關(guān)心真正操作的 ContentProvider 實現(xiàn)類。
ContentResolver 是一個抽象類,我們熟悉的 Context#getContentResolver() 獲得的其實是它的子類 ApplicationContentResolver。
Context.java
public abstract ContentResolver getContentResolver();
ContextImpl.java
class ContextImpl extends Context {
private final ApplicationContentResolver mContentResolver;
@Override
public ContentResolver getContentResolver() {
return mContentResolver;
}
private static final class ApplicationContentResolver extends ContentResolver {
private final ActivityThread mMainThread;
@Override
protected IContentProvider acquireProvider(Context context, String auth) {
...
}
@Override
protected IContentProvider acquireExistingProvider(Context context, String auth) {
...
}
@Override
public boolean releaseProvider(IContentProvider provider) {
...
}
...
}
}
在文章《Android | ContentProvider 精通篇》中,我會詳細(xì)介紹 ContentResolver#query(...) 方法的執(zhí)行過程,在那里我們再討論 ApplicationContentResolver 方法體中的具體行為。
4.2 ContentUris
ContentUris 是 Uri 的工具類,在 ContentUris 的文檔注釋中主要描述了 ContentProvider URI 所遵循的格式,此外 ContentUris 還提供了三個工具方法:
1、從 Uri 中解析主鍵 id
public static long parseId(Uri contentUri) {
String last = contentUri.getLastPathSegment();
return last == null ? -1 : Long.parseLong(last);
}
2、向 Uri 追加一個 id
public static Uri.Builder appendId(Uri.Builder builder, long id) {
return builder.appendEncodedPath(String.valueOf(id));
}
3、向 Uri 追加一個 id
public static Uri withAppendedId(Uri contentUri, long id) {
return appendId(contentUri.buildUpon(), id).build();
}
4.3 UriMatcher
UriMatcher 是用于自定義 ContentProvider 的工具類,主要作用是根據(jù) Uri 匹配對應(yīng)的數(shù)據(jù)表。
public class ExampleProvider extends ContentProvider {
1、初始化 UriMatcher 對象,NO_MATCH 表示不匹配任何 Uri
private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
2、注冊 Uri 已經(jīng)對應(yīng)的返回碼
static {
uriMatcher.addURI("com.example.app.provider", "table3", 1);
uriMatcher.addURI("com.example.app.provider", "table3/#", 2);
}
...
3、 查詢
public Cursor query(
Uri uri,
String[] projection,
String selection,
String[] selectionArgs,
String sortOrder) {
switch (uriMatcher.match(uri)) {
case 1:
3.1 匹配 table3
if (TextUtils.isEmpty(sortOrder)) sortOrder = "_ID ASC";
break;
case 2:
3.2 匹配 table3/#
selection = selection + "_ID = " + uri.getLastPathSegment();
break;
default:
3.3 默認(rèn)
...
}
3.4 真正執(zhí)行查詢
}
}
可以使用通配符:
-
*:匹配任意長度字符串 -
#:匹配任意長度的數(shù)字字符串
4.4 ContentObserver
ContentObserver .java
子類重寫實現(xiàn)監(jiān)聽邏輯
public void onChange(boolean selfChange) {
// Do nothing. Subclass should override.
}
public void onChange(boolean selfChange, Uri uri) {
onChange(selfChange);
}
ContentObserver 用于監(jiān)聽 ContentProvider 中指定 Uri 標(biāo)識數(shù)據(jù)的變化(增 / 刪 / 改),使用時需要用到 ContentResolver 的兩個方法:
ContentResolver.java
注冊監(jiān)聽
public final void registerContentObserver(Uri uri, boolean notifyForDescendants, ContentObserver observer)
注銷監(jiān)聽
public final void unregisterContentObserver(ContentObserver observer)
需要注意:ContentProvider 內(nèi)部需要手動通知修改事件,才能有效回調(diào)給 ContentResolver,例如:
ContentProvider 實現(xiàn)類
public class UserContentProvider extends ContentProvider {
public Uri insert(Uri uri, ContentValues values) {
...
通知
getContext().getContentResolver().notifyChange(uri, null);
}
}
5. 總結(jié)
ContentProvider 是進(jìn)程間內(nèi)容共享的統(tǒng)一接口,底層實現(xiàn)進(jìn)程間通信的是 Binder 機(jī)制,使用 ContentProvider 的優(yōu)點是透明地提供內(nèi)容,外界不用關(guān)心內(nèi)容的層的數(shù)據(jù)實現(xiàn)方式。
Uri 的作用是唯一標(biāo)識 ContentProvider 的數(shù)據(jù),MIME 類型描述了擴(kuò)展名與應(yīng)用程度的對應(yīng)關(guān)系,例如 .html 對應(yīng)的 MIME 類型為 text/html;
ContentProvider 提供了 CURD 四個核心方法類訪問數(shù)據(jù),執(zhí)行在服務(wù)提供進(jìn)程的 Binder 線程池,而 onCreate() 方法執(zhí)行在服務(wù)提供進(jìn)程主線程