Android视图手册之ContentProvider

第五篇 Android视图手册之ContentProvider

作为Android四大组件之一的ContentProvider,虽然在平常使用的过程中亮相并不多,但这不影响其在Android系统中的地位。因为平时我们的应用数据都是内部自我消化和独立的,一旦涉及和其他应用进行数据交互时,ContentProvider的作用就体现出来了。

概述

ContentProvider(内容提供者),作用是为不同的应用之间数据共享,提供统一的接口,通过uri来标识其它应用要访问的数据,通过ContentResolver的增、删、改、查方法实现对共享数据的操作。还可以通过注册ContentObserver来监听数据是否发生了变化来对应的刷新页面。

其中ContentProvider只是承担了一个中间工作者的角色,内部的数据源的操作由提供的应用开发者自己实现,例如Sqlite,文件,XMl,网络等等。
在这里插入图片描述

统一资源标识符(URI)

在了解ContentProvider之前,我们先来简单认知一下什么是统一资源标识符(URI)。
URI本质上是一个字符串,这个字符串的作用是唯一地标记资源的位置或者名字。它不仅能够标记万维网的资源,也可以标记其他的,如邮件系统,本地文件系统等任意资源,而资源既可以是存在磁盘上的静态文本,页面数据也可以是由java,php提供的动态服务。
一个正常的URI标识如下图所示,往往由这四部分组成:
在这里插入图片描述
而在Android中 我们如果想用ContentProvider进行数据共享,就需要按下述规则进行:

  • 协议名:要用content作为前缀(这是Android规定的)
  • 主机名:要用对应ContentProvider在注册清单中的Authority值(在清单文件中配置)
  • 路径名:这里需填入资源路径,在你创建ContentProvider时可以进行相应资源路径的加入
  • 查询参数:这个参数可以不进行填入,因为查询参数ContentProvider已经提供方式给我们去调用了,当然你如果想进行填入的话也可以,自己拿到解析后的值进行对应逻辑处理就行。

示例: content://com.ftd.test.myprovider/test

使用步骤

大致步骤如下:

  1. 自定义 ContentProvider
  2. 在清单文件AndroidManifest中进行注册
  3. 进行数据访问 进程内 | 进程间
ContentProvider自定义

首先我们先需要继承ContentProvider,实现其中的方法,如下:

package com.ftd.test;

import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

/**
 * 内容提供者
 * tangxianfeng
 * 2022.10.29
 */
public class MyProvider extends ContentProvider {

    /**
     * 进行创建
     */
    @Override
    public boolean onCreate() {
        return false;
    }
    /**
     * 获取数据类型
     */
    @Nullable
    @Override
    public String getType(@NonNull Uri uri) {
        return null;
    }
    /**
     * 查询数据
     */
    @Nullable
    @Override
    public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
        return null;
    }
    /**
     * 插入数据
     */
    @Nullable
    @Override
    public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
        return null;
    }
    /**
     * 删除数据
     */
    @Override
    public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
        return 0;
    }
    /**
     * 更新数据
     */
    @Override
    public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
        return 0;
    }
}
各个方法的作用
  • onCreate()
    ContentProvider 创建的时候会进行调用的方法,一般可用于数据库等需要初始化的操作。
  • getType(Uri)
    返回MIME类型对应内容的URI,这里的getType一般用做类型的自定义返回,当我们某些情况传入的类型是需要校验的时候,可以通过重写该方法实现,一般可不做处理。
  • query(Uri, String[], String, String[], String)
    查询数据,返回一个数据Cursor对象。
  • insert(Uri, ContentValues)
    插入一条数据
  • delete(Uri, String, String[])
    根据条件删除数据
  • update(Uri, ContentValues, String, String[])
    根据条件更新数据

如下为我用游标进行简单模拟的一些操作,主要涉及查询和插入,实际情况中我们可以用sqlite等数据库进行替代。

package com.ftd.test;

import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.Context;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

/**
 * 内容提供者
 * tangxianfeng
 * 2022.10.29
 */
public class MyProvider extends ContentProvider {
    private static final String AUTHORITY = "com.ftd.test.myprovider"; //用于区分不同的ContentProvider

    //UriMatch主要为了区配URI,比如应用A提供了数据,但是并不是说有的应用都可以操作这些数据,只有提供了满足应用A的URI,才能访问A的数据,而UriMatch就是做这个区配工作的
    private static final UriMatcher sUriMatcher;
    //用以表明数据类型
    public static final int USER_DIR = 0;
    public static final int USER_ITEM = 1;

    private MatrixCursor TestCursor;
    static {
        sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
        //只对这三种uri进行处理 如果匹配上 通过 sUriMatcher.match(uri)可以得到 对应的Code
        sUriMatcher.addURI(AUTHORITY, "test", USER_ITEM);
        sUriMatcher.addURI(AUTHORITY, "test1", USER_ITEM);
        sUriMatcher.addURI(AUTHORITY, "testmulti", USER_DIR);
    }

