课程 3: Content Providers 简介

这节课是 Android 开发(入门)课程 的第四部分《数据与数据库》的第三节课,导师依然是 Jessica Lin 和 Katherine Kuan。这节课抛弃上节课直接在 Activity 中操作数据库的做法 ,引入 Content Providers 作为数据库和 Activity (UI) 之间的抽象层,使 Pets App 更符合 Android 框架的设计规范,保持以一致的方式管理对结构化数据集的访问 (Keep a consistent way to manage access to a structured set of data)。

关键词:Content Providers、Content Resolver、Content URI & URI Matcher、Content Authority、Data Validation、MIME Type

Content Providers 的优势

Content Providers 主要为应用带来三个方面的好处:

课程 3: Content Providers 简介_第1张图片
UI 把 Content Providers 看作一个暗箱操作

首先,Content Providers 在 Activity (UI) 与数据库之间添加一个抽象层,将数据库内部抽象化,隐藏数据存储的详情;也就是说,对于 UI 而言,UI 代码直接与 Content Providers 交互,不关心数据是存储在数据库中,还是文本文件中。这样一来,Content Providers 即使改变数据的存储方式,UI 代码也可以保持不变。另外,Content Providers 通常也承担着数据验证 (Data Validation) 的重要角色,确保输入数据库的数据是有效的。

课程 3: Content Providers 简介_第2张图片
Cursor Loader 需要借助 Content Providers 实现异步查询

其次,Content Providers 能够与其它框架类配合使用,例如 Cursor Loader 借助 Content Providers 实现数据变化时自动更新,使列表内容保持最新状态,这是下节课的主要内容;还有,桌面小部件 (App Widgets) 以及实现数据上传云端和提供搜索建议的 Sync Adapters 也需要利用 Content Providers。上面这些例子也证明了 Android 框架倾向于应用通过 Content Providers 来规范对结构化数据集的访问。

课程 3: Content Providers 简介_第3张图片
通过 Content Providers 向获得访问权限的应用分享数据

最后,在使用 Content Providers 之前,应用的数据是封闭的 (siloed),通过 Content Providers 可以将应用数据分享给获得访问权限的应用。这是一种安全的管理应用数据访问的方式,其它应用根据应用规定的 Contract(主要是 Content URI 与 CRUD 操作对应的方法)与 Content Providers 交互获取数据;不过其实应用访问自身的数据的流程也类似,这就带出了 Content Providers 工作流的话题。

Content Providers 的工作流
课程 3: Content Providers 简介_第4张图片
应用通过 Content Providers 访问自身数据

首先,在 Activity (UI) 中调用 Content Resolver 的 CRUD 操作对应的方法,并传递一个 URI。然后 ContentResolver 对象能够把 Content Providers 作为客户端进行交互,也就是说 Content Resolver 能够根据 URI 选择哪一个 Content Providers 客户端传递 CRUD 操作指令及其数据,最终由 Content Providers 实现数据访问。因此,UI 实际上是直接与 Content Resolver 交互的,选中的 Content Providers 完成数据访问后返回数据给 Content Resolver 最终再传递给 UI。例如在 Pets App 中,CatalogActivity 通过调用 ContentResolver 的 query method 获取一个 Cursor 对象。

In CatalogActivity.java

Cursor cursor = getContentResolver().query(
        PetEntry.CONTENT_URI,   // The content URI of the words table
        projection,             // The columns to return for each row
        null,                   // Selection criteria
        null,                   // Selection criteria
        null);                  // The sort order for the returned rows

Tips:
Content Resolver 与 Content Provider 交互的一个重要特性是,UI 调用 ContentResolver 对象的 CRUD 操作对应的方法时,如 query method,Content Resolver 会调用选中的 Content Providers 中相同名称的方法 (identically-named methods),即相同的 query method,以使 Content Providers 完成数据访问。因此,在实现 Content Providers 这个抽象类的时候,必须 override 四个 CRUD 操作对应的方法,名称分别为 queryupdateinsertdelete

Content URI

在 Content Providers 的整个端到端的工作流中,一个关键的参数是 UI 发出的 URI (Uniform Resource Identifier),指统一资源标识符,之前在《课程 4: 偏好》中提到。对于 Content Providers 而言,URI 指定了需要进行 CRUD 操作的数据,它可以是一行、多行、整个数据库,或者一个文本文件、图片文件等媒体文件。因此,在这里 URI 被称为 Content URI,格式如下:

