ContentProvider使用详解

一、ContentProvider

ContentProvider:内容提供程序,提供安全的数据访问机制,支持跨进程通信(IPC),主要用于向其他应用提供数据。通过Context中ContentResolver对象作为客户端与contentprovider进行通讯,一般是对其数据的CRUD操作。Android本身提供了音频、视频、图像和个人联系信息的contentprovider。

contentprovider以一个或多个表的形式将数据呈现给外部应用。contentprovider不需要具有主键,也无需将_ID用作其主键的列名(如果存在主键),但是如果要将来自contentprovider的数据与ListView绑定,则其中一个列名必须是_ID.

访问:

应用从具有 ContentResolver 客户端对象的内容提供程序访问数据。 此对象具有调用提供程序对象(ContentProvider 的某个具体子类的实例)中同名方法的方法。 ContentResolver 方法可提供持续存储的基本“CRUD”(创建、检索、更新和删除)功能。
客户端应用进程中的 ContentResolver 对象和拥有提供程序的应用中的 ContentProvider 对象可自动处理跨进程通信。 ContentProvider 还可充当其数据存储区和表格形式的数据外部显示之间的抽象层。

// 查找并返回结果集,用户字典为例
mCursor = getContentResolver().query(
    UserDictionary.Words.CONTENT_URI,   // URI映射至提供程序中的表 from table_name
    mProjection,                        // 要查找的列 col,col...
    mSelectionClause                    // 选择行的条件 where col = value
    mSelectionArgs,                     // 参数替换选择条件中?占位符
    mSortOrder);                        // 显示顺序order by col,col...

其中内容URI包括整个提供程序的符号名称(授权)和一个指向表的名称(路径),如上述代码中URI是

content://user_dictionary/words

其中user_dictionary字符串时提供程序的授权,words字符串是表的路径。content://始终显示,并将此标识为内容URI。还有允许将ID值追加到URI末尾来访问表中某行的。

如从上述表中检索_ID为4的行,URI为:

Uri singleUri = ContentUris.withAppendedId(UserDictionary,Words.CONTENT_URI, 4);

在更新或删除某一行时常会用到id。

Uri和Uri.Builder类提供构建字符串格式规范的URI的方法。ContentUris包含可以将ID值追加到URI后的方法。

二、检索数据示例

注意:官方建议要在子线程中进行异步执行查询(ContentResolver.query())。通常使用CursorLoader类执行此操作。

从contentprovider检索数据,依照以下步骤:

1.请求provider的读取访问权限;

2.定义将查询发送至提供程序的代码。

请求权限:

这个权限无法在运行时获取,应使用元素和provider定义的准确权限名称在Manifest中指明。指定之后,用户安装时就会默认隐式的授予这个权限。

一般读取权限的准确名称可以通过其文档获取。例如上述的用户字典provider在其清单中定义了权限android.permission.READ_USER_DICTIONARY用于读取用户字典。当然还有增删改的权限:android.permission.WRITE_USER_DICTIONARY

构建要查找的列以及条件:

// 要查找的列投影
String[] mProjection =
{
    UserDictionary.Words._ID,    // Contract class constant for the _ID column name
    UserDictionary.Words.WORD,   // Contract class constant for the word column name
    UserDictionary.Words.LOCALE  // Contract class constant for the locale column name
};
// where条件
String mSelectionClause = null;
// 占位符对应参数
String[] mSelectionArgs = {""};
// 从UI获取文字
mSearchString = mSearchWord.getText().toString();
// 此处要进行输入检查,防止非法输入
// 如果是空串,则默认查找所有
if (TextUtils.isEmpty(mSearchString)) {
    // 设置选择子句为空,返回所有结果
    mSelectionClause = null;
    mSelectionArgs[0] = "";
} else {
    // 构建选择子句
    mSelectionClause = UserDictionary.Words.WORD + " = ?";
    // 将用户输入的字符串放入对应
    mSelectionArgs[0] = mSearchString;
}
// 查表并返回结果集
mCursor = getContentResolver().query(
    UserDictionary.Words.CONTENT_URI,  // 单词表对应的URI
    mProjection,                       // 查找的列
    mSelectionClause                   // 选择子句
    mSelectionArgs,                    // 选择子句占位符参数
    mSortOrder);                       // 排序方式