    /**
     * 进行创建
     */
    @Override
    public boolean onCreate() {
        //这里可以创建数据库,也可以创建其他类型的数据,如下为我用游标创建的一个表
        String[] tableCursor = new String[] { "time", "food", "where" };
        TestCursor = new MatrixCursor(tableCursor);
        TestCursor.addRow(new Object[] { "2022", "烤鸭", "北京" });
        TestCursor.addRow(new Object[] { "2021", "肉夹馍", "陕西" });
        TestCursor.addRow(new Object[] { "2020", "大饼", "山东" });
        return true;
    }

    /**
     * 获取数据类型
     */
    @Nullable
    @Override
    public String getType(@NonNull Uri uri) {
        if(sUriMatcher.match(uri)==USER_DIR) {
            // 查询多条数据
            return "vnd.android.cursor.dir/testmulti";
        }else{
            // 查询一条数据
            return "vnd.android.cursor.item/testsingle";
        }
    }

    /**
     * 查询数据
     * uri 代表资源位置的uri
     * projection 代表返回内容(conlumn名)
     * selection 设置条件
     * selectionArgs是selection的内容
     * sortOrder是根据column排序
     */
    @Nullable
    @Override
    public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
        switch (sUriMatcher.match(uri)) {
            case USER_DIR:
               return TestCursor;
            case USER_ITEM:
                //条件查询等情况处理
                break;
        }
        return null;
    }
    /**
     * 插入数据
     */
    @Nullable
    @Override
    public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
        if (values==null){
            return null;
        }
        switch (sUriMatcher.match(uri)) {
            case USER_DIR:
                break;
            case USER_ITEM:
                String time = (String) values.get("time");
                String food = (String) values.get("food");
                String where = (String) values.get("where");
                TestCursor.addRow(new Object[] { time, food, where });
                getContext().getContentResolver().notifyChange(uri, null);//通知指定URI数据已改变
                break;
            default:
        }
        return null;
    }

    /**
     * 删除数据
     * selection 设置条件
     * selectionArgs是selection的内容
     */
    @Override
    public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
        return 0;//此处就不赘述了,拿到条件后根据条件对数据进行删除即可
    }
    /**
     * 更新数据
     * selection 设置条件
     * selectionArgs是selection的内容
     */
    @Override
    public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
        return 0;//此处就不赘述了,拿到条件后根据条件对数据进行更新即可
    }
}

在清单文件中进行注册
	<provider
            android:authorities="com.ftd.test.myprovider"
            android:name="com.ftd.test.MyProvider"
            android:exported="true"
            android:grantUriPermissions="true"
            android:permission="ftd.Provider"
            android:readPermission="ftd.Provider"
            android:writePermission="ftd.Provider"
            android:enabled="true"
            android:multiprocess="true"/>

前文提到的android:authorities 就是在这里设置的,用作唯一标识。
ContentProvider而言,有很多权限控制,可以AndroidManifest.xml文件中对节点的属性进行配置,一般使用如下一些属性设置:

  • android:grantUriPermssions:临时许可标志。
  • android:permission:Provider读写权限。
  • android:readPermission:Provider的读权限。
  • android:writePermission:Provider的写权限。
  • android:enabled:标记允许系统启动Provider。
  • android:exported:标记允许其他应用程序使用这个Provider。
  • android:multiProcess:标记允许系统启动Provider相同的进程中调用客户端。
数据交互操作

在进行数据交互操作时,我们可以通过ContentResolver统一对数据进行管理和操做:

  • 查询
        // 获取ContentResolver
        ContentResolver resolver =  getContentResolver();
        // 通过ContentResolver 向ContentProvider中查询数据
        Cursor cursor = resolver.query( Uri.parse("content://com.ftd.test.myprovider/testmulti"), null, null, null, null);
        while (cursor.moveToNext()){
            System.out.println("query data:" + cursor.getString(0) +" "+ cursor.getString(1)+"   "+ cursor.getString(2));
            // 将表中数据全部输出
        }
        cursor.close();
        // 关闭游标
  • 插入
       // 获取ContentResolver
        ContentResolver resolver =  getContentResolver();

        // 插入表中数据
        ContentValues values = new ContentValues();
        values.put("time", "2002");
        values.put("food", "瘦肉丸");
        values.put("where", "温州");

        // 通过ContentResolver 根据URI 向ContentProvider中插入数据
        resolver.insert(Uri.parse("content://com.ftd.test.myprovider/test"),values);

  • 删除
// 获取ContentResolver
ContentResolver resolver =  getContentResolver();

 // 删除表中数据
