四大组件之ContentProvider详解

相关定义

  1. ContentProvider是Android提供给上层的一个组件,主要用于实现数据访问的统一管理和数据共享。这里的数据管理是通过定义统一的访问接口来完成,如增删改查。同时,它采用了类似Internet的URL机制,将数据以URI的形式来标识,这样其他App就可以采用一套标准的URI规范来访问同一处数据,而不用关心具体的实现细节。

  2. ContentProvider有助于应用管理其自身和其他应用所存储数据的访问,并提供与其他应用共享数据的方法。它们会封装数据,并提供用于定义数据安全性的机制。其底层使用了binder来完成App进程之间的通信,同时使用匿名共享内存来作为共享数据的载体。


    image.png
  3. 优点:

  • ContentProvider可精细控制数据访问权限。可以选择仅在应用内限制对内容提供程序的访问,授予访问其他应用数据的权限,或配置读取和写入数据的不同权限。
  • 可以使用ContentProvider将细节抽象化,以用于访问应用中的不同数据源。例如,可能会在 SQLite 数据库中存储结构化记录,以及视频和音频文件。
  1. ContentProvider以一个或多个表的形式将数据呈现给外部应用,这些表与在关系型数据库中找到的表类似。行表示提供程序收集的某种数据类型实例,行中的每个列表示为实例所收集的每条数据。


    image.png

如何共享数据

ContentProvider通过uri来标识其它应用要访问的数据,通过ContentResolver的增、删、改、查方法实现对共享数据的操作。还可以通过注册ContentObserver来监听数据是否发生了变化来对应的刷新页面。

//ContentProvider是一个抽象类,如果我们需要开发自己的内容提供者我们就需要继承这个类并复写其方法,需要实现的主要方法如下:
public boolean onCreate()
//在创建ContentProvider时使用
public Cursor query()
//用于查询指定uri的数据返回一个Cursor
public Uri insert()
//用于向指定uri的ContentProvider中添加数据
public int delete()
//用于删除指定uri的数据
public int update()
//用户更新指定uri的数据
public String getType()
//用于返回指定的Uri中的数据MIME类型

数据访问的方法insert,delete和update可能被多个线程同时调用,此时必须是线程安全的。Android 系统会在启动提供程序时调用 onCreate()。在此方法中,应该只执行快速运行的初始化任务,并将数据库创建和数据加载推迟到提供程序实际收到数据请求时进行。如果您在 onCreate() 中执行冗长的任务,则会减慢提供程序的启动速度。

其它应用可以通过ContentResolver来访问ContentProvider提供的数据,而ContentResolver通过uri来定位自己要访问的数据,所以我们要先了解uri。
URI的格式如下:

[scheme:][//host:port][path][?query]

例如
URI:http://www.baidu.com:8080/wenku/jiatiao.html?id=123456&name=jack
自定义ContentProvider
public class StudentContentProvider extends ContentProvider {

    //这里的AUTHORITY就是我们在AndroidManifest.xml中配置的authorities
    private static final String AUTHORITY = "com.jrmf360.studentProvider";

    //匹配成功后的匹配码
    private static final int MATCH_CODE = 100;

    private static UriMatcher uriMatcher;

    private StudentDao studentDao;

    //数据改变后指定通知的Uri
    private static final Uri NOTIFY_URI = Uri.parse("content://" + AUTHORITY + "/student");

    static {
        //匹配不成功返回NO_MATCH(-1)
        uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);

        //添加我们需要匹配的uri
        uriMatcher.addURI(AUTHORITY,"student", MATCH_CODE);
    }


    @Override
    public boolean onCreate() {
        studentDao = StudentDao.getInstance(getContext());
        return false;
    }

    @Nullable
    @Override
    public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection,
                        @Nullable String[] selectionArgs, @Nullable String sortOrder) {
        int match = uriMatcher.match(uri);
        if (match == MATCH_CODE){ //判断uri是否匹配
            Cursor cursor = studentDao.queryStudent(); //数据库查询
            return cursor;
        }
        return null;
    }

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

    @Nullable
    @Override
    public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
        if (uriMatcher.match(uri) == MATCH_CODE){
            studentDao.insertStudent(values);
            notifyChange();
        }
        return null;
    }

    @Override
    public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
        if (uriMatcher.match(uri) == MATCH_CODE){
            int delCount = studentDao.deleteStudent();
            notifyChange();
            return delCount;
        }
        return 0;
    }

    @Override
    public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection,
                      @Nullable String[] selectionArgs) {
        return 0;
    }