:////
  1. Scheme: 固定为 content:// 表示该 URI 为 Content URI。

  2. Content Authority: 内容主机名,它是 Content URI 最重要的部分,它指定了所需的 Content Providers 客户端。Content Authority 是由 AndroidManifest 中 provider 的 android:authorities 属性决定的,通常设置为应用独一无二的包名,例如 Pets App 的 Content Authority 就设置为 com.example.android.pets。如果是应用访问自身数据的 Content URI,那么其 Content Authority 就要和 AndroidManifest 中的保持一致。

Note:
Android Developers 文档推荐 Content Authority 在包名之外添加 provider 的字样,以表示与包名有所区别。例如包名为 com.example. 时,Content Authority 可以是 com.example..provider,不过课程中没有这么做。

  1. Type of Data: 数据类型,它指定了需要进行操作的数据。常见的模式是,数据类型为表名,表示需要访问该表格的数据;如果在此之后 Content URI 没有 ID 后缀,那么就表示访问整个表格的数据,这通常在 Create/Insert 新数据或者 Delete 整个表格的应用场景使用。例如在 Pets App 中表示需要访问整个 pets 表格的 Content URI 为:

     content://com.example.android.pets/pets
    

Note:
当所需的数据不是数据库,而是文件时,这个部分就是文件的目录路径,它可能是多层结构。

  1. ID: 可选,指定表格中某一行数据进行操作,通常用于 Update 或 Delete 某一行数据的应用场景,例如在 Pets App 中表示需要访问 pets 表格第 5 行的 Content URI 为:(假设表格行数从 1 开始)

     content://com.example.android.pets/pets/5
    

综上所述,将 Content URI 写进 Pets App,首先在 AndroidManifest 中设置 Content Provider 的内容主机名,其中第一行的属性指定了 Content Provider 的名称及其路径,.data.PetProvider 表示 PetProvider 类放在 data 目录下;第三行的属性设置应用数据是否通过 Content Provider 对外开放,设为 false 表示应用不对外分享数据。

In AndroidManifest.xml


然后在 PetContract.java 中设置 Content URI 的各个常量,其中使用了 Uri 类的 parse method 将字符串转换为一个 Uri 对象,以及 withAppendedPath method 来构建新的 Uri 对象。

In PetContract.java

public static final String CONTENT_AUTHORITY = "com.example.android.pets";
public static final Uri BASE_CONTENT_URI = Uri.parse("content://" + CONTENT_AUTHORITY);
public static final String PATH_PETS = "pets";

public static final Uri CONTENT_URI = Uri.withAppendedPath(BASE_CONTENT_URI, PATH_PETS);
URI Matcher
课程 3: Content Providers 简介_第5张图片

设计好 UI 端的 Content URI 之后,Content Providers 将通过 URI Matcher 接收并解析 URI,以确定如何处理访问数据的请求。例如在 Pets App 中,存在两种类型的 Content URI,一种表示对整个 pets 表格进行操作,另一种表示对 pets 表格的某一行进行操作,URI Matcher 需要判断 UI 发出的 Content URI 是哪一种类型,针对不同类型的 Content URI,Content Providers 需要进行不同的处理方法。

因此,URI Matcher 应该根据所有可能的 Content URI 类型构建出一个匹配模型。首先,为所有可能的 Content URI 定义唯一的代码,例如在 Pets App 中,为两种类型的 Content URI 分别定义两个代码。代码数值可任意指定,但是要保证每个代码的唯一性。

URI pattern Code Constant Name
content://com.example.android.pets/pets 100 PETS
content://com.example.android.pets./pets/# 101 PET_ID

其中,对于第二种类型的 Content URI,使用了通用匹配符 # 表示任意长度数字的字符串;另一个常用的通配符是 * 表示任意长度的字符串,例如 Contacts App 中的一个 Content URI 用于通过姓名查找联系人,因此 URI 会以未知长度的字符串结束。

content://com.android.contacts/lookup/*

在为所有可能的 Content URI 定义唯一的代码之后,新建一个 UriMatcher 对象,并通过 addURI method 添加匹配规则,完整代码如下。

In PetProvider.java

