Android - 内容提供者 ContentProvider

本文介绍了Android中ContentProvider组件,它是不同APP间数据共享的组件,为存储和读取数据提供统一接口。详细阐述了ContentProvider提供数据的方式,包括在Manifest中注册、使用Uri等,还介绍了ContentResolver获取数据以及ContentObserver监听数据变化的方法。

官方页面

一、概念

不同APP间进行数据共享的组件。ContentProvider 为存储和读取数据提供了统一的接口,其它APP通过 ContentResolver 来访问 ContentProvider 提供的数据,而 ContentResolver 通过 URI 来定位自己要访问的数据。

简化访问屏蔽细节提供统一访问接口,只需要利用 URI 来访问,简化了应用间数据共享的操作(多为数据库)。
安全访问数据很重要,提供给其它APP访问(外部访问)但不能随便操作,暴露的是自己定义好的操作方式。
运行在主线程

ContentProvider的 onCreate() 运行在主线程,而 query()、insert()、delete()、update()是运行在子线程中,调用这些函数并不会阻塞 ContentProvider 所在进程的主线程,但是可能会阻塞调用者所在进程的主线程(系统让调用线程等待异步操作完成),因此需要在子线程中访问ContentProvider。

1.1 统一资源标识符 Uri

通过 URI 提供唯一标识供外部访问自己,协议(scheme)固定为 content。详见

//转换成URI,编译器推荐使用KTX方式
//val uri = Uri.parse("content://com.example.yy/book")
val uri = "content://com.example.yy/book".toUri()

1.2 UriMatcher 工具类

先用来注册自定义的 URI,就可以用来查询外部传入的 URI 是否能处理(是不是自己注册过的)。

addURI()

注册Uri

public void addURI(String authority, String path, int code)

将自己定义的 URI 进行注册。其中参数authority与path组成一个URI,参数code是自定义的匹配码,用来区分不同的操作。

match()

查询Uri是否注册过

public int match(Uri uri)

对外部传入的 URI 进行匹配查询,确保传来的 URI 是注册过的,是该 ContentProvider 可以处理的。返回的是注册该 URI 时定义的匹配码,通过对匹配码的判断作出对应的处理,如果查找不到对应的 URI 会返回 -1。

val AUTHORITY = "com.jomurphys.myapp"    //xml中定义的那个
val PATH = "table"    //
val CODE_A = 1    //不同的 code 用来区分不同的操作
val CODE_B = 2

//初始化传入的常量 NO_MATCH 是匹配失败返回的CODE(值为-1),意味着不匹配任何路径的返回码
val matcher = UriMatcher(UriMatcher.NO_MATCH)
//注册需要的URI
matcher.addURI(AUTHORITY, PATH, CODE_A)
matcher.addURI(AUTHORITY, PATH, CODE_B)
//查询外部传来的URI进行不同的操作
val code = matcher.match(uri)
when (code) {
    CODE_A ->
    CODE_B ->
    else -> 
}

1.3 ContentUris 工具类

用来操作 URI 的 ID 部分。

withAppendedId( )

public static Uri withAppendedId(Uri contentUri, long id)

添加ID

removeId( )

public static Uri removeId(Uri contentUri)

移除ID

parseId( )

public static long parseId(Uri contentUri)

获取ID

val oldUri1 = "www.jomurphys.myapp:8080/23/44".toUri()
val oldUri2 = "www.jomurphys.myapp8080/23/44".toUri()    //去掉冒号

val newUri1 = ContentUris.withAppendedId(uri1, 123)    //打印:www.jomurphys.myapp:/123
val newUri2 = ContentUris.withAppendedId(uri2, 123)    //打印:www.jomurphys.myapp8080/23/44/123
val newUri3 = ContentUris.removeId(uri1)    //报错
val newUri4 = ContentUris.removeId(uri2)    //打印:/www.jomurphys.myapp8080/23

1.4 ContentValues

用于将数据封装成符合 SQLite 数据库要求的格式。其实就是一个 Map,key只能是 String 类型,value只能是基本数据类型,不能存储自定义对象。

