Android 中的 IPC 方式(四) ContentProvider

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/sinat_29874521/article/details/82703546

ContentProvider 是 Android 中提供的专门用于不同应用间进行数据共享的方式,从这一点来看,它天生就适合进程间通信。和 Messenger 一样,ContentProvider 的底层实现也是 Binder,由此可见,Binder 在 Android 系统中是何等重要。虽然 ContentProvider 的底层是 Binder,但是它的使用过程要比 AIDL 简单的多,这是因为系统为我们做了封装,使得我们无需关心底层细节即可轻松实现,ContentProvider 使用起来很简单,包括自己创建一个 ContentProvider 也不是什么难事,尽管如此,它的细节还是相当多的,比如 CRUD 操作、防止 SQL 注入和权限控制等。

系统配置了许多 ContentProvider,比如通讯录信息、日程表信息等。要跨进程访问这些信息,只需要通过 ContentProvider 的 query。update。insert 和 deleted 方法即可。我们来实现一个 ContentProvider,并演示如何在其他的应用中获取 ContentProvider 中的数据从而实现进程间通信这一目的。首先,我们先创建一个 ContentProvider,名字就叫 BookProvider,创建一个自定义的 ContentProvider 很简单,只需要继承 ContentProvider 类并实现它的六个方法即可。这六个抽象方法很好理解,onCreate 代表 ContentProvider 的创建,一般来说,我们需要做一些初始化的工作;getType 用来返回一个 Uri 请求所对应的 MIME 类型(媒体类型),比如图片、视频等,这个媒体类型还是有点复杂的,如果我们的应用不关注这个选项,可以直接在这个方法中返回 null 或者 “*/*”;剩下的四个方法对应于 CRUD 操作,即实现对数据的增、删、改、查功能。根据 Binder 的工作原理,我们知道这六个方法均运行在 ContentProvider 的进程中,除了 onCreate 油系统回调并在主线程里运行,其他五个方法均由外界回调并运行在 Binder 线程池中。

ContentProvider 主要以表格的形式组织数据,并且可以包含多个表,对于每个表格来说,它们都具有行和列的层次性,行往往对应一条数据,列对应一条数据中的一个字段,这点和数据库相类似。除了表格的形式,ContentProvider 还支持文件数据,比如图片、视频等。文件数据和表格数据的结构不同,因此处理这类数据时可以在 ContentProvider 中返回文件的句柄给外界从而让文件来访问 ContentProvider 中的信息。Android 系统提供的 MediaStore 功能就是文件类型的 ContentProvider。另外,虽然 ContentProvider 的底层数据看起来很像一个 SQLite 数据库,但是 ContentProvider 对底层的存储数据没有任何要求,我们可以使用 SQLite 数据库,也可以使用普通文件,甚至可以采用内存中的一个对象进行数据存储。

下面看一个最简单的示例,它演示了 ContentProvider 的工作过程。首先创建一个 BookProvider 类,它继承自 ContentProvider 并实现了 ContentProvider 的六个方法,然后注册这个 ContentProvider,如下所示。其中 android/:authorities 是 ContentProvider 的唯一标识,通过这个属性外部应用就可以访问我们的 ContentProvider 了,因为,android/:authorities 必须是唯一的,为了掩饰进程间通信,我们让 BookProvider 运行在独立的进程中并给它加了权限,这样外界应用如果想访问 BookProvider,就必须声明“com.demo.text.demotext.PEOVIDER”这个权限,ContentProvider 的权限还可以细分为读权限和写权限,分别对应 android:readPermission 和 android:writePermission 属性,如果分别声明了读权限和写权限,那么外界应用也必须声明对应的权限才能进行读/写操作,否则外界应用会异常终止。

        <provider
            android:name=".BookProvider"
            android:authorities="com.demo.text.demotext.BookProvider"
            android:permission="com.demo.text.demotext.PEOVIDER"
            android:process=":provider"/>

注册了 ContentProvider 以后,我们就可以在外部应用中访问到它了。为了方便演示,这里仍然选择在同一个应用的其他进程中去访问这个 BookProvider。

        Uri uri = Uri.parse("content://com.demo.text.demotext.BookProvider");
        getContentResolver().query(uri, null, null, null, null);
        getContentResolver().query(uri, null, null, null, null);
        getContentResolver().query(uri, null, null, null, null);

在上面的代码中,我们通过 ContentProvider 对象的 query 方法去查询 BookProvider 中的数据,其中 “content://com.demo.text.demotext.BookProvider”唯一标识了 BookProvider,而这个标识正式前面我们为 BookProvider 的 android/:authorities 属性所指定的值,我们运行后看一下 log,我们可以看出,BookProvider 的 query 方法被调用了三次,并且这三次调用 不在同一个线程中,可以看出,它们运行在一个 Binder 线程中。

09-14 17:11:08.206 31899-31899/? I/BookProvider: onCreate,current thread:Thread[main,5,main]
09-14 17:11:08.212 31899-31913/? I/BookProvider: query,current thread:Thread[Binder_2,5,main]
09-14 17:11:08.213 31899-31912/? I/BookProvider: query,current thread:Thread[Binder_1,5,main]
09-14 17:11:08.214 31899-31913/? I/BookProvider: query,current thread:Thread[Binder_2,5,main]

接下来,在上面的基础上,我们继续完善 BookProvider,从而使其对外部应用提供数据。现在我们要提供一个 BookProvider,外部应用可以通过 BookProvider 来访问图书信息,为了更好的演示 BookProvider,用户还可以通过 BookProvider 访问用户信息。为了完成上述共鞥,我们需要一个数据库来管理图书和用户信息,这个数据库不难实现,代码如下:

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_TABLE_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_TABLE_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) {

    }

    public DbOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
        super(context, name, factory, version);
    }

}

上述代码是一个简单的数据库的实现,我们借助 SQLiteOpenHelper 来管理数据库的创建、升级和降级。下面我们就要通过 BookProvider 向外界提供上述数据库中的信息了。我们知道,ContentProvider 通过 Uri 来区分外界要访问的数据集合,在本例中支持外界对 BookProvider 中的 book 表和 user 表进行访问,为了知道外界要访问的是哪个表,我们需要为它们定义单独的 Uri 和 Uri_Code,并将对应的 Uri 和 Uri_Code 相关联,我们可以换使用 UriMatcher 的 addURI 方法将 Uri 和 Uri_Code 关联到一起。这样,当外界请求访问 BookProvider 时,我们就可以根据请求的 Uri 来得到 Uri_Code,有了 Uri_Code 我们就可以知道外界想访问的是哪个表,然后就可以进行相应的数据操作了。具体代码如下:

public class BookProvider extends ContentProvider {

    private static final String TAG = "BookProvider";
    public static final String AUTHORITY = "com.demo.text.demotext.BookProvider";
    public static final Uri BOOK_CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/book");
    public 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);
    }
    
    ...

}

从上面的代码可以看出,我们分别为 book 表和 user 表制定了 Uri,分别为“content://com.demo.text.demotext.BookProvider/book” 和 “content://com.demo.text.demotext.BookProvider/user”,这两个 Uri 所关联的 Uri_Code 分别为 0 和 1,这个关联过程是通过下面的语句来完成的:

        sUriMatcher.addURI(AUTHORITY, "book", BOOK_URI_CODE);
        sUriMatcher.addURI(AUTHORITY, "user", USER_URI_CODE);

将 Uri 和 Uri_Code 管理以后,我们就可以通过如下方式来获取外界所需要访问的数据源,根据 Uri 先取出 Uri_Code,根据 Uri_Code 在得到数据表的名称,知道了外界要访问的表,接下来就可以响应外界的增删改查了。

    private String getTableName(Uri uri) {
        String tableName = null;
        switch (sUriMatcher.match(uri)) {
            case BOOK_URI_CODE:
                tableName = DbOpenHelper.BOOK_TABLE_NAME;
                break;
            case USER_URI_CODE:
                tableName = DbOpenHelper.USER_TABLE_NAME;
                break;
            default:
                break;
        }
        return tableName;
    }

接着,我们就可以实现增删改查方法了。如下 query 方法的实现,首先我们要从 Uri 中取出外界要访问的表的名称,然后根据外界传递的查询参数就可以进行数据库的查询操作了,这个过程实现比较简单。

    @Nullable
    @Override
    public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
        Log.i(TAG, "query,current thread:" + Thread.currentThread());
        String table = getTableName(uri);
        if(table == null) {
            throw new IllegalArgumentException("Unsupported URI:" + uri);
        }
        return mDb.query(table, projection, selection, selectionArgs, null, null, sortOrder, null);
    }

另外三个方法的实现和 query 方法是类似的,只有一点不同,那就是 insert、update 和 delete 方法会引起数据源的改变,这个时候我们徐彤通过 ContentProvider 的 notifyChange 方法来通知外界当前 ContentProvider 中的数据已经发生改变。要观察一个 ContentProvider 中数据的改变情况,可以通过 ContentResolver 的 registerContentObserver 方法来注册观察者,通过 unregisterContentObserver 方法来接触观察者。对于这三个方法,这里不再详细解释了,BookProvider 的完整代码如下:

public class BookProvider extends ContentProvider {

    private static final String TAG = "BookProvider";
    public static final String AUTHORITY = "com.demo.text.demotext.BookProvider";
    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);

    private Context context;
    private SQLiteDatabase mDb;

    static {
        sUriMatcher.addURI(AUTHORITY, "book", BOOK_URI_CODE);
        sUriMatcher.addURI(AUTHORITY, "user", USER_URI_CODE);
    }

    private void initProviderData() {
        mDb = new DbOpenHelper(context).getWritableDatabase();
        mDb.execSQL("delete from " + DbOpenHelper.BOOK_TABLE_NAME);
        mDb.execSQL("delete from " + DbOpenHelper.USER_TABLE_NAME);
        mDb.execSQL("insert into book values(3, 'Anddroid');");
        mDb.execSQL("insert into book values(4, 'IOS');");
        mDb.execSQL("insert into book values(5, 'Html5');");
        mDb.execSQL("insert into user values(1, 'jake', 1);");
        mDb.execSQL("insert into user values(2, 'jasmine', 0);");
    }

    @Override
    public boolean onCreate() {
        context = getContext();
        initProviderData();
        Log.i(TAG, "onCreate,current thread:" + Thread.currentThread());
        return true;
    }

    @Nullable
    @Override
    public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
        Log.i(TAG, "query,current thread:" + Thread.currentThread());
        String table = getTableName(uri);
        if(table == null) {
            throw new IllegalArgumentException("Unsupported URI:" + uri);
        }
        return mDb.query(table, projection, selection, selectionArgs, null, null, sortOrder, null);
    }

    @Nullable
    @Override
    public String getType(@NonNull Uri uri) {
        Log.i(TAG, "getType");
        return null;
    }

    @Nullable
    @Override
    public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
        Log.i(TAG, "insert");
        String table = getTableName(uri);
        if(table == null) {
            throw new IllegalArgumentException("Unsupported URI:" + uri);
        }
        mDb.insert(table, null, values);
        context.getContentResolver().notifyChange(uri, null);
        return uri;
    }

    @Override
    public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
        Log.i(TAG, "delete");
        String table = getTableName(uri);
        if(table == null) {
            throw new IllegalArgumentException("Unsupported URI:" + uri);
        }
        int count = mDb.delete(table, selection, selectionArgs);
        if(count > 0) {
            context.getContentResolver().notifyChange(uri, null);
        }
        return count;
    }

    @Override
    public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
        Log.i(TAG, "update");
        String table = getTableName(uri);
        if(table == null) {
            throw new IllegalArgumentException("Unsupported URI:" + uri);
        }
        int row = mDb.update(table, values, selection, selectionArgs);
        if(row > 0) {
            context.getContentResolver().notifyChange(uri, null);
        }
        return row;
    }

    private String getTableName(Uri uri) {
        String tableName = null;
        switch (sUriMatcher.match(uri)) {
            case BOOK_URI_CODE:
                tableName = DbOpenHelper.BOOK_TABLE_NAME;
                break;
            case USER_URI_CODE:
                tableName = DbOpenHelper.USER_TABLE_NAME;
                break;
            default:
                break;
        }
        return tableName;
    }

}

需要注意的是,query、update、insert、delete 四大方法是存在多线程并发访问的,因此方法内部需要做好线程同步。在本例中,由于采用的 SQLite 并且只有一个 SQLiteDatabase 的连接,所以可以对应多线程的情况,具体原因是 SQLiteDatabase 内部对数据库的操作是有同步处理的,但是如果通过多个 SQLiteDatabase 对象来操作数据库就无法保证县城同步,因为 SQLiteDatabase 对象之间无法进行线程同步。如果 ContentProvder 的底层数据集是一快内存的话,比如是 List,着这种情况下同 List 遍历、插入、删除操作就需要进行线程同步,否则会引起并发错误,这点尤其需要注意的。到这里 BookProvder 已经实现完成了,接下来我们在外部访问一下它,看看能否正常工作。 

        Uri bookUrl = Uri.parse("content://com.demo.text.demotext.BookProvider/book");
        ContentValues values = new ContentValues();
        values.put("_id", 6);
        values.put("name", "PHP");
        getContentResolver().insert(bookUrl, values);
        Cursor bookCursor = getContentResolver().query(bookUrl, new String[]{"_id", "name"}, null, null, null);
        while (bookCursor.moveToNext()) {
            Book book = new Book();
            book.setBookId(bookCursor.getInt(0));
            book.setBookName(bookCursor.getString(1));
            Log.i(TAG, "query book:" + book.toString());
        }
        bookCursor.close();

        Uri userUrl = Uri.parse("content://com.demo.text.demotext.BookProvider/user");
        Cursor userCursor = getContentResolver().query(userUrl, new String[]{"_id", "name", "sex"}, null, null, null);
        while (userCursor.moveToNext()) {
            UserBean userBean = new UserBean();
            userBean.setUserId(userCursor.getInt(0));
            userBean.setUserName(userCursor.getString(1));
            userBean.setMale(userCursor.getInt(2) == 1);
            Log.i(TAG, "query user:" + userBean.toString());
        }
        userCursor.close();

默认情况下,BookProvider 的数据库有三本书和两个用户,在上面的代码中,我们首先添加一本书:“PHP”,接着查询所有的图书,这个时候应该查询出四本书,因为我们刚刚添加了一本,然后查询所有用户,这个时候应该查询出两个用户,是不是这样呢?我们来看下 log:

09-17 13:36:34.677 6394-6394/com.demo.text.demotext I/MainActivity: query book:[bookId:3, bookName:Anddroid]
09-17 13:36:34.677 6394-6394/com.demo.text.demotext I/MainActivity: query book:[bookId:4, bookName:IOS]
09-17 13:36:34.677 6394-6394/com.demo.text.demotext I/MainActivity: query book:[bookId:5, bookName:Html5]
09-17 13:36:34.677 6394-6394/com.demo.text.demotext I/MainActivity: query book:[bookId:6, bookName:PHP]
09-17 13:36:34.681 6394-6394/com.demo.text.demotext I/MainActivity: query user:User:{userId:1, userName:jake, isMale:true}, with child:{null}
09-17 13:36:34.681 6394-6394/com.demo.text.demotext I/MainActivity: query user:User:{userId:2, userName:jasmine, isMale:false}, with child:{null}

从上述 log 可以看到,我们确实查询到了四本书两个用户,这说明 BookProvider 已经能够处理外部的请求了,其实,ContentProvider 除了支持对数据的增删改查这四个操作,还支持自定义调用,这个过程是通过 ContentProvider 的 Call 方法和 ContentResolver 的 Call 方法来完成的。当然 ContentProvider 还有一些细节没有讲到,在后续的文章中会进一步讲解。

猜你喜欢

转载自blog.csdn.net/sinat_29874521/article/details/82703546