// 有些provider执行时如果出错会返回空值,有些会抛出异常
if (null == mCursor) {
    // 处理异常
} else if (mCursor.getCount() < 1) {
    // 查询成功,但是没有数据
} else {
    // 查询成功,有数据,可以处理
}
上述的查询操作其实与数据库中查询SQL类似:

SELECT _ID,word,locale FROM words WHERE wore = <用户输入参数> ORDER BY word ASC;

当然,如果provider对应的数据就是数据库中的数据,可能就会遇到SQL注入这样的攻击。

例如 String mSelectionClause = "var = " + mUserInput,如mUserInput = "nothig; DROP TABLE *;"会导致数据库删除所有表。

解决方法当然是使用占位符?(占位符会将参数进行转义操作),防止恶意注入SQL。

三、结果操作

1)如果没有与选择条件匹配的行,则会返回Cursor.getCount()为0的Cursor对象;

2)如果出现内部错误,则结果将取决于具体的provider,可能会返回null,也可能抛出异常;

3)provider可能会根据查询的发出者性质来限制对列的访问。例如:联系人提供程序限定只有同步适配器才能访问某些列,这些列不会被返回值Activity或者服务;

4)一般,Cursor通过SimpleCursorAdapter与ListView关联,但要注意,这种关联方式,Cursor中必须包含_ID列。代码如下:

// 定义要从Cursor中获取的列名列表
String[] mWordListColumns =
{
    UserDictionary.Words.WORD,   // Contract class constant containing the word column name
    UserDictionary.Words.LOCALE  // Contract class constant containing the locale column name
};
// 定义与上述列名对应的数据要显示到的UI的id
int[] mWordListItems = { R.id.dictWord, R.id.locale};
// 创建Adapter
mCursorAdapter = new SimpleCursorAdapter(
    getApplicationContext(),               // ApplicationContext
    R.layout.wordlistrow,                  // 布局文件
    mCursor,                               // 查询结果集
    mWordListColumns,                      // cursor中需要的列名
    mWordListItems,                        // 布局文件中的对应控件id
    0);                                    // Flags (一般用不着)
mWordList.setAdapter(mCursorAdapter);
5)查询数据,示例:

// 确定名为WORD的列的列索引
int index = mCursor.getColumnIndex(UserDictionary.Words.WORD);
if (mCursor != null) {
    while (mCursor.moveToNext()) {

        // 获取值
        newWord = mCursor.getString(index);
    }
} else {
    // ...
}

四、其它操作

插入:

// 用于接收插入结果的URI
Uri mNewUri;
ContentValues mNewValues = new ContentValues();
mNewValues.put(UserDictionary.Words.APP_ID, "example.user");
mNewValues.put(UserDictionary.Words.LOCALE, "en_US");
mNewValues.put(UserDictionary.Words.WORD, "insert");
mNewValues.put(UserDictionary.Words.FREQUENCY, "100");

mNewUri = getContentResolver().insert(
    UserDictionary.Word.CONTENT_URI,
    mNewValues                          
);
其中newUri格式如下:content://user_dictionary/word/,id_value为新行的_ID.如果想要获取该id值,可以使用ContentUris.parseId()方法。

更新:

ContentValues mUpdateValues = new ContentValues();

// 更新条件
String mSelectionClause = UserDictionary.Words.LOCALE +  "LIKE ?";
String[] mSelectionArgs = {"en_%"};

// 被更新的行数
int mRowsUpdated = 0;

/*
 * 设置要更新的字段与值,此处是置空
 */
mUpdateValues.putNull(UserDictionary.Words.LOCALE);

mRowsUpdated = getContentResolver().update(
    UserDictionary.Words.CONTENT_URI,  
    mUpdateValues                       
    mSelectionClause                    
    mSelectionArgs                      
);
注意要进行输入校验。

删除:

// 删除条件
String mSelectionClause = UserDictionary.Words.APP_ID + " LIKE ?";
String[] mSelectionArgs = {"user"};