private static final int PETS = 100;
private static final int PET_ID = 101;

private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);

static {
    sUriMatcher.addURI(PetContract.CONTENT_AUTHORITY, PetContract.PATH_PETS, PETS);
    sUriMatcher.addURI(PetContract.CONTENT_AUTHORITY, PetContract.PATH_PETS + "/#", PET_ID);
}
  1. UriMatcher 对象的名称以一个小写的 s 开头表示静态字段,这是由 Android 的 Java 代码规范 建议的。类似的字段命名规范还有,非公开且非静态的字段名称以 m 开头;其他字段以小写字母开头;公开静态 final 字段(常量)为全部大写并用下划线连接。
  2. 新建 UriMatcher 对象时,其构造函数需要传入一个初始匹配代码,通常使用 UriMatcher 类自带的 NO_MATCH 常量。
  3. 分别通过 addURI method 传入每个类型的 Content URI 及其对应的唯一代码,添加匹配规则。
  4. addURI method 放入 static 代码块中,确保当这个类内的任何方法被调用时,static 代码块内的代码会首先运行。

综上所述,URI Matcher 的作用是,根据 Content URI 类型代码匹配 UI 传来的 Content URI,确保 Content Providers 仅处理正确的 Content URI 传递的数据访问请求。

1. Query

例如在 Pets App 中,PetProvider 的 query method 通过 URI Matcher 的匹配结果对数据库进行不同的操作。

In PetProvider.java

@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
    SQLiteDatabase database = mDbHelper.getReadableDatabase();

    Cursor cursor;

    int match = sUriMatcher.match(uri);
    switch (match) {
        case PETS:
            cursor = database.query(PetEntry.TABLE_NAME, projection, selection, selectionArgs, null, null, sortOrder);
            break;
        case PET_ID:
            selection = PetEntry._ID + "=?";
            selectionArgs = new String[] { String.valueOf(ContentUris.parseId(uri)) };

            cursor = database.query(PetEntry.TABLE_NAME, projection, selection, selectionArgs, null, null, sortOrder);
            break;
        default:
            throw new IllegalArgumentException("Cannot query unknown URI " + uri);
    }
    return cursor;
}
  1. mDbHelper 对象在 onCreate method 中新建,并且定义为全局变量,所以在这里首先通过 mDbHelper 获取一个 SQLiteDatabase 对象。
  2. 通过调用 UriMatcher 的 match method 对传入的 Content URI 进行匹配,获得匹配代码后通过 switch/case 语句对数据库进行不同的操作。
    (1)对于查询整个表格的 Content URI,直接将输入参数,包括 selection、selectionArgs 等参数,传入 SQLiteDatabase 对象的 query method,返回一个 Cursor 对象。
    (2)对于查询表格中某一行的 Content URI,需要手动设置 selection 和 selectionArgs 参数,其中需要调用 ContentUris 的 parseId method 解析出 Content URI 的 ID,并调用 String.valueOf method 将 int 转换为 String。最后同样是将参数传入 SQLiteDatabase 对象的 query method,返回一个 Cursor 对象。
    (3)当 Content URI 未匹配以上两种类型的任何一种时,抛出一个异常,告知开发者异常信息。

2. Insert

类似地,在 Pets App 中,PetProvider 的 insert method 通过 URI Matcher 的匹配,仅对请求整个表格的 Content URI 有效。

In PetProvider.java

@Override
public Uri insert(Uri uri, ContentValues contentValues) {
    final int match = sUriMatcher.match(uri);
    switch (match) {
        case PETS:
            return insertPet(uri, contentValues);
        default:
            throw new IllegalArgumentException("Insertion is not supported for " + uri);
    }
}

当传入的 Content URI 匹配代码为 PETS 代表的 100 时,调用 insertPet method 对数据库进行插入新行操作,输入参数为 Content URI 和 ContentValues 对象。否则,抛出一个异常。

In PetProvider.java