resolver.delete(Uri.parse("content://com.ftd.test.myprovider/test"),"time=",new String[]{"2022"});
        
  • 更新
        // 获取ContentResolver
        ContentResolver resolver =  getContentResolver();

        // 更新表中数据
        ContentValues values = new ContentValues();
        values.put("time", "2002");
        values.put("food", "瘦肉丸");
        values.put("where", "温州");

        resolver.update(Uri.parse("content://com.ftd.test.myprovider/test"),values,,"time=",new String[]{"2022"});

到此一个ContentProvider的大致使用操作就结束了。

小知识

1、MIME类型
MIME:全称Multipurpose Internet Mail Extensions,多功能Internet邮件扩充服务。它是一种多用途网际邮件扩充协议,在1992年最早应用于电子邮件系统,但后来也应用到浏览器。MIME类型就是设定某种扩展名的文件用一种应用程序来打开的方式类型,当该扩展名文件被访问的时候,浏览器会自动使用指定应用程序来打开。多用于指定一些客户端自定义的文件名,以及一些媒体文件打开方式。
简单来说,MIME类型就是用来标识当前的Activity所能打开的文件类型
举个栗子:

<activity 
   android:name=".TESTActivity" 
   android:label="@string/test"> 
   <intent-filter> 
       <action android:name="com.ftd.test"/> 
       <categoryandroid:name="android.intent.category.DEFAULT"/> 
       <data android:mimeType="image/bmp"/> 
   </intent-filter> 
</activity> 

这里指定了data域的MimeType值是"image/bmp",即在利用隐式Intent匹配时,只有指定MimeType是"image/bmp"时,才能启用这个Activity,也就是说,这个Activity只能打开image/bmp类型的文件。
典型的调用方式如下:

Intent intent =new Intent();
intent.setAction(Intent.ACTION_DIAL);
intent.setData(Uri.parse("tel:10086"));
startActivity(intent);

Intent.ACTION_DIAL这个activity会通过tel:10086获取自己用的通讯录的contentprovider的type. 如果与intentfilter中指定的mimeType一致, 那么证明传过来的这个参数uri是可用的, 那么就打开拨号这个界面。

而在getType中,我们可以看到getType这个函数会根据传进来的URI,相应生成一个代表MimeType的字符串;而此字符串的生成也有规则:

  • 如果是单条记录应该返回以vnd.android.cursor.item/ 为首的字符串
  • 如果是多条记录,应该返回vnd.android.cursor.dir/ 为首的字符串

MIME前面的一部分我们按照Google的要求来写,后面一部分就可以根据我们自己的实际需要来写。

2、ontentProvider的底层是采用 Android中的Binder机制来实现的,如果想具体研究该原理,可先去了解Binder机制

可能遇到的相关问题

1、提供 ContentProvider的应用未唤醒能拿到数据吗
不能,Android中A应用向B应用去通过ContentProvider去获取数据时,是需要先唤醒B应用,在通过B的ContentProvider去获取资源数据。如果B的进程没有被唤醒,就拿不到数据了,此时后台会发现Log:ActivityThread: Failed to find providerinfo for xxx.
2、ContentProvider的getType的作用?

  • 隐式调用activity传入data数据. 且这个data数据, 是某个ContentProvider的uri参数
  • 为了防止data是activity无法处理, 所以activity才需要设置mime进行data校验
  • 为了acvity在校验自定义ContentProvider时能有结果, 所以才要实现ContentProvider的getType.
  • 如果当该类型需要涉及存储,读取等权限时,也可以在跳转activity前先进行判断申请。
    3、为什么要使用通过ContentResolver类从而与ContentProvider类进行交互,而不直接访问ContentProvider类?
    一般来说,一款应用要使用多个ContentProvider,若需要了解每个ContentProvider的不同实现从而再完成数据交互,操作成本高 & 难度大
    所以再ContentProvider类上加多了一个 ContentResolver类对所有的ContentProvider进行统一管理。
    4、ContentProvider的优点
    1)安全
    ContentProvider为应用间的数据交互提供了一个安全的环境:允许把自己的应用数据根据需求开放给 其他应用 进行 增、删、改、查,而不用担心因为直接开放数据库权限而带来的安全问题
    2)访问简单 & 高效
    对比于其他对外共享数据的方式,数据访问方式会因数据存储的方式而不同:
    采用 文件方式 对外共享数据,需要进行文件操作读写数据;
    采用 Sharedpreferences 共享数据,需要使用sharedpreferences API读写数据
    这使得访问数据变得复杂 & 难度大。
    而采用ContentProvider方式,其 解耦了 底层数据的存储方式,使得无论底层数据存储采用何种方式,外界对数据的访问方式都是统一的,这使得访问简单 & 高效
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值