// 被删除行数
int mRowsDeleted = 0;

//执行删除
mRowsDeleted = getContentResolver().delete(
    UserDictionary.Words.CONTENT_URI,   
    mSelectionClause                    
    mSelectionArgs                      
);
注意要进行输入校验。

contentprovider可以提供如下类型:integer、文本、long、float、double、二进制大对象(blob),一般文档会给出列对应的数据类型,也可以通过Cursor.getType()来确定数据类型。

provider会维护其定义的每个内容URI的MIME数据类型,使用包含复杂 数据结构或者文件的provider时,通常要MIME类型。例如联系人provider中ContactsContract.Data会使用MIME类型标记每行中存储的联系人数据类型。使用ContentRsolver.getType()获取与内容URI对应的MIME类型。

五、contentprovider的替代

批量访问:可以通过ContentProviderOperation类中的方法创建一批访问调用,通过ContentResolver.applyBatch()应用,返回结果数组。

适用于插入大量航,或者通过同一方法调用在多表中插入,或者通常用于跨进程将一组操作作为事务处理执行。

异步查询:在单独线程中执行查询,一般使用CursorLoader。

通过Intent访问数据:Intent提供对provider的间接访问。即使不具备访问权限,也可以访问provider中的数据。

通过临时权限获取访问权限:

将Intent发送至具有权限的应用,然后接收包含URI权限的结果Intent。

具有永久权限的应用将通过在结果Intent中设置标志来授予临时权限:

读:FLAG_GRANT_READ_URI_PERMISSION,写:FLAG_GRANT_WRITE_URI_PERMISSION

但是这些标志不会为其授权包含在内容URI中的提供程序提供常规的读取或写入权限。访问权限仅适用于URI。(还不是太理解,先记下吧)

六、创建ContentProvider

数据存储:

一般是文件数据和数据库,甚至可以使网络数据。

表数据应该具有主键,如果想让结果链接到ListView,则必须含有列名_ID;

文件数据需要告诉用户,要使用ContentResolver来访问数据;

二进制大型对象(BLOB)类型存储大小或结构会发生变化的数据,如协议缓冲区或JSON结构。一般要通过MIME类型来指示数据类型。

内容URI:

ContentProvider的每个数据访问方法都将内容URI作为参数,可以利用他确定要访问的表、行或文件。

授权:provider通常具有单一授权。为避免与其他provider冲突,应使用域名作为基础。例如:如果包名为:com.example.,则provider 应为com.example..provider授权。

路径:通常我们通过追加指向单个表的路径来根据授权创建内容URI。例如:com.example..provider/和com.example..provider/

内容URI ID:provider通过接收末尾含有ID的内容URI来确定行。当用户将数据Cursor使用CursorAdapter绑定到ListView时(此时Cursor必须有一列为_ID),用户选中其中一行,可以根据其ID来执行查询或者修改操作。例如:com.example..provider//1。

注意其中有通用匹配符号*和#,*表示任意长度的有效字符串,#则表示任意长度的数字字符串。

实现contentprovider类:

必须方法:

query()查找,返回Cursor。如果是数据库,可以直接返回数据库的query返回的Cursor。如果没有匹配行,则返回一个实例,但是getCount()为0,出现异常返回null。如果不是数据库,则要创建实例返回。

insert()插入,返回插入行的内容URI,末尾含有ID;使用withAppendedId()向表内容URI追加新行的_ID值。

update()更新,返回更新行数;

delete()删除,返回删除的行数;

getType()返回内容URI对应的MIME类型,必须实现;

onCreate()初始化内容提供程序,系统会在创建provider之后立即调用此方法,只要ContentResolver对象尝试访问该provider时,才会创建它。

*getSystemTypes(),系统在provider提供文件时要实现的方法。

考虑因素:

1.除onCreate()外,其他方法必须是线程安全的。

2.避免在onCreate()中执行长时间操作,将初始化任务推迟到实际需要时进行。

3.不需要的方法可以忽略。






你可能感兴趣的:(Android,training+guide,随笔)