//创建方式一
val values1 = contentValuesOf(
    "a" to 1,
    "b" to 2
)
//创建方式二
val values2 = ContentValues().apply {
    put("a", 1)
    put("b", 2)
}
//获取元素,获取不到返回null
val element1 = values1.get("a")    //类型Any
val element2 = values1.getAsInteger("b")  //类型Integer

二、ContentProvider 提供数据

2.1 Manifest 中注册

四大组件都需要在Manifest中注册。系统提供的 ContentProvider 位于 android.provider 包中。

<provider
    android:authorities="com.jomurphys.myapp"    //Uri标识
    android:name=".MyContentProvide"    //指定实现类的名称
    android:exported="true"    //是否对外暴露,为true其他APP就能访问到
/>

2.2 自定义ContentProvider

重写的函数其实就是实现自定义的CRUD操作,在必要的时候通知注册该 URI 的监听者共享数据发生了变化。

初始化

public abstract boolean onCreate()

外部第一次访问就会调用,用来初始化对外提供的数据(运行在主线程不要做耗时操作)

public abstract @Nullable Uri insert(

        @NonNull Uri uri,

        @Nullable ContentValues values

)

返回新条目的 URI 或 null。

public abstract int delete(

        @NonNull Uri uri,

        @Nullable String selection,
        @Nullable String[] selectionArgs

);

返回受影响的行数。

public abstract int update(

        @NonNull Uri uri,

        @Nullable ContentValues values,
        @Nullable String selection,

        @Nullable String[] selectionArgs

);

返回受影响的行数。

public abstract @Nullable Cursor query(

        @NonNull Uri uri,        //要查询的 ContentProvider 的 URI。

        @Nullable String[] projection,        //要查询的字段(列Column),用 null 表示返回所有字段内容。
        @Nullable String selection,        //查询条件,相当于SQL语句中的where,用 null 表示不进行筛选。

        @Nullable String[] selectionArgs,        //如果 selection 里有?符号这里可以以实际值代替。没有的话可以为null。
        @Nullable String sortOrder        //对结果进行排序,相当于SQL语句中的Order by,升序 asc /降序 desc,null为默认排序。

);

返回游标或null。

数据类型

public abstract String getType(Uri uri)

返回当前 URI 对应数据的 MIME 类型或 null。详见

class MyContentProvider : ContentProvider() {

    //定义匹配码
    private val MATCH_CODE_A = 1
    private val MATCH_CODE_B = 2
    //定义Uri各部分
    private val AUTHORITY = "com.jomurphys.myapp"    //在xml中注册的那个
    private val PATH = "table"
    //初始化并配置UriMatcher
    private val uriMacher by lazy {
        //传入的常量 NO_MATCH 是匹配失败返回的CODE(值为-1),意味着不匹配任何路径的返回码
        UriMatcher(UriMatcher.NO_MATCH).apply {
            //添加我们需要注册的Uri,#为通配符
            addURI(AUTHORITY, PATH, MATCH_CODE_A)
            addURI(AUTHORITY, PATH + "/#", MATCH_CODE_B)
        }
    }
    //数据更改后用来通知的
    private val NOTIFY_URI = "content://$AUTHORITY$PATH/自己提供的数据库表名".toUri()

    //外部第一次访问就会调用,用来初始化对外提供的数据(数据库)
    override fun onCreate(): Boolean {
        val db = DemoDatabase.getInstance(getContext())
        val dao = db.demoDao()
    }

    override fun query(
        uri: Uri, projection: Array<out String?>?,
        selection: String?,
        selectionArgs: Array<out String?>?,
        sortOrder: String?
    ): Cursor? {
        //根据传入的Uri获取匹配的CODE
        val code = uriMacher.match(uri)
        //判断匹配的是哪个CODE,并做相应查询
        //返回数据库的 Cursor 或 null
        return when(code) {
            MATCH_CODE_A -> null
            MATCH_CODE_B -> null
            else -> null    //都不匹配返回null
        }
    }

    override fun getType(uri: Uri): String? {
        val code = uriMacher.match(uri)
        //返回该 URI 对应数据的 MIME 类型
        return when(code) {
            MATCH_CODE_A -> "audio/mp3"
            MATCH_CODE_B -> "image/*"
            else -> null    //都不匹配返回null
        }
    }

