目錄
前置知識
這篇文章的內容會涉及以下前置 / 相關知識,貼心的我都幫你準備好了,請享用~
- Binder 機制
1. 概述
1.1 作用
ContentProvider 是進程間內容共享的統一接口。注意:ContentProvider 的作用不是實現進程間通信,它只是爲進程間通信提供了一套統一接口,真正實現進程間通信的是底層的 Binder 機制。
1.2 優點:透明地提供內容
使用 ContentProvider 允許應用透明地將數據開放給其它應用,無論底層數據採用何種實現方式(網絡、內存、文件或數據庫),外界對於數據的訪問方式都是統一的 & 固定的。外界只關心採用 CURD 來訪問 ContentProvider 的數據,至於其內部數據的實現是採用文件存儲還是數據庫存儲,外界是不感知的。
1.3 ContentProvider 是單例嗎?
通常來說,ContentProvider 是單例的,特殊情況可以設置android:multiprocess
屬性來決定是不是單例:當屬性值爲 true 時,每個調用者進程都會存在一個 ContentProvider 實例,官方的解釋是可以避免進程間通訊的開銷,但是這種方式在實際開發中很少運用。因此我們說一般情況下 ContentProvider 是單例的,只在服務提供進程創建實例。
2. 相關概念
2.1 統一資源標識符(URI)
統一資源標識符(Uniform Resource Indentifier)的作用是 唯一標識 ContentProvider 的數據。在通過 ContentResolver 解析數據時,URI 是必要的參數,其遵循的格式體現在ContentUris.java
:
Content URIs have the syntax:content://authority/path/id
可以看到,URI 遵循固定的格式,一共分爲四個部分:[圖片上傳失敗...(image-df2b78-1666750926778)]
元素 | 描述 |
---|---|
schema(方案) | 固定爲 content:// |
authority(權威) | 標識 ContentProvider 的唯一字符串,對應於註冊時指定的 android:authority 屬性 |
path(路徑) | 標識 authority 數據的某些子集 |
id(記錄 id) | 標識 path 子集中的某個記錄(不指定是標識全部記錄) |
系統預置了一些 ContentProvider,例如通訊錄、媒體資源等,這裏舉出一些常用的系統 ContentProvider 的 Authority,它們的接口約定定義在目錄/android.provider
:
Authority | 描述 |
---|---|
com.android.contacts | 通訊錄 |
media | 媒體 |
com.android.calendar | 日曆 |
user_dictionary | 用戶詞典 |
2.2 MIME 數據類型
MIME類型(Multipurpose Internal Mail Extensions,多用途互聯網郵件擴展類型)是一種互聯網標準,用於指定某種擴展名的文件與應用程序的對應關係。一個 MIME 類型分爲「主類型」+「子類型」,例如 .html 文件對應的 MIME 類型爲 text/html,其中 text 爲主類型,html 爲子類型。
在 ContentProvider 中,通過 getType(Uri) 方法來確定 URI 對應的 MIME 類型,返回值可以返回 標準 MIME 類型或者自定義 MIME 類型,這是一個抽象方法,需要由子類實現:
ContentProvider.java
public abstract String getType(Uri uri);
2.2.1 標準 MIME 類型
標準 MIME 類型中常見的主類型有:
- 聲音:audio
- 視頻:video
- 圖像:image
- 文本:text
對應的 MIMIE 類型舉例:
擴展名 | 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 使用表格的形式管理數據,對外暴露四個操作方法,分別是:添加、刪除、更新、查詢(insert、delete、update、query):
添加數據(Binder 線程)
public abstract Uri insert(Uri uri, ContentValues values);
刪除數據(Binder 線程)
public abstract int delete(Uri uri, String selection, String[] selectionArgs);
更新數據(Binder 線程)
public abstract int update(Uri uri, ContentValues values, String selection, String[] selectionArgs);
查詢數據(Binder 線程)
public abstract Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder);
除了 4 個核心方法外,ContentProvider 還有其他比較重要的方法,例如:
啓動回調(主線程)
public abstract boolean onCreate();
返回 Uri 對應的 MIME 類型(調用線程)
public abstract String getType(Uri uri);
需要注意:四個核心方法執行在 ContentProvider 註冊進程,並在 Binder 線程池中執行,而不是主線程。考慮到存在多線程併發訪問,爲了保證數據安全在實現 ContentProvider 是還需要保證線程同步。而 onCreate() 方法執行在 ContentProvider 註冊進程的主線程,因此不能執行耗時操作。關於 onCreate() 方法的調用我在 第 4 節 ContentProvider 的啓動過程 中會詳細介紹。
主要方法 | 執行線程 |
---|---|
insert() | Binder 線程 |
delete() | Binder 線程 |
update() | Binder 線程 |
query() | Binder 線程 |
onCreate() | 主線程 |
3.1 插入數據
要插入一行新數據,需要使用 ContentProvider#insert(...)。例如,下面程序將一條日程數據插入的系統日曆中:
ContentValues eventValues = new ContentValues();
eventValues.put(CalendarContract.Events.CALENDAR_ID, catId); // 日曆賬號 ID
eventValues.put(CalendarContract.Events.EVENT_TIMEZONE, TimeZone.getDefault().getID()); // 時區
eventValues.put(CalendarContract.Events.DTSTART, beginTimeMillis); // 開始時間
eventValues.put(CalendarContract.Events.DTEND, endTimeMillis); // 結束時間
eventValues.put(CalendarContract.Events.TITLE, title); // 標題
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 正好就是插入數據時使用的 URI。需要注意的是,你不需要指定數據的 _ID
列,該列是表的主鍵,ContentProvider 會自動維護該列並分配一個唯一值。而要從 Uri 中提取 _ID 列的值,可以調用 ContentUris.parseId(...):
ContentUris.java
public static long parseId(Uri contentUri) {
String last = contentUri.getLastPathSegment();
return last == null ? -1 : Long.parseLong(last);
}
提示: 客戶端程序並非直接調用 ContentProvider#insert(),而是通過 ContentResolver#insert() 間接調用,下文會提到。
3.2 查詢數據
從 ContentProvider 中查詢數據的流程主要分爲三個步驟:
3.2.1 請求訪問權限
ContentProvider 程序可以指定其他應用程序必須具備的權限,例如讀取用戶詞典需要android.permission.READ_USER_DICTIONARY
,寫入用戶詞典需要android.permission.WRITE_USER_DICTIONARY
。
爲了獲取 ContentProvider 程序所需的權限,你的應用需要在 Manifest 文件中使用 <uses-permission> 來請求它們。當 Android Package Manager 安裝 APK 時,會提示用戶應用所需要的權限,用戶繼續安裝相當於隱式授予權限。當然了,在 Android 6.0 以後部分權限還需要動態申請。
<uses-permission android:name = “ android.permission.READ_USER_DICTIONARY” >
3.2.2 構造查詢條件
ContentProvider 查詢和 SQL 查詢是相似的,如下表對比:
ContentProvider 查詢 | SQL 查詢 | 作用 |
---|---|---|
Uri | FROM table_name | 查詢的數據集合 |
projection | col,col,col... | 查詢結果所需的列 |
selectionClause | WHERE col = value | 選擇條件 |
selectionArgs | (沒有確切地等效項) | 選擇條件參數(如果 selection )中使用了 ? 佔位符 |
sortOrder | ORDER BY col,col,... | 結果集 Cursor 的排序規則 |
cursor = context .getContentResolver().query(
UserDictionary.Words.CONTENT_URI,
projection,
selectionClause,
selectionArgs,
sortOrder);
例如查詢手機通訊錄:
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 處理結果集
查詢結果是一個 Cursor 對象,處理範例如下:
if (null == mCursor) {
// 失敗
} else if (mCursor.getCount() < 1) {
// 查詢結果爲空
} else {
// 查詢結果非空
while (cursor.moveToNext()) {
// 聯繫人名稱
String contractName = cursor.getString(cursor.getColumnIndexOrThrow(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
// 聯繫人電話
String phone = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
...
}
cursor.close(); // 記得關閉結果集
}
3.3 刪除數據
刪除數據與查詢類似,需要構造查詢條件,刪除操作結束會返回成功刪除的行數。
int rowsDeleted = context.getContentResolver().delete(...);
3.4 更新數據
更新操作類似於查詢操作和插入操作的結合體,既需要構造 ContentValues 對象,也需要構造查詢條件,刪除操作結束後返回成功修改的行數。
int rowsUpdated = context.getContentResolver().update(
UserDictionary.Words.CONTENT_URI,
updateValues,
selectionClause,
selectionArgs
);
4. ContentProvider 核心類
4.1 ContentResolver
外界(包括當前進程的其他組件)無法直接訪問 ContentProvider 的,而是需要通過 ContentResolver 來間接訪問。這種設計的優點是 統一管理應用依賴的 ContentProvider,而不需要關心真正操作的 ContentProvider 實現類。
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 精通篇》中,我會詳細介紹 ContentResolver#query(...) 方法的執行過程,在那裏我們再討論 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 的工具類,主要作用是根據 Uri 匹配對應的數據表。
public class ExampleProvider extends ContentProvider {
1、初始化 UriMatcher 對象,NO_MATCH 表示不匹配任何 Uri
private static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
2、註冊 Uri 已經對應的返回碼
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 默認
...
}
3.4 真正執行查詢
}
}
可以使用通配符:
-
*
:匹配任意長度字符串 -
#
:匹配任意長度的數字字符串
4.4 ContentObserver
ContentObserver .java
子類重寫實現監聽邏輯
public void onChange(boolean selfChange) {
// Do nothing. Subclass should override.
}
public void onChange(boolean selfChange, Uri uri) {
onChange(selfChange);
}
ContentObserver 用於監聽 ContentProvider 中指定 Uri 標識數據的變化(增 / 刪 / 改),使用時需要用到 ContentResolver 的兩個方法:
ContentResolver.java
註冊監聽
public final void registerContentObserver(Uri uri, boolean notifyForDescendants, ContentObserver observer)
註銷監聽
public final void unregisterContentObserver(ContentObserver observer)
需要注意:ContentProvider 內部需要手動通知修改事件,纔能有效回調給 ContentResolver,例如:
ContentProvider 實現類
public class UserContentProvider extends ContentProvider {
public Uri insert(Uri uri, ContentValues values) {
...
通知
getContext().getContentResolver().notifyChange(uri, null);
}
}
5. 總結
ContentProvider 是進程間內容共享的統一接口,底層實現進程間通信的是 Binder 機制,使用 ContentProvider 的優點是透明地提供內容,外界不用關心內容的層的數據實現方式。
Uri 的作用是唯一標識 ContentProvider 的數據,MIME 類型描述了擴展名與應用程度的對應關係,例如 .html 對應的 MIME 類型爲 text/html;
ContentProvider 提供了 CURD 四個核心方法類訪問數據,執行在服務提供進程的 Binder 線程池,而 onCreate() 方法執行在服務提供進程主線程