Android四大组件——ContentProvider(基础篇)

概述

ContentProvider是Android中提供的专门用于不同应用间数据交互和共享的组件。ContentProvider实际上是对SQLiteOpenHelper的进一步封装,以一个或多个表的形式将数据呈现给外部应用,通过Uri映射来选择需要操作数据库中的哪个表,并对表中的数据进行增删改查处理。ContentProvider其底层使用了Binder来完成APP进程之间的通信,同时使用匿名共享内存来作为共享数据的载体。ContentProvider支持访问权限管理机制,以控制数据的访问者及访问方式,保证数据访问的安全性。

相关知识

在介绍ContentProvider之前,我们先简单介绍一下使用过程中涉及到的相关知识。

URI

URI(Uniform Resource Identifier)即统一资源标识符,是一个用于标识某一互联网资源名称的字符串。以联系人Contacts的Uri为例,其结构如下所示:

Uri结构图.png

  • schema: Android中固定为content://。
  • authority: 用于唯一标识一个ContentProvider。
  • path: ContentProvider中数据表的表名。
  • id: 数据表中数据的标识,可选字段。

MIME类型

MIME(Multipurpose Internet Mail Extensions)即多用途互联网邮件扩展类型,是指定某种扩展名的文件用什么应用程序来打开的方式类型。当该扩展名文件被访问的时候,浏览器会自动使用指定应用程序来打开。多用于指定一些客户端自定义的文件名,以及一些媒体文件打开方式。

类型/子类型(Content-Type/subtype ) 扩展名
application/vnd.android.package-archive .apk
text/plain .txt
image/jpeg .jpeg
text/html .html
audio/x-pn-realaudio .rmvb
audio/mpeg .mp3
video/mp4 .mp4
image/png .png
application/json .json
application/pdf .pdf

关于MIME的类型可以参看我的Android进阶之旅------>MIME类型大全,里面列出了所有的类型。

UriMatcher类

UriMatcher类是一个工具类,帮助匹配ContentProvider中的Uri。只提供了两个方法——addURI和match方法。

    private final static String AUTHORITY = "com.android.peter.provider";
    private final static int STUDENT_URI_CODE = 0;
    private final static UriMatcher sUriMatcher;

    static {
        sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
        //把Uri和Uri_Code相关联
        sUriMatcher.addURI(AUTHORITY,"student",STUDENT_URI_CODE);
    }
    //通过match方法能够根据传递的uri匹配到对应的Uri_Code
    int uriType = sUriMatcher.match(uri);

ContentUris类

ContentUris类代码很短,只包含了withAppendedId、parseId、appendId三个静态方法。以接下来示例中要用到的uri("content://com.android.peter.provider/student")为例,依次执行withAppendedId和parseId方法。

        Uri uri = Uri.parse("content://com.android.peter.provider/student");
        Uri withAppendedIdUri = ContentUris.withAppendedId(uri, 1);
        Log.d(TAG," withAppendedId ~ uri = " + withAppendedIdUri.toString());
        long parseId = ContentUris.parseId(withAppendedIdUri);
        Log.d(TAG," parseId ~ uri = " + parseId);

输出的log如下:

04-10 17:56:03.668 15652-15652/com.android.peter.contentproviderdemo D/MainActivity:  withAppendedId ~ uri = content://com.android.peter.provider/student/1
04-10 17:56:03.669 15652-15652/com.android.peter.contentproviderdemo D/MainActivity:  parseId ~ uri = 1

从log中可以看出,调用withAppendedId方法会在原始的uri后面添加了一个值为1的id,调用parseId方法可以取出这个id。

appendId方法用于通过Uri.Builder方式生成的Uri使用。

        Uri.Builder ub = new Uri.Builder();
        ub.authority("com.android.peter.provider")
        .appendPath("student");
        Log.d(TAG,"ub = " + ub.toString());
        Uri.Builder appendIdUri = ContentUris.appendId(ub,1);
        Log.d(TAG,"appendIdUri = " + appendIdUri.toString());

输出log如下:

04-10 18:10:48.283 19995-19995/com.android.peter.contentproviderdemo D/MainActivity: ub = //com.android.peter.provider/student
04-10 18:10:48.284 19995-19995/com.android.peter.contentproviderdemo D/MainActivity: appendIdUri = //com.android.peter.provider/student/1

ContentProvider的使用

ContentProvider 的使用可以简单的归纳为以下几步:
1、创建自己的数据列表;
2、自定义ContentProvider实现相关的抽象方法;
3、在AndroidManifest中声明provider以及定义相关访问权限;
4 、通过ContentResolver根据URI进行增删改查。