private Uri insertPet(Uri uri, ContentValues values) {
    String name = values.getAsString(PetEntry.COLUMN_PET_NAME);
    if (name == null) {
        throw new IllegalArgumentException("Pet requires a name");
    }

    Integer gender = values.getAsInteger(PetEntry.COLUMN_PET_GENDER);
    if (gender == null || !PetEntry.isValidGender(gender)) {
        throw new IllegalArgumentException("Pet requires valid gender");
    }

    Integer weight = values.getAsInteger(PetEntry.COLUMN_PET_WEIGHT);
    if (weight != null && weight < 0) {
        throw new IllegalArgumentException("Pet requires valid weight");
    }

    SQLiteDatabase database = mDbHelper.getWritableDatabase();

    long id = database.insert(PetEntry.TABLE_NAME, null, values);
    if (id == -1) {
        Log.e(LOG_TAG, "Failed to insert row for " + uri);
        return null;
    }

    return ContentUris.withAppendedId(uri, id);
}
  1. 由于这里从 UI 引入了新数据,所以需要先进行数据验证 (Data Validation)。具体的做法是,调用 ContentValues 对象各个对应的 getter method,获取必要的键的值,对值进行验证。如果值不可接受,就抛出异常,告知开发者异常信息。例如,通过 getAsString 获取传入的 ContentValues 对象键为 COLUMN_PET_NAME 的值并存为字符串,如果该字符串为 null,那么就抛出一个异常。
  2. Content Providers 的 insert method 的返回值为带新插入行 ID 的一个 Content Uri 对象。因此,这里需要通过调用 ContentUris 的 withAppendedId method 构建一个带 ID 后缀的 Uri 对象,其中传入的 ID 参数是 SQLiteDatabase 的 insert method 的返回值。

3. Update

类似地,在 Pets App 中,PetProvider 的 update method 通过 URI Matcher 的匹配,需要对两种 Content URI 都有效。

In PetProvider.java

@Override
public int update(Uri uri, ContentValues contentValues, String selection, String[] selectionArgs) {
    final int match = sUriMatcher.match(uri);
    switch (match) {
        case PETS:
            return updatePet(uri, contentValues, selection, selectionArgs);
        case PET_ID:
            selection = PetEntry._ID + "=?";
            selectionArgs = new String[] { String.valueOf(ContentUris.parseId(uri)) };
            return updatePet(uri, contentValues, selection, selectionArgs);
        default:
            throw new IllegalArgumentException("Update is not supported for " + uri);
    }
}
  1. 对于查询整个表格的 Content URI,直接将输入参数,包括 selection、selectionArgs 参数,传入辅助方法 updatePet 进行处理。
  2. 对于查询表格中某一行的 Content URI,需要手动设置 selection 和 selectionArgs 参数,方法与上述 query method 相同。最后同样是将参数传入辅助方法 updatePet 进行处理。
  3. 当 Content URI 未匹配以上两种类型的任何一种时,抛出一个异常,告知开发者异常信息。

In PetProvider.java

private int updatePet(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
    if (values.containsKey(PetEntry.COLUMN_PET_NAME)) {
        String name = values.getAsString(PetEntry.COLUMN_PET_NAME);
        if (name == null) {
            throw new IllegalArgumentException("Pet requires a name");
        }
    }

    if (values.containsKey(PetEntry.COLUMN_PET_GENDER)) {
        Integer gender = values.getAsInteger(PetEntry.COLUMN_PET_GENDER);
        if (gender == null || !PetEntry.isValidGender(gender)) {
            throw new IllegalArgumentException("Pet requires valid gender");
        }
    }

    if (values.containsKey(PetEntry.COLUMN_PET_WEIGHT)) {
        Integer weight = values.getAsInteger(PetEntry.COLUMN_PET_WEIGHT);
        if (weight != null && weight < 0) {
            throw new IllegalArgumentException("Pet requires valid weight");
        }
    }

    if (values.size() == 0) {
        return 0;
    }

    SQLiteDatabase database = mDbHelper.getWritableDatabase();

    return database.update(PetEntry.TABLE_NAME, values, selection, selectionArgs);
}
  1. insert method 类似,这里的重点也是在数据验证上。但不同的是,更新数据时 ContentValues 对象中某些键/值对可能不存在,因此首先需要通过 containKey method 判断,仅对存在的键/值对进行数据验证。
  2. Content Providers 的 insert method 的返回值为更新行的数量值。因此,如果数据验证全部通过,直接将 SQLiteDatabase 的 update method 的返回值输出;如果 ContentValues 对象为空,返回值 0,表示没有行被更新。

4. Delete

类似地,在 Pets App 中,PetProvider 的 delete method 通过 URI Matcher 的匹配,需要对两种 Content URI 都有效。

