四大组件之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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Vinson武

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值