下面以创建一个student数据库列表,并通过自定义ContentProvider来访问数据为例,详细讲解一下每一步具体都做什么。

  • 创建自己的数据列表

首先,你应该根据项目需要选择合适的数据类型设计你的数据库列表,并转化成对应的可执行的SQL语句。然后,派生抽象类SQLiteOpenHelper创建其子类DBOpenHelper 并实现构造方法以及重载onCreate和onUpgrade方法。最后,在onCreate方法中执行你设计好的SQL语句。

public class DBOpenHelper extends SQLiteOpenHelper {
    private final static String TAG = "DBOpenHelper";

    private final static String DATABASE_NAME = "com_android_peter_provider.db";
    public final static String DATABASE_STUDENT_TABLE_NAME = "student";
    private final static int DATABASE_VERSION = 1;

    private Context mContext;

    /**
     * student table
     * @id primary key
     * @name student's name. e.g:peter.
     * @gender student's gender. e.g: 0 male; 1 female.
     * @number student's number. e.g: 201804081702.
     * @score student's score. more than 0 and less than 100. e.g:90.
     * */
    private final static String CREATE_STUDENT_TABLE = "CREATE TABLE IF NOT EXISTS "
            + DATABASE_STUDENT_TABLE_NAME
            + "(id INTEGER PRIMARY KEY,"
            + "name TEXT VARCHAR(20) NOT NULL,"
            + "gender BIT DEFAULT(1),"
            + "number TEXT VARCHAR(12) NOT NULL,"
            + "score INTEGER CHECK(score >= 0 and score <= 100))";

    public DBOpenHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
        mContext = context;
    }

    public DBOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
        super(context, DATABASE_NAME, factory, DATABASE_VERSION);
        mContext = context;
    }

    public DBOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version, DatabaseErrorHandler errorHandler) {
        super(context, DATABASE_NAME, factory, DATABASE_VERSION, errorHandler);
        mContext = context;
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        Log.d(TAG, "onCreate");
        db.execSQL(CREATE_STUDENT_TABLE);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        Log.d(TAG, "onUpgrade o = " + oldVersion + " , n = " + newVersion);
    }
}

创建的数据库位于"/data/data/包名/databases/"目录中。

  • 自定义ContentProvider类实现相关的抽象方法

创建一个自定义的ContentProvider需要实现以下几个方法:

方法 功能
onCreate() 初始化
query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) 查询数据
insert(@NonNull Uri uri, @Nullable ContentValues values) 插入数据
update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) 更新数据
delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) 删除数据
getType(@NonNull Uri uri) 获得数据的MIME类型

具体实现如下:

public class StudentContentProvider extends ContentProvider {
    private final static String TAG = "StudentProvider";

    private final static String AUTHORITY = "com.android.peter.provider";
    private final static int STUDENT_URI_CODE = 0;

    private Context mContext;
    private SQLiteDatabase mDataBase;
    private final static UriMatcher sUriMatcher;

    static {
        sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
        sUriMatcher.addURI(AUTHORITY,"student",STUDENT_URI_CODE);
    }

    @Override
    public boolean onCreate() {
        mContext = getContext();
        mDataBase = new DBOpenHelper(mContext).getWritableDatabase();

        return true;
    }

    @Nullable
    @Override
    public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
        int uriType = sUriMatcher.match(uri);
        Cursor cursor;
        switch (uriType) {
            case STUDENT_URI_CODE:
                cursor = mDataBase.query(DBOpenHelper.DATABASE_STUDENT_TABLE_NAME,projection,selection,selectionArgs,null,null,sortOrder,null);
                break;
            default:
                throw new IllegalArgumentException("UnSupport Uri : " + uri);
        }

        return cursor;
    }

    @Nullable
    @Override
    public String getType(@NonNull Uri uri) {
        return null;
    }

    @Nullable
    @Override
    public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
        int uriType = sUriMatcher.match(uri);
        long row;

        switch (uriType) {
            case STUDENT_URI_CODE:
                row = mDataBase.insert(DBOpenHelper.DATABASE_STUDENT_TABLE_NAME,null, values);
                break;
            default:
                throw new IllegalArgumentException("UnSupport Uri : " + uri);
        }

        if(row > -1) {
            mContext.getContentResolver().notifyChange(uri,null);
            return ContentUris.withAppendedId(uri, row);
        }

        return null;
    }

    @Override
    public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
        int uriType = sUriMatcher.match(uri);
        int rowDelete;

        switch (uriType) {
            case STUDENT_URI_CODE:
                rowDelete = mDataBase.delete(DBOpenHelper.DATABASE_STUDENT_TABLE_NAME,selection,selectionArgs);
                break;
            default:
                throw new IllegalArgumentException("UnSupport Uri : " + uri);
        }

        if(rowDelete > 0) {
            mContext.getContentResolver().notifyChange(uri,null);
        }

        return rowDelete;
    }

    @Override
    public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
        int uriType = sUriMatcher.match(uri);
        int rowUpdate;
        switch (uriType) {
            case STUDENT_URI_CODE:
                rowUpdate = mDataBase.update(DBOpenHelper.DATABASE_STUDENT_TABLE_NAME,values,selection,selectionArgs);
                break;
            default:
                throw new IllegalArgumentException("UnSupport Uri : " + uri);
        }

        if(rowUpdate > 0) {
            mContext.getContentResolver().notifyChange(uri,null);
        }

        return rowUpdate;
    }
}

