Android提供了5种方式存储数据。
(1)SQLite数据库存储数据
(2)文件存储数据
(3)网络存储数据
(4)SharedPreferences存储数据
(5)ContentProvider存储数据
ContentProvider
1、适用场景
(1)ContentProvider为存储和读取数据提供了统一的接口
(2)使用ContentProvider,应用程序可以实现数据共享
(3)android内置的许多数据都是使用ContentProvider形式,供开发者调用的(如视频,音频,图片,通讯录等)
2、ContentProvider介绍
(1)ContentProvider简介 App中新建一个类继承ContentProvider类,并重写该类用于提供数据和存储数据的方法,就可以向其他应用共享其数据。虽然使用其他方法也可以对外共享数据,但数据访问方式会因数据存储的方式而不同,如:采用文件方式对外共享数据,需要进行文件操作读写数据;采用sharedpreferences共享数据,需要使用sharedpreferences API读写数据。而使用ContentProvider共享数据的好处是统一了数据访问方式。
(2)Uri 通用资源标志符(Universal Resource Identifier, 简称"URI")简介 Uri uri = Uri.parse("content://com.changcheng.provider.contactprovider/contact") 在Content Provider中使用的查询字符串有别于标准的SQL查询。很多诸如select, add, delete, modify等操作我们都使用一种特殊的URI来进行,这种URI由3个部分组成, “content://”, 代表数据的路径,和一个可选的标识数据的ID。
举个实际的例子:
content://com.example.project:200/folder/subfolder/etc
\---------/ \---------------------------/ \---/ \--------------------------/
scheme host port path
\--------------------------------/
authority
1.scheme:ContentProvider(内容提供者)的scheme已经由Android所规定为:content://。
2.主机名(或Authority):用于唯一标识这个ContentProvider,外部调用者可以根据这个标识来找到它。
3.路径(path):可以用来表示我们要操作的数据,路径的构建应根据业务而定,如下:
content://media/internal/images 这个URI将返回设备上存储的所有图片
content://contacts/people/ 这个URI将返回设备上的所有联系人信息
content://contacts/people/45 这个URI返回单个结果(联系人信息中ID为45的联系人记录)
尽管这种查询字符串格式很常见,但是它看起来还是有点令人迷惑。为此,Android提供一系列的帮助类(在android.provider包下),里面包含了很多以类变量形式给出的查询字符串,这种方式更容易让我们理解一点,因此,如上面content://contacts/people/45这个URI就可以写成如下形式:
Uri person = ContentUris.withAppendedId(People.CONTENT_URI, 45);
然后执行数据查询:
Cursor cur = managedQuery(person, null, null, null);
这个查询返回一个包含所有数据字段的游标,可以通过迭代这个游标来获取所有的数据:
cursor = contentResolver.query(IPersonProvider.CONTENT_URI, new String[] { IPersonProvider.PERSON_ID,IPersonProvider.PERSON_NAME, IPersonProvider.PERSON_AGE },
null, null, "_id");
if (cursor != null && cursor.moveToFirst()) {
Person p = new Person();
do {
p.setName(cursor.getString(cursor.getColumnIndexOrThrow(IPersonProvider.PERSON_NAME)));
p.setId(cursor.getString(cursor.getColumnIndexOrThrow(IPersonProvider.PERSON_ID)));
p.setAge(cursor.getInt(cursor.getColumnIndexOrThrow(IPersonProvider.PERSON_AGE)));
str.append(" person.id="+p.getId()+" person.name="+p.getName()+" person.age="+p.getAge()+ "\n");
textView.setText(str.toString());
}while (cursor.moveToNext());
} else {
Log.i("TAG", "query failure!");
}
使用ContentResolver.update()方法来修改数据,写一个修改数据的方法,修改指定id的行的人的姓名。updateRecord(1,"ssss")修改第1条记录的人姓名为ssss:
private void updateRecord(int recNo, String name) {
Uri uri = ContentUris.withAppendedId(IPersonProvider.CONTENT_URI, recNo);
ContentValues values = new ContentValues();
values.put(IPersonProvider.PERSON_NAME, name);
getContentResolver().update(uri, values, null, null);
}
使用ContentResolver.insert()方法来插入数据,该方法接受一个要增加的记录的目标URI,以及一个包含了新记录值的Map对象,调用后的返回值是新记录的URI,包含记录号。
private int insertData(Person person) {
ContentValues values = new ContentValues();
values.put(IPersonProvider.PERSON_NAME,person.getName());
values.put(IPersonProvider.PERSON_AGE,person.getAge());
Uri uri = getContentResolver().insert(IPersonProvider.CONTENT_URI,values);
String lastPath = uri.getLastPathSegment();
if (TextUtils.isEmpty(lastPath)) {
Log.i(TAG, "insert failure!");
} else {
Log.i(TAG, "insert success! the id is " + lastPath);
}
return Integer.parseInt(lastPath);
}
使用ContentResolver.insert()方法来删除数据:
删除该表所有信息
private void deleteRecords() {
Uri uri =IPersonProvider.CONTENT_URI;
getContentResolver().delete(uri, null, null);
}
删除指定姓名的行
private void deleteRecords(String name) {
Uri uri =IPersonProvider.CONTENT_URI;
getContentResolver().delete(uri, IPersonProvider.PERSON_NAME + " = 'Michelle'",null );
}
3、创建ContentProvider
当应用需要通过ContentProvider对外共享数据时,第一步需要继承ContentProvider并重写下面方法:
public class PersonContentProvider extends ContentProvider{
public boolean onCreate()
public Uri insert(Uri uri, ContentValues values)
public int delete(Uri uri, String selection, String[] selectionArgs)
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs)
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)
public String getType(Uri uri)
}
并定义需要用到常量数据
public interface IPersonProvider extends BaseColumns {
public static final String AUTHORITY = "com.caidongdong.contentprovider.person";
public static final String CONTENT_TYPE = "vnd.android.cursor.dir/person";
public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/person";
public static final String PERSON_NAME = "name";
public static final String PERSON_AGE = "age";
public static final Uri CONTENT_URI = Uri.parse("content://"+ AUTHORITY +"/persons");
public static final String TABLE_NAME = "person";
public static final String DEFAULT_SORT_ORDER = "age desc";
}
第二步需要在AndroidManifest.xml使用对该ContentProvider进行配置,为了能让其他应用找到该ContentProvider ,ContentProvider采用了authorities(主机名/域名)对它进行唯一标识,你可以把ContentProvider看作是一个网 站(想想,网站也是提供数据者),authorities 就是他的域名:
<!-- 注意这个地方的位置,是在application标签里面;android:authorities对应Provider.AUTHORITY -->
<provider
android:name=".provider.PersonProvider"
android:authorities="com.caidongdong.contentprovider.person"
android:multiprocess="true"
android:exported="true">
</provider>
创建你的数据存储系统。大多数Content Provider使用Android文件系统或SQLite数据库来保持数据,但是你也可以以任何你想要的方式来存储。如果你要存储字节型数据,比如位图文件等,数据列其实是一个表示实际保存文件的URI字符串,通过它来读取对应的文件数据。处理这种数据类型的Content Provider需要实现一个名为_data的字段,_data字段列出了该文件在Android文件系统上的精确路径。这个字段不仅是供客户端使用,而且也可以供ContentResolver使用。客户端可以调用ContentResolver.openOutputStream()方法来处理该URI指向的文件资源;如果是ContentResolver本身的话,由于其持有的权限比客户端要高,所以它能直接访问该数据文件。
4、监听ContentProvider数据变化
如果ContentProvider的访问者需要知道ContentProvider中的数据发生变化,可以在ContentProvider发生数据变化时调用getContentResolver().notifyChange(uri, null)来通知注册在此URI上的访问者,例子如下:
public class PersonContentProvider extends ContentProvider {
public Uri insert(Uri uri, ContentValues values) {
db.insert("person", "personid", values);
getContext().getContentResolver().notifyChange(uri, null);
}
}
如果ContentProvider的访问者需要得到数据变化通知,必须使用ContentObserver对数据(数据采用uri描述)进行监听,当监听到数据变化通知时,系统就会调用ContentObserver的onChange()方法:
getContentResolver().registerContentObserver(Uri.parse("content://com.ljq.providers.personprovider/person"),
true, new PersonObserver(new Handler()));
public class PersonObserver extends ContentObserver{
public PersonObserver(Handler handler) {
super(handler);
}
public void onChange(boolean selfChange) {
//此处可以进行相应的业务处理
}
}
5、权限设置与线程同步
(1)可以在代码中通过setReadPermission()和setWritePermission()两个方法来设置ContentProvider的操作权限,也可以在配置文件中通过android:readPermission和android:writePermission属性来控制。
(2)因为ContentProvider可能被不同的进程和线程调用,所以里面的方法必须是线程安全的。
自定义ContentProvider读写权限
<permission
android:name="com.caidongdong.contentprovider.person.read"
android:label="provider pomission"
android:protectionLevel="normal"/>
<permission
android:name="com.caidongdong.contentprovider.person.write"
android:label="provider write pomission"
android:protectionLevel="normal" />
在provider标签中也需要设置改权限才有效
<provider
android:name=".provider.PersonProvider"
android:authorities="com.caidongdong.contentprovider.person"
android:readPermission="com.caidongdong.contentprovider.person.read"
android:writePermission="com.caidongdong.contentprovider.person.write"
android:multiprocess="true"
android:exported="true">
这里对读写都加了权限,若第三方app想要获得数据,就必须要在Manifest.xml中获得读和写的权限
<uses-permission android:name="com.caidongdong.contentprovider.person.read" />
如果这里不申请写权限,那么是没有办法向提供数据的app写入数据的。
提一下这个android:multiprocess="true",数据可能被不同的第三方app同时访问,要保持数据的同步线程安全就需要配置进程之间是否是单例,这句代码就是使用单例模式的意思。
如果你对下面几个问题感兴趣,那么就点击这个链接
1. 在应用程序A里面怎么跨进程拿到ContentProvider的对象呢?
2. ContentProvider实例对象是保存在哪里呢?
3. ContentProvider的方法实现要注意线程安全吗?
6、效果展示
附上自己做的demo图,一个提供数据源的app,一个第三方访问ContentProvider的app。
访问ContentProvider的app
需要源码的请留下邮箱