1. ContentProvider简介
ContentProvider是Android中专门用于不同应用间进行数据共享的方式,也就是可以实现进程间通信。和Messenger一样,ContentProvider的底层实现同样也是Binder。
系统预置了许多ContentProvider,比如通讯录信息、日程表信息等,要跨进程访问这些信息,只需要通过ContentResolver的query,update,insert和delete方法即可。
ContentProvider主要以表格的形式来组织数据,并且可以包含多个表,对于每个表格来说,他们都具有行和列的层次性,行往往对应一条记录,而列对应一条记录中的一个字段。和数据库类似。
除了表格形式,ContentProvider还支持文件数据,比如图片、视频等。文件数据和表格数据的结构不同,因此处理此类数据时可以在ContentProvider中返回文件的句柄(handle)给外界从而让文件来访问ContentProvider中的文件信息。Android系统提供的MediaStore功能就是文件类型的ContentProvider,详细参考MediaStore。
虽然ContentProvider的底层看起来像是一个SQLite数据库,但是ContentProvider对底层的数据存储方式没有任何要求,我们既可以使用SQLite数据库,也可以使用普通文件,甚至可以使用内存中的对象进行存储。
2. 自己实现ContentProvider
public class BookProvider extends ContentProvider {
@Override
public boolean onCreate() {
Log.e("aaa", "thread:" + Thread.currentThread().getName());
return false;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
Log.e("aaa", "thread:" + Thread.currentThread().getName());
return null;
}
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
throw new UnsupportedOperationException("Not yet implemented");
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
throw new UnsupportedOperationException("Not yet implemented");
}
// getType用来返回一个Uri请求所对应的MIME类型(媒体类型),比如图片,视频等,如果我们不关注类型,可以返回null或*/*
@Override
public String getType(Uri uri) {
return "*/*";
}
@Override
public Uri insert(Uri uri, ContentValues values) {
throw new UnsupportedOperationException("Not yet implemented");
}
}
2. manifest中的ContentProvider配置
<provider
android:name="._2activity.ContentProvider.BookProvider"
android:authorities="qingfengmy.developmentofart.bookprovider"
android:exported="true"
android:permission="qingfengmy.developmentofart.provider"
android:process=":provider" />
authorities是ContentProvider的唯一标识,通过这个属性外部应用就可以访问我们的BookProvider,因此authorities必须是唯一的,建议加上包名前缀,保证唯一。
permission是我们加的权限,访问我们Provider数据必须加上这个权限。权限还支持readPermission和writePermission属性,如果声明了读权限和写权限,那么外界应用还要加上这两个权限。读权限和写权限是高于普通权限的,也就是三个权限都定义了,外界只要有读权限就可读,有写权限就可以写,有普通权限什么也干不了。
export是允许让其他应用访问。
<permission
android:name="qingfengmy.developmentofart.provider"
android:protectionLevel="normal" />
自定义权限,provider中添加的权限是需要自定义的,否则只是一个普通字符串。权限是需要在系统中声明的,字符串没有声明,是永远匹配不到的。
3. 外界访问
Uri uri = Uri.parse("content://qingfengmy.developmentofart.bookprovider");
getContentResolver().query(uri,null,null,null,null);
getContentResolver().query(uri,null,null,null,null);
getContentResolver().query(uri,null,null,null,null);
Uri中,content是scheme协议名称,后面的是我们在manifest中配置的authorities的值,是唯一的。执行三次查询,打印他们的线程名称如下:
06-17 09:22:32.040 8594-8594/qingfengmy.developmentofart:remote E/aaa: thread:main
06-17 09:22:32.042 8594-8606/qingfengmy.developmentofart:remote E/aaa: thread:Binder_2
06-17 09:22:32.042 8594-8605/qingfengmy.developmentofart:remote E/aaa: thread:Binder_1
06-17 09:22:32.043 8594-8606/qingfengmy.developmentofart:remote E/aaa: thread:Binder_2
其中onCreate是main线程;其他三次查询是三个不同的子线程,binder开头,说明是Binder线程池中的。
另外,我们在同一应用中, 尽管访问和provider是不同进程,我们应用中不配置user-permission也可以访问数据。
如果不同应用,肯定是不同进程,不配置权限,访问不了。报如下错误:
Caused by: java.lang.SecurityException:
Permission Denial:
opening provider qingfengmy.developmentofart._2activity.ContentProvider.BookProvider
from ProcessRecord{9e674b5 10691:qingfengmy.behaviordemo.free/u0a95}
(pid=10691, uid=10095) that is not exported from uid 10668
加上权限则可以访问。可见权限是应用级别的,不是进程级别的。同应用没有权限限制,不同应用才有限制。Binder中的权限拦截,是java代码主动检测的,所以本应用内,也需要配置user-permission。
4. 实现数据库管理图书和用户信息
public class DbOpenHelper extends SQLiteOpenHelper {
private static final String DB_NAME = "book_provider.db";
public static final String BOOK_TABLE_NAME = "book";
public static final String USER_TALBE_NAME = "user";
private static final int DB_VERSION = 1;
private String CREATE_BOOK_TABLE = "CREATE TABLE IF NOT EXISTS "
+ BOOK_TABLE_NAME + "(_id INTEGER PRIMARY KEY," + "name TEXT)";
private String CREATE_USER_TABLE = "CREATE TABLE IF NOT EXISTS "
+ USER_TALBE_NAME + "(_id INTEGER PRIMARY KEY," + "name TEXT,"
+ "sex INT)";
public DbOpenHelper(Context context) {
super(context, DB_NAME, null, DB_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(CREATE_BOOK_TABLE);
db.execSQL(CREATE_USER_TABLE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// TODO ignored
}
}
5. 定义Uri和Uri_Code
private static final String AUTHORITY = "qingfengmy.developmentofart.bookprovider";
private static final Uri BOOK_CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/book");
private static final Uri USER_CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/user");
public static final int BOOK_URI_CODE = 0;
public static final int USER_URI_CODE = 1;
private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
static {
sUriMatcher.addURI(AUTHORITY, "book", BOOK_URI_CODE);
sUriMatcher.addURI(AUTHORITY, "user", USER_URI_CODE);
}
ContentProvider通过Uri来区分外界要访问的数据集合,BookProvider支持book表和user表的访问,所以要单独定义Uri和Uri_Code,并用UriMatcher的addURI关联起来。上面单独定义了book的uri和code,以及user的uri和code.
addURI方法定义如下:
public void addURI(String authority, String path, int code){}
6. 根据uri获取表名
private String getTableName(Uri uri){
String tableName = null;
// match方法是根据addURI加入的uri和code返回code
switch (sUriMatcher.match(uri)){
case BOOK_URI_CODE:
tableName = DbOpenHelper.BOOK_TABLE_NAME;
break;
case USER_URI_CODE:
tableName = DbOpenHelper.USER_TALBE_NAME;
break;
}
return tableName;
}
7. 实现查询操作
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
String tableName = getTableName(uri);
return mDb.query(tableName, projection, selection, selectionArgs, null, null, sortOrder, null);
}
List<Book> bookList = new ArrayList<>();
Uri uri = Uri.parse("content://qingfengmy.developmentofart.bookprovider/book");
Cursor bookCursor = (Cursor) getContentResolver().query(uri, null, null, null, null);
while (bookCursor.moveToNext()) {
int bookId = bookCursor.getInt(0);
String bookName = bookCursor.getString(1);
Book book = new Book(bookId, bookName);
bookList.add(book);
}
Log.e("aaa", bookList.toString());
8. 实现insert操作
@Override
public Uri insert(Uri uri, ContentValues values) {
String tableName = getTableName(uri);
mDb.insert(tableName,null,values);
// 回调监听
getContext().getContentResolver().notifyChange(uri,null);
return uri;
}
数据变化时的监听
getContentResolver().registerContentObserver(uri, true, new ContentObserver(new Handler()) {
@Override
public void onChange(boolean selfChange, Uri uri) {
super.onChange(selfChange, uri);
Log.e("aaa", "onChange:" + uri.toString());
}
});
9. 线程同步问题
query,update,insert,delete四大方法都是存在多线程并发访问的,因此要做好线程同步。
在本例中,由于采用的是SQLite并且只有一个SQLiteDatabase的连接,所以可以正确应对多线程的情况。具体原因是SQLiteDatabase内部对数据库的操作有同步处理。
10. call方法
ContentProvider除了增删改查四大方法之外,还可以自定义方法。这个过程是通过ContentProvider的Call方法和ContentProvider的Call方法来完成的。