注意:

  1. onCreate()由系统回调并运行在主线程里,其他五个方法由外界回调并运行在Binder线程池中。所以,不要在onCreate方法中做耗时操作。
  2. 增删改查操作存在多线程并发访问问题,因此方法内部要做好线程同步。
  3. SQLiteDatabase的insert方法的返回值是插入数据所在的行号,update和delete方法的返回值代表此次操作影响到的行数。
  • 在AndroidManifest中声明provider以及定义相关访问权限

在注册ContentProvider的时候通过android:process属性设置provider运行在单独的进程里,模拟进程间通信。

    <!-- student provider 访问权限声明 -->
    <permission
        android:name="com.android.peter.provider.READ_PERMISSION"
        android:label="Student provider read permission"
        android:protectionLevel="normal"
        />
    <permission
        android:name="com.android.peter.provider.WRITE_PERMISSION"
        android:label="Student provider read permission"
        android:protectionLevel="normal"
        />

    <!-- 声明ContentProvider -->
    <application
         ...
         <provider
            android:name=".StudentContentProvider"
            android:authorities="com.android.peter.provider"
            android:readPermission="com.android.peter.provider.READ_PERMISSION"
            android:writePermission="com.android.peter.provider.WRITE_PERMISSION"
            android:process=":provider"
            android:exported="true"/>
        ...
    </application>

为了方便起见,权限声明时protectionLevel设置的是最低风险权限(normal),关于其他等级权限和说明如下:

权限等级 说明
normal 低风险权限,只要申请了就可以使用,安装时不需要用户确认。
dangerous 高风险权限,安装时需要用户确认授权才可使用。
signature 只有当申请权限应用与声明此权限应用的数字签名相同时才能将权限授给它。
signatureOrSystem 签名相同或者申请权限的应用为系统应用才能将权限授给它。
  • 通过ContentResolver根据URI进行增删改查

在Activity中分别定义insertValue、queryValue、updateValue、deleteValue对自定义的provider进行测试。其中,database中id=1的数据为预存数据,为了方便查看数据的变化;Student类为student数据的封装类。

public class ClientActivity extends AppCompatActivity {
    private final static String TAG = "ClientActivity";

    /**
     * student table
     * @id primary key
     * @name student's name. e.g:peter.
     * @gender student's gender. e.g: 0 male; 1 female.
     * @number student's number. e.g: 201804081702.
     * @score student's score. more than 0 and less than 100. e.g:90.
     * */
    private final static String AUTHORITY = "com.android.peter.provider";
    private final static Uri STUDENT_URI = Uri.parse("content://" + AUTHORITY + "/student");

    private Context mContext;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_client);

        mContext = this;
    }

    @Override
    protected void onResume() {
        super.onResume();

        Log.d(TAG,"------------insert---------");
        insertValue();
        queryValue();
        Log.d(TAG,"------------update---------");
        updateValue();
        queryValue();
        Log.d(TAG,"------------delete---------");
        deleteValue();
        queryValue();
    }

    @Override
    protected void onPause() {
        super.onPause();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
    }

    private void insertValue() {
        ContentValues contentValues = new ContentValues();
        contentValues.put("id",0);
        contentValues.put("name","peter");
        contentValues.put("gender",0);
        contentValues.put("number","201804081705");
        contentValues.put("score","100");

        mContext.getContentResolver().insert(STUDENT_URI,contentValues);
    }

    private void queryValue() {
        Cursor cursor = getContentResolver().query(STUDENT_URI, new String[]{"id", "name","gender","number","score"},null,null,null);
        while (cursor.moveToNext()) {
            Student student = new Student();
            student.id = cursor.getInt(cursor.getColumnIndex("id"));
            student.name = cursor.getString(cursor.getColumnIndex("name"));
            student.gender = cursor.getInt(cursor.getColumnIndex("gender"));
            student.number = cursor.getString(cursor.getColumnIndex("number"));
            student.score = cursor.getInt(cursor.getColumnIndex("score"));
            Log.d(TAG,"student = " + student.toString());
        }
    }

    private void updateValue() {
        ContentValues contentValues = new ContentValues();
        contentValues.put("id",0);
        contentValues.put("name","update");
        contentValues.put("gender",1);
        contentValues.put("number","201804111048");
        contentValues.put("score","90");

        getContentResolver().update(STUDENT_URI,contentValues,"id = ?",new String[] {"0"});
    }

    private void deleteValue() {
        getContentResolver().delete(STUDENT_URI,"name = ?",new String[]{"update"});
    }
}
//student数据封装类
public class Student {
    private final static String TAG = "Student";