//在该方法中我们通知ContentObserver那个uri数据发生了变化,以便及时更新页面。
    private void notifyChange(){
        getContext().getContentResolver().notifyChange(NOTIFY_URI,null);
    }
}

在xml注释

<provider
        android:authorities="com.jrmf360.studentProvider"
        android:name=".StudentContentProvider"
        android:exported="true"/>

这里的authorities唯一标识该内容提供者,这样其它的应用才可以找到该内容提供者并操作它的数据;exported为true当前内容提供者可以被其它应用使用,默认为true。

访问已有ContentProvider

获得ContentResolver

contentResolver = getContentResolver();

注册ContentObserver来监听对应uri的数据变化,这步不是必须的,如果不需要监听数据变化也可以不注册

private static final String AUTHORITY = "com.jrmf360.studentProvider";
private static final Uri STUDENT_URI = Uri.parse("content://" + AUTHORITY + "/student");

contentResolver.registerContentObserver(STUDENT_URI, true, new MyContentObserver(handler));

内容变化监听者

public class MyContentObserver extends ContentObserver {

    Handler mHandler;
    public MyContentObserver(Handler handler) {
        super(handler);
        mHandler = handler;
    }

    @Override
    public void onChange(boolean selfChange) {
        super.onChange(selfChange);
    }

    @Override
    public void onChange(boolean selfChange, Uri uri) {
        super.onChange(selfChange, uri);
        Message message = Message.obtain();
        message.obj = uri;
        mHandler.sendMessage(message);
    }
}

自定义ContentObserver类,必须实现其构造函数,在构造函数中我们需要传递一个Handler,到此我们可以明白,ContentObserver在收到数据变化的通知后通过Handler机制来通知主线程更新UI。

@Override
public void onClick(View v) {
    int id = v.getId();
    if (id == R.id.btn_query) {
        Cursor cursor = contentResolver.query(STUDENT_URI, null, null, null, null, null);
        if (cursor != null && cursor.getCount() > 0) {
            while (cursor.moveToNext()) {
                String name = cursor.getString(cursor.getColumnIndex("name"));
                int age = cursor.getInt(cursor.getColumnIndex("age"));
                String desc = cursor.getString(cursor.getColumnIndex("desc"));
                Log.e(getClass().getSimpleName(), "Student:" + "name = " + name + ",age = " + age + ",desc = " + desc);
            }
            cursor.close();
        }
    } else if (id == R.id.btn_insert) {
        ContentValues contentValues = new ContentValues();
        contentValues.put("name", "新插入");
        contentValues.put("age", "-100");
        contentValues.put("desc", "我是新插入的呦。。。");
        contentResolver.insert(STUDENT_URI, contentValues);
    } else if (id == R.id.btn_del) {
        String selectionClause = "name LIKE ?";
        String[] selectionArgs = {"林%峰"};
        int rowsDeleted = getContentResolver().delete(
            STUDENT_URI,   
            selectionClause,                   
            selectionArgs                     
        );
    }else{
        ContentValues updateValues = new ContentValues();

        String selectionClause = "name LIKE ?";
        String[] selectionArgs = {"林%"};
        
        // Defines a variable to contain the number of updated rows
        int rowsUpdated = 0;

        updateValues.putNull("name"); //置空
        
        rowsUpdated = getContentResolver().update(
            UserDictionary.Words.CONTENT_URI,   
            updateValues,                      // the columns to update
            selectionClause,                   // the column to select on
            selectionArgs                      // the value to compare to
        );
            }
        }
防止恶意输入

如果内容提供程序管理的数据位于SQL数据库,则在原始 SQL 语句中加入不受信任的外部数据可能会导致 SQL 注入。

String selectionClause = "var = " + userInput;

