文章目录
一、为什么要使用ContentProvider?
文件存储和SharedPreferences存储中提供了MODE_WORLD_READABLE和MODE_WORLD_WRITEABLE这两种操作模式,用于供给其他应用程序访问当前应用的数据,但 这两种模式在Android 4.2版本中都已被废弃了。为什么呢?因为Android官方已经不再推荐使 用这种方式来实现跨程序数据共享的功能,而是推荐使用更加安全可靠的ContentProvider技 术。
可能你会有些疑惑,为什么要将我们程序中的数据共享给其他程序呢?当然,这个是要视情况 而定的,比如账号和密码这样的隐私数据显然是不能共享给其他程序的,不过一些可以让其他 程序进行二次开发的数据是可以共享的。例如系统的通讯录程序,它的数据库中保存了很多联 系人信息,如果这些数据都不允许第三方程序进行访问的话,恐怕很多应用的功能就要大打折 扣了。除了通讯录之外,还有短信、媒体库等程序都实现了跨程序数据共享的功能,而使用的 技术当然就是ContentProvider了,下面我们就对这一技术进行深入的探讨。
ContentProvider主要用于在不同的应用程序之间实现数据共享的功能,它提供了一套完整的 机制,允许一个程序访问另一个程序中的数据,同时还能保证被访问数据的安全性。目前,使 用ContentProvider是Android实现跨程序共享数据的标准方式。
不同于文件存储和SharedPreferences存储中的两种全局可读写操作模式,ContentProvider 可以选择只对哪一部分数据进行共享,从而保证我们程序中的隐私数据不会有泄漏的风险。
二、使用现有的ContentProvider来读取和操作相应程序中的数据
1.初步介绍
如果一个应用程序通过ContentProvider对其数据提供了外部访问接口,那么任何其他的应用 程序都可以对这部分数据进行访问。Android系统中自带的通讯录、短信、媒体库等程序都提供 了类似的访问接口,这就使得第三方应用程序可以充分地利用这部分数据实现更好的功能。
2.ContentResolver的基本用法
对于每一个应用程序来说,如果想要访问ContentProvider中共享的数据,就一定要借助 ContentResolver类,可以通过Context中的getContentResolver()方法获取该类的实 例。ContentResolver中提供了一系列的方法用于对数据进行增删改查操作,其中insert() 方法用于添加数据,update()方法用于更新数据,delete()方法用于删除数据,query()方 法用于查询数据。有没有似曾相识的感觉?没错,SQLiteDatabase中也是使用这几个方法进 行增删改查操作的,只不过它们在方法参数上稍微有一些区别。
不同于SQLiteDatabase,ContentResolver中的增删改查方法都是不接收表名参数的,而是 使用一个Uri参数代替,这个参数被称为内容URI。内容URI给ContentProvider中的数据建立 了唯一标识符,它主要由两部分组成:authority和path。authority是用于对不同的应用程序 做区分的,一般为了避免冲突,会采用应用包名的方式进行命名。比如某个应用的包名是 com.example.app,那么该应用对应的authority就可以命名为 com.example.app.provider。path则是用于对同一应用程序中不同的表做区分的,通常会添 加到authority的后面。比如某个应用的数据库里存在两张表table1和table2,这时就可以将 path分别命名为/table1和/table2,然后把authority和path进行组合,内容URI就变成了 com.example.app.provider/table1和com.example.app.provider/table2。不过,目前还 很难辨认出这两个字符串就是两个内容URI,我们还需要在字符串的头部加上协议声明。因此, 内容URI最标准的格式如下:
content://com.example.app.provider/table1
content://com.example.app.provider/table2
有没有发现,内容URI可以非常清楚地表达我们想要访问哪个程序中哪张表里的数据。也正是因 此,ContentResolver中的增删改查方法才都接收Uri对象作为参数。如果使用表名的话,系 统将无法得知我们期望访问的是哪个应用程序里的表。
在得到了内容URI字符串之后,我们还需要将它解析成Uri对象才可以作为参数传入。解析的方 法也相当简单,代码如下所示:
val uri = Uri.parse("content://com.example.app.provider/table1")
只需要调用Uri.parse()方法,就可以将内容URI字符串解析成Uri对象了。
现在我们就可以使用这个Uri对象查询table1表中的数据了,代码如下所示:
val cursor = contentResolver.query(
uri,
projection,
selection,
selectionArgs,
sortOrder)
这些参数和SQLiteDatabase中query()方法里的参数很像,但总体来说要简单一些,毕竟这 是在访问其他程序中的数据,没必要构建过于复杂的查询语句。
query方法参数说明:
查询完成后返回的仍然是一个Cursor对象,这时我们就可以将数据从Cursor对象中逐个读取 出来了。读取的思路仍然是通过移动游标的位置遍历Cursor的所有行,然后取出每一行中相应 列的数据,代码如下所示:
while (cursor.moveToNext()) {
val column1 = cursor.getString(cursor.getColumnIndex("column1"))
val column2 = cursor.getInt(cursor.getColumnIndex("column2"))
}
cursor.close()
掌握了最难的查询操作,剩下的增加、修改、删除操作就更不在话下了。我们先来看看如何向 table1表中添加一条数据,代码如下所示:
val values = contentValuesOf("column1" to "text", "column2" to 1)
contentResolver.insert(uri, values)
可以看到,仍然是将待添加的数据组装到ContentValues中,然后调用ContentResolver的 insert()方法,将Uri和ContentValues作为参数传入即可。
如果我们想要更新这条新添加的数据,把column1的值清空,可以借助ContentResolver的 update()方法实现,代码如下所示:
val values = contentValuesOf("column1" to "")
contentResolver.update(uri, values, "column1 = ? and column2 = ?", arrayOf("text", "1"))
注意,上述代码使用了selection和selectionArgs参数来对想要更新的数据进行约束,以 防止所有的行都会受影响。
最后,可以调用ContentResolver的delete()方法将这条数据删除掉,代码如下所示:
contentResolver.delete(uri, "column2 = ?", arrayOf("1"))
使用现有的ContentProvider的例子有读取通讯录,于此不在赘述!
三、创建自己的ContentProvider
1.创建ContentProvider的步骤
如果想要实现跨程序共享数据的功能,可以通过新建一个类去继承 ContentProvider的方式来实现。ContentProvider类中有6个抽象方法,我们在使用子类继承 它的时候,需要将这6个方法全部重写。观察下面的代码示例:
class MyProvider : ContentProvider() {
override fun onCreate(): Boolean {
return false
}
override fun query(uri: Uri, projection: Array<String>?, selection: String?,
selectionArgs: Array<String>?, sortOrder: String?): Cursor? {
return null }
override fun insert(uri: Uri, values: ContentValues?): Uri? {
return null
}
override fun update(uri: Uri, values: ContentValues?, selection: String?,
selectionArgs: Array<String>?): Int {
return 0 }
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
return 0
}
override fun getType(uri: Uri): String? {
return null
} }
对于这6个方法,相信大多数你已经非常熟悉了,我再来简单介绍一下吧。
(1) onCreate()。初始化ContentProvider的时候调用。通常会在这里完成对数据库的创建和
升级等操作,返回true表示ContentProvider初始化成功,返回false则表示失败。
(2) query()。从ContentProvider中查询数据。uri参数用于确定查询哪张表,projection 参数用于确定查询哪些列,selection和selectionArgs参数用于约束查询哪些行, sortOrder参数用于对结果进行排序,查询的结果存放在Cursor对象中返回。
(3) insert()。向ContentProvider中添加一条数据。uri参数用于确定要添加到的表,待添 加的数据保存在values参数中。添加完成后,返回一个用于表示这条新记录的URI。
(4) update()。更新ContentProvider中已有的数据。uri参数用于确定更新哪一张表中的数 据,新数据保存在values参数中,selection和selectionArgs参数用于约束更新哪些行, 受影响的行数将作为返回值返回。
(5) delete()。从ContentProvider中删除数据。uri参数用于确定删除哪一张表中的数据, selection和selectionArgs参数用于约束删除哪些行,被删除的行数将作为返回值返回。
(6) getType()。根据传入的内容URI返回相应的MIME类型。
可以看到,很多方法里带有uri这个参数,这个参数也正是调用ContentResolver的增删改查 方法时传递过来的。而现在我们需要对传入的uri参数进行解析,从中分析出调用方期望访问的 表和数据。
回顾一下,一个标准的内容URI写法是:
content://com.example.app.provider/table1
接下来说说如何创建自己的ContentProvider并且构建MatriCursor来传输非数据库数据;
2.构建MatriCursor来传输非数据库数据
当需要共享之数据不是数据库数据时,使用cursor来处理明显不合适了,于是采用MatriCursor来处理非数据数据;因为此时共享数据不是数据库数据,故而返回不了cursor,所以我们可以通过MatrixCursor这个东西,来虚构出一张表;
下面例子使用到的虚构出来的表:
3.共享非数据库数据的应用添加代码:
需要提供自定义ContentProvider的项目代码:
清单文件中添加:
<!-- com.chuyitech.gourmagic.device 本项目包名(此处可不加) .contentprovider.TokenAndUserIdContentProvider
自定义contentprovider相对路径
-->
<provider
android:name="com.chuyitech.gourmagic.device.contentprovider.TokenAndUserIdContentProvider"
android:authorities="com.chuyi.cookdemo.provider"
android:enabled="true"
android:exported="true" />
注意上面的authorities(为了能让其他应用找到该ContentProvider,需要使用主机名/域名来对它进行唯一之标识),同时该authorities要和使用该ContentProvider共享数据的本机上另外一个app配合使用,
同时发现没有,此处之authorities,就是上面的URI的组成部分,结合content来,得到标准的内容URI
content://com.chuyi.cookdemo.provider
通过Uri解析,得到uri参数
Uri.parse("content://com.chuyi.cookdemo.provider")
此处添加自定义ContentProvider
package com.chuyitech.gourmagic.device.contentprovider;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.text.TextUtils;
/**
* created sjx by 22/8/15
* 向外提供Token和UserId
*/
public class TokenAndUserIdContentProvider extends ContentProvider {
private static final String[] COLUMN_NAME = {"token", "userId"};
private static MatrixCursor matrixCursor;
public static String mToken;
public static String userId;
public TokenAndUserIdContentProvider() {
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
// Implement this to handle requests to delete one or more rows.
throw new UnsupportedOperationException("Not yet implemented");
}
@Override
public String getType(Uri uri) {
// TODO: Implement this to handle requests for the MIME type of the data
// at the given URI.
throw new UnsupportedOperationException("Not yet implemented");
}
@Override
public Uri insert(Uri uri, ContentValues values) {
// TODO: Implement this to handle requests to insert a new row.
throw new UnsupportedOperationException("Not yet implemented");
}
@Override
public boolean onCreate() {
// TODO: Implement this to initialize your content provider on startup.
return false;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
matrixCursor = new MatrixCursor(COLUMN_NAME);
matrixCursor.addRow(new Object[]{mToken, userId});
return matrixCursor;
}
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
// TODO: Implement this to handle requests to update one or more rows.
throw new UnsupportedOperationException("Not yet implemented");
}
}
根据上面代码发现依据MatrixCursor来构建了一张虚拟表COLUMN_NAME,然后通过addRow方法来共享了COLUMN_NAME中的mToken,userId值,这两个值是静态的,可以从外部直接调用赋值,看看如何赋值的,
TokenAndUserIdContentProvider.mToken = token;
TokenAndUserIdContentProvider.userId = HaierUserProperties.getuHomeUserId();
getContentResolver().notifyChange(Uri.parse("content://com.chuyi.cookdemo.provider"),null);
监听数据变化:
如果ContentProvider的访问者需要知道数据发生的变化,可以在ContentProvider发生数据变化时调用getContentResolver().notifyChange(uri, null)来通知注册在此URI上的访问者。
而访问者必须使用ContentObserver对数据(数据采用uri描述)进行监听,当监听到数据变化通知时,系统就会调用ContentObserver的onChange()方法:
只给出类中监听部分的代码:
getContentResolver().notifyChange(Uri.parse("content://com.chuyi.cookdemo.provider"),null);
至此,本应用提供自定义ContentProvider共享非数据库数据的处理已经完成;
4.其他应用访问这些共享非数据库数据需要添加代码:
清单文件:
<queries>
<provider android:authorities="com.chuyitech.device.haier.provider" />
</queries>
上面为何我要添加标签,具体原因我不记得了,这里给出该标签的意思,下次用到共享非数据库数据时再说,
添加工具类来供调用者共享非数据库数据:
package com.chuyi.device.haier.utils;
import android.content.ContentValues;
import android.content.Context;
import android.database.ContentObserver;
import android.database.Cursor;
import android.net.Uri;
import android.os.Handler;
import android.util.Log;
import com.chuyi.device.haier.UserInfo;
import java.util.Objects;
/**
* Created by sjx on 2022/8/15
* 对外暴露用户token和userid类
*/
public class UserUtils {
private static UserUtils mInstance = new UserUtils();
private String mToken;
private String mUserID;
private String deviceId;
/*这里的uri一定要和自定义ContentProvider中的authority一摸一样*/
public static String authorities = "content://com.chuyitech.device.haier.provider";
/*这里的非数据库数据也一定要一样*/
private static final String[] COLUMN_NAME = {"token", "userId", "deviceId"};
private Cursor mCursor;
private Handler mHandler = new Handler();
private OnChangeListener mListener;
private Context mContext;
public static UserUtils getInstance() {
return mInstance;
}
/*以下这些函数方法都可以调用,相应逻辑可以在相应函数中添加*/
public UserInfo getUserInfo(Context context) {
this.mContext = context;
getCursorData();
context.getContentResolver().registerContentObserver(Uri.parse(authorities),
true, new ContentObserver(mHandler) {
@Override
public void onChange(boolean selfChange) {
if (mListener != null) {
mListener.onChange();
// todo 此onChange方法中添加监听到共享数据变化时执行的操作,例如返回共享数据给调用该方法者
}
super.onChange(selfChange);
}
});
return new UserInfo(mToken, mUserID, deviceId);
}
public boolean isUserLogin() {
getCursorData();
return mToken != null && mUserID != null;
}
public boolean refreshToken(Context context) {
ContentValues values = new ContentValues();
// todo 此处调用自定义ContentProvider中的update方法来更新共享数据
int count = context.getContentResolver().update(Uri.parse(authorities), values,
null, null);
/* count==1则刷新token成功 count==-1则刷新token失败*/
Log.i("UserUtils", "count = " + count);
return count > 0;
}
public void registerOnChangeListener(OnChangeListener listener) {
this.mListener = listener;
}
public interface OnChangeListener {
void onChange();
}
private void getCursorData() {
// todo 此处调用自定义ContentProvider中的query方法来更新共享数据
mCursor = mContext.getContentResolver().query(Uri.parse(authorities), COLUMN_NAME,
null, null, null);
if (mCursor != null) {
while (mCursor.moveToNext()) {
mToken = mCursor.getString(Math.max(0, mCursor.getColumnIndex(COLUMN_NAME[0])));
mUserID = mCursor.getString(Math.max(0, mCursor.getColumnIndex(COLUMN_NAME[1])));
deviceId = mCursor.getString(Math.max(0, mCursor.getColumnIndex(COLUMN_NAME[2])));
}
}
}
}
5.将共享数据的获取工具类打包成sdk对外提供:
上面的工具类打包成sdk,提供相应的使用文档,如下:
总结
提示:这里对文章进行总结:
例如:以上就是今天要讲的内容,本文仅仅简单介绍了pandas的使用,而pandas提供了大量能使我们快速便捷地处理数据的函数和方法。