Android ContentProvider_1 使用方法 目錄 前置知識 1. 概述 2. 相關概念 3. 主要方法 4. ContentProvider 核心類 5. 總結


目錄


前置知識

這篇文章的內容會涉及以下前置 / 相關知識,貼心的我都幫你準備好了,請享用~

  • 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)]

例如:content://com.xx.demo/user/1

元素 描述
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() 方法執行在服務提供進程主線程

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章