若执行此操作,则会允许用户将恶意 SQL 连接至 SQL 语句。例如,用户可以为 mUserInput 输入“nothing; DROP TABLE *;”,从而生成选择子句 var = nothing; DROP TABLE *;。由于选择子句是作为 SQL 语句处理的,因此这可能会导致提供程序清空基础 SQLite 数据库中的所有表(除非提供程序设置为可捕获 SQL 注入尝试)。

如要避免此问题,请使用将 ? 作为可替换参数的选择子句,以及单独的选择参数数组。执行此操作时,用户输入直接受查询约束,而不解释为 SQL 语句的一部分。由于用户输入未作为 SQL 处理,因此其无法注入恶意 SQL。请使用以下选择子句(而非连接)加入用户输入:

String selectionClause =  "var = ?";

//选择参数数组
selectionArgs[0] = userInput;

ContentProvider启动过程

ContentProvider 是一种内容共享型组件,其主要职责是向其他组件以及其他应用提供数据。当 ContentProvider 所在的进程启动时,ContentProvider 会同时启动并被发布到 AMS 中
注意:ContentProvider 的 onCreate 要先于 Application 的 onCreate 执行

  1. 当一个应用启动时,==入口方法是ActivityThread的main方法==,其中创建ActivityThread的实例并创建主线程的消息队列;
  2. ==ActivityThread的attach方法中会远程调用AMS的attachApplication==,并将ApplicationThread提供给AMS,ApplicationThread主要用于ActivityThread和AMS之间的通信;
  3. ==AMS的attachApplication会调用ApplicationThread的bindApplication方法,这个方法会通过H切换到ActivityThread中去执行,即调用handleBindApplication方法==;
  4. ==handleBindApplication方法会创建Application对象并加载ContentProvider==,注意是先加载ContentProvider,然后调用Application的onCreate方法。

==(Content Provider会存储在AMS中的mProviderMap中,这个mProviderMap起到缓存的作用,防止每次使用相同的Content Provider时都调用AMS的getContentProvider方法)==。

ContentProvider工作原理

  • ==在使用 ContentProvider 时,我们会通过 ContentResolver 来进行操作(URI)
    Context 对象的 getContenResolver 方法,返回一个 ApplicationContentResolver 类对象==,该类继承自 ContentResolver ,在 ContentImpl 构造方法中通 ApplicationContentResolver 类的构造方法初始化 ApplicationContentResolver 对象。
  • ==以 query 方法为例,ApplicationContentResolver 的 query 方法,会调用 ContentResolver 的 query 方法==,query 方法中调用 acquireProvider 方法来获取 IContentProvider 对象,IContentnProvider 继承了 IInterface 接口,ApplicationContentResolver 在 ContentResolver 中是抽象方法,==最终会调用 ApplicationContentResolver 的 acquireProvider 方法,在调用 query 方法时会将 Uri 构造成 String 并以参数的形式传递
    acquireProvider 方法中==,==会调用当前应用 ActivityThread 的 acquireProvider 方法来获取能处理当前 Uri 的 IContentProvider 对象==,==acquireProvider 方法中,会根据要操作的 Uri 来查找是否当前发布的 ContentProvider 中是否有对应的,如果有则直接返回,如果没有则通过进程间通信通知 AMS 的 getContentProvider 方法启动需要的 ContentProvider 所在的进程==,进程启动之后 ContentProvider 也会发布,得到对应的 ContentProvider 之后 AMS 会将其返回,acquireProvider 方法中也将得到的 IContentProvider 返回到 ApplicationContentResolver 的 query 方法中。
    ==ApplicationContentResolver 的 acquireProvider 方法中得到 IContentProvider 之后,通过 AMS 来访问 ContentProvider== ,这里的 IContentProvider 是 AIDL 中的 Proxy,通过 IPC 调用 ContentProvider.Transport 对象的方法,Transport 的 query 方法,调用我们自定义的 ContentProvider 的 query 方法,并将结果返回到 acquireProvider 方法,在返回到客户端调用的 query 方法,完成工作。

参考链:
https://www.jianshu.com/p/5e13d1fec9c9

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

推荐阅读更多精彩内容