    public Integer id;
    public String name;
    public Integer gender;
    public String number;
    public Integer score;

    @Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", gender=" + gender +
                ", number='" + number + '\'' +
                ", score=" + score +
                '}';
    }
}

为了方便查看数据,insert、update、delete操作之后都会调用query重新查询打印log。增加一条id=0的数据,然后更新这条数据,最后删除这条数据,log如下:

04-11 10:51:13.405 31349-31349/com.android.peter.contentproviderclient D/ClientActivity: ------------insert---------
04-11 10:51:13.778 31349-31349/com.android.peter.contentproviderclient D/ClientActivity: student = Student{id=0, name='peter', gender=0, number='201804081705', score=100}
04-11 10:51:13.779 31349-31349/com.android.peter.contentproviderclient D/ClientActivity: student = Student{id=1, name='lemon', gender=1, number='201804091601', score=100}
04-11 10:51:13.779 31349-31349/com.android.peter.contentproviderclient D/ClientActivity: ------------update---------
04-11 10:51:13.796 31349-31349/com.android.peter.contentproviderclient D/ClientActivity: student = Student{id=0, name='update', gender=1, number='201804111048', score=90}
04-11 10:51:13.796 31349-31349/com.android.peter.contentproviderclient D/ClientActivity: student = Student{id=1, name='lemon', gender=1, number='201804091601', score=100}
04-11 10:51:13.796 31349-31349/com.android.peter.contentproviderclient D/ClientActivity: ------------delete---------
04-11 10:51:13.810 31349-31349/com.android.peter.contentproviderclient D/ClientActivity: student = Student{id=1, name='lemon', gender=1, number='201804091601', score=100}

如果是访问其它应用定义的provider只需要在AndroidManifest中声明相应权限即可使用,使用方法跟上面Activity中的示例是一样的。

    <uses-permission android:name="com.android.peter.provider.READ_PERMISSION"/>
    <uses-permission android:name="com.android.peter.provider.WRITE_PERMISSION"/>

ContentObserver

Android中提供的用来监听ContentProvider变化的抽象类,可以通过ContentResolver的registerContentObserver和unregisterContentObserver方法来注册和注销ContentObserver监听器。当被监听的ContentProvider发生变化时,就会回调对应的ContentObserver的onChange方法。具体用法如下:

    private final static int CONTENT_PROVIDER_CHANGED = 20180412;

    private Handler mHandler;
    private ContentObserver mContentObserver;
    //更新UI线程 
    private class ObserverHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            Log.d(TAG,"handleMessage msg = " + msg);
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_client);

        mContext = this;
        //实例化ContentObserver 
        mHandler = new ObserverHandler();
        mContentObserver = new ContentObserver(mHandler) {
            @Override
            public void onChange(boolean selfChange, Uri uri) {
                super.onChange(selfChange, uri);
                Log.d(TAG,"onChange selfChange = " + selfChange + " , uri = " + uri.toString());
              mHandler.obtainMessage(CONTENT_PROVIDER_CHANGED).sendToTarget();
            }
        };
         //注册监听    
         mContext.getContentResolver().registerContentObserver(STUDENT_URI,true,mContentObserver);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        //注销监听
        if(mContentObserver != null) {
            mContext.getContentResolver().unregisterContentObserver(mContentObserver);
        }
    }

在使用完记得一定要注销ContentObserver以免引起内存泄漏。

小结

本文基于Android 8.0对ContentProvider使用过程中涉及到的一些知识点进行了简单的介绍和整理,归纳总结了ContentProvider的使用步骤,并通过一个示例详细的说明了每个步骤具体都做些什么。

Demo

参考文献

ContentProvider从入门到精通
Android 进阶11:进程通信之 ContentProvider 内容提供者
Android:关于ContentProvider的知识都在这里了!
Android SQLite数据库的详细使用
内容提供者ContentProvider的基本使用
ContentProvider数据库共享之——读写权限与数据监听
ContentObserver

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容