@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
    SQLiteDatabase database = mDbHelper.getWritableDatabase();

    final int match = sUriMatcher.match(uri);
    switch (match) {
        case PETS:
            return database.delete(PetEntry.TABLE_NAME, selection, selectionArgs);
        case PET_ID:
            selection = PetEntry._ID + "=?";
            selectionArgs = new String[] { String.valueOf(ContentUris.parseId(uri)) };
            return database.delete(PetEntry.TABLE_NAME, selection, selectionArgs);
        default:
            throw new IllegalArgumentException("Deletion is not supported for " + uri);
    }
}
  1. 对于查询整个表格的 Content URI,直接将输入参数,包括 selection、selectionArgs 参数,传入 SQLiteDatabase 对象的 delete method,返回值为受影响的行数。
  2. 对于查询表格中某一行的 Content URI,需要手动设置 selection 和 selectionArgs 参数,方法与上述 query method 相同。最后同样是将参数传入 SQLiteDatabase 对象的 delete method,返回值为受影响的行数。
  3. 当 Content URI 未匹配以上两种类型的任何一种时,抛出一个异常,告知开发者异常信息。
MIME Type

MIME (Multipurpose Internet Mail Extensions) 类型,也称为内容类型 (Content Type)、媒体类型 (Media Type)。它是网络传输内容的一种标识符,由文件格式及其内容决定。也就是说,一个 MIME 类型至少包括两个部分:一个类型 (Type) 和一个子类型 (Subtype)。此外,它还可能包括一个或多个可选参数 (Optional Parameter)。例如,一个 HTML 文件的互联网媒体类型可能是

text/html; charset = UTF-8

在这个例子中,文件类型为 text,子类型为 html,而 charset 是一个可选参数,其值为 UTF-8

对于 Android 应用而言,MIME 类型遵循特定的格式 (Vendor tree),即以 "vnd.android.cursor…" 开头,后接 Content Authority 以及数据路径。其中,开头的基础类型 (Base Type) 根据目录 (directory, abbr. dir) 与子项 (item) 分为两种:

  • 目录 MIME 基础类型:vnd.android.cursor.dir
  • 子项 MIME 基础类型:vnd.android.cursor.item

上面两种基础类型已经在 ContentResolver 类中分别定义为常量,可直接调用。因此在 Pets App 中,为数据库 pets 自定义两个 MIME 类型,代码如下:

In PetContract.java

public static final String CONTENT_LIST_TYPE =
                ContentResolver.CURSOR_DIR_BASE_TYPE + "/" + CONTENT_AUTHORITY + "/" + PATH_PETS;

public static final String CONTENT_ITEM_TYPE =
                ContentResolver.CURSOR_ITEM_BASE_TYPE + "/" + CONTENT_AUTHORITY + "/" + PATH_PETS;

MIME 类型与文件拓展名相对应,因此计算机系统通常通过拓展名来确定一个文件的媒体类型并决定与其相关联的软件。在 Android 中,MIME 类型通常在发送 Intent 请求时与 URI 配合,帮助系统确定设备上最适合处理请求的应用组件。例如,一个能够显示图片的 Activity 不一定能够播放音频,但是两者的 URI 类似,通过 MIME 类型就可以明确其中一种。

针对 Content URI 的情况,系统就会检查相应的 Content Providers,通过其 getType() method 获取 MIME 类型。这是因为,所有 Content Providers 都会通过 MIME 类型来定义它所处理的数据类型,这是一种标准化的做法。这种做法既保证了代码之间的标准化交互,也使不同数据类型之间不易混淆。例如一个 RailwayProvider 可能处理 trains、stations、tickets 等不同类型的数据,而通过独一无二的 MIME 类型能将他们明确分别。

在 Pets App 中,Content Providers 在 getType() method 返回对应的 MIME 类型。

In PetProvider.java

@Override
public String getType(Uri uri) {
    final int match = sUriMatcher.match(uri);
    switch (match) {
        case PETS:
            return PetEntry.CONTENT_LIST_TYPE;
        case PET_ID:
            return PetEntry.CONTENT_ITEM_TYPE;
        default:
            throw new IllegalStateException("Unknown URI " + uri + " with match " + match);
    }
}

你可能感兴趣的:(课程 3: Content Providers 简介)