    override fun insert(uri: Uri, values: ContentValues?): Uri? {
        val code = uriMacher.match(uri)
        //返回新条目的 URI 或 null
        return when(code) {
            MATCH_CODE_A -> {
                //通知所用注册该URI的观察者,该URI共享的数据发生了变化
                context?.contentResolver?.notifyChange(NOTIFY_URI, null)    //观察者这里用null代替了
                null
            }
            MATCH_CODE_B -> null
            else -> null    //都不匹配返回null
        }
    }

    override fun delete(
        uri: Uri,
        selection: String?,
        selectionArgs: Array<out String?>?
    ): Int {
        val code = uriMacher.match(uri)
        //返回受影响的行数
        return when(code) {
            MATCH_CODE_A -> 0
            MATCH_CODE_B -> 0
            else -> 0
        }
    }

    override fun update(
        uri: Uri,
        values: ContentValues?,
        selection: String?,
        selectionArgs: Array<out String?>?
    ): Int {
        val code = uriMacher.match(uri)
        //返回受影响的行数
        return when(code) {
            MATCH_CODE_A -> 0
            MATCH_CODE_B -> 0
            else -> 0
        }
    }
}

四、ContentResolver 获取数据

4.1 调用 ContentProvider

外部进程使用 ContentResolver 通过 URI 即可访问不同的 ContentProvider 中的数据。提供了与 ContentProvider 中同名的 CRUD 四个方法,

public Uri insert(Uri uri, ContentValues values)
public int delete(Uri uri, String selection, String[] selectionArgs)
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs)
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)
//可通过在所有继承Context的类中 通过调用getContentResolver()来获得
val contentResolver = contentResolver
val uri = Uri.parse("content://com.jomurphys/myapp:8080/demo")

//查
val cursor = contentResolver.query(uri, null, null, null)
if (cursor != null && cursor.count > 0) {
    while (cursor.moveToNext()) {

    }
    cursor.close()
}
//增
val contentValues = ContentValues().apply {
    put("name", "Zhangsan")
    put("age", 18)
}
contentResolver.insert(uri, contentValues)
//删
contentResolver.delete(uri, null, null)

4.2 处理文件 Uri

很多 API 会返回文件的 Uri,可以转成流进行处理。

getType()

public final @Nullable String getType(@NonNull Uri url) 

判断 Uri 文件的类型(如 image/jpeg),不是文件返回 null。

openInputStream()

public final @Nullable InputStream openInputStream(@NonNull Uri uri) throws FileNotFoundException

通过 Uri 打开输入流。

openOutputStream()

public final @Nullable OutputStream openOutputStream(@NonNull Uri uri) throws FileNotFoundException

通过 Uri 打开输出流。

五、ContentObserver 监听数据变化

数据发生变化后,通知外部访问者。

 5.1 自定义内容观察者

//自定义内容观察者
class MyObserve(val handler: Handler) : ContentObserver(handler) {
    override fun onChange(selfChange: Boolean, uri: Uri?) {
        super.onChange(selfChange, uri)
        //更新界面
        val message = Message.obtain()
        message.obj = uri
        handler.sendMessage(message)
    }
}

5.2 在 ContentProvider 中发出提醒 

class MyContentProvider : ContentProvider() {
    override fun insert(uri: Uri, values: ContentValues?): Uri? {
        //数据发生变化后,通知外部访问者
        context?.contentResolver?.notifyChange(uri, null)    //观察者这里用null代替了
        return null
    }
}

5.3 注册解绑监听

val handler = Handler(mainLooper) { false }
val myObserve = MyObserve(handler)

val contentResolver = contentResolver
val uri = Uri.parse("content://com.jomurphys.myapp/demo")

//注册监听
contentResolver.notifyChange(uri, myObserve)
//记得在生命周期中注销监听
contentResolver.unregisterContentObserver(myObserve)

六、补充

访问 assert 资源目录下的数据库 

用代码将数据库复制到/data/data/packagename/databases/目录下就直接能访问了。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值