首先了解一下创建一个ContentProvider类需要哪些必要的参数(要继承ContentProvider父类):
1.URI(重写的每个函数中都会用得到的参数)
2.MIME类型
因为重写的方法中要重写一个方法叫做getType()来返回MIME型
其中会用到一个类:UriMatcher
第一步:初始化UriMathcer对象:
UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);
第二步:在ContentProvider中注册Uri:
第三步:在getType中拿到对应的表名或者MIME参数;
3.重写四个增删改查函数;
为什么说ContentProvider增删改查操作是线程安全的呢?
因为通过SQLiteOpenHelper.getWritableDatabase()/getReadableDatabase() 创建数据库类的对象时,
这两个方法都调用了getDatabaseLocked(),该方法已经实现了线程安全,优先采用getReadableDatabase()方法;
getWritableDatabase() :方法以读写方式打开数据库,一旦数据库的磁盘空间满了,数据库就只能读而不能写,倘若使用的是getWritableDatabase() 方法就会报错。
getReadableDatabase():方法则是先以读写方式打开数据库,如果数据库的磁盘空间满了,就会打开失败,当打开失败后会继续尝试以只读方式打开数据库。如果该问题成功解决,则只读数据库对象就会关闭,然后返回一个可读写的数据库对象。
ContentProvider使用分为了进程内访问和进程间通信访问:
同进程内通信访问:
1.创建数据库类
基本的建表sql语句:
String SQL="create table 表名("
+"id integer primary key autoincrement,"
+"name varchar(20)," //***************************varchar(20)或者char(20),price real(实数),page integer(int 类)等等
+"author char(8))";
再执行语句db.execSQL(SQL);
2:自定义 ContentProvider 类
在上面将ContentProvider的时候基本都有讲述;
public class MyProvider extends ContentProvider {
private Context mContext;
DBHelper mDbHelper = null;
SQLiteDatabase db = null;
public static final String AUTOHORITY = "cn.scu.myprovider";
// 设置ContentProvider的唯一标识
public static final int User_Code = 1;
public static final int Job_Code = 2;
// UriMatcher类使用:在ContentProvider 中注册URI
private static final UriMatcher mMatcher;
static{
mMatcher = new UriMatcher(UriMatcher.NO_MATCH);
// 初始化
mMatcher.addURI(AUTOHORITY,"user", User_Code);
mMatcher.addURI(AUTOHORITY, "job", Job_Code);
// 若URI资源路径 = content://cn.scu.myprovider/user ,则返回注册码User_Code
// 若URI资源路径 = content://cn.scu.myprovider/job ,则返回注册码Job_Code
}
// 以下是ContentProvider的6个方法
/**
* 初始化ContentProvider
*/
@Override
public boolean onCreate() {
mContext = getContext();
// 在ContentProvider创建时对数据库进行初始化
// 运行在主线程,故不能做耗时操作,此处仅作展示
mDbHelper = new DBHelper(getContext());
db = mDbHelper.getWritableDatabase();
// 初始化两个表的数据(先清空两个表,再各加入一个记录)
db.execSQL("delete from user");
db.execSQL("insert into user values(1,'Carson');");
db.execSQL("insert into user values(2,'Kobe');");
db.execSQL("delete from job");
db.execSQL("insert into job values(1,'Android');");
db.execSQL("insert into job values(2,'iOS');");
return true;
}
/**
* 添加数据
*/
@Override
public Uri insert(Uri uri, ContentValues values) {
// 根据URI匹配 URI_CODE,从而匹配ContentProvider中相应的表名
// 该方法在最下面
String table = getTableName(uri);
// 向该表添加数据
db.insert(table, null, values);
// 当该URI的ContentProvider数据发生变化时,通知外界(即访问该ContentProvider数据的访问者)
mContext.getContentResolver().notifyChange(uri, null);
// // 通过ContentUris类从URL中获取ID
// long personid = ContentUris.parseId(uri);
// System.out.println(personid);
return uri;
}
/**
* 查询数据
*/
@Override
public Cursor query(Uri uri, String[] projection, String selection,
String[] selectionArgs, String sortOrder) {
// 根据URI匹配 URI_CODE,从而匹配ContentProvider中相应的表名
// 该方法在最下面
String table = getTableName(uri);
// // 通过ContentUris类从URL中获取ID
// long personid = ContentUris.parseId(uri);
// System.out.println(personid);
// 查询数据
return db.query(table,projection,selection,selectionArgs,null,null,sortOrder,null);
}
/**
* 更新数据
*/
@Override
public int update(Uri uri, ContentValues values, String selection,
String[] selectionArgs) {
// 由于不展示,此处不作展开
return 0;
}
/**
* 删除数据
*/
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
// 由于不展示,此处不作展开
return 0;
}
@Override
public String getType(Uri uri) {
// 由于不展示,此处不作展开
return null;
}
/**
* 根据URI匹配 URI_CODE,从而匹配ContentProvider中相应的表名
*/
private String getTableName(Uri uri){
String tableName = null;
switch (mMatcher.match(uri)) {
case User_Code:
tableName = DBHelper.USER_TABLE_NAME;
break;
case Job_Code:
tableName = DBHelper.JOB_TABLE_NAME;
break;
}
return tableName;
}
}
3.Manifest清单注册类
<provider android:name="MyProvider"
android:authorities="cn.scu.myprovider"
/>
4.进程内访问 ContentProvider中的数据
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
/**
* 对user表进行操作
*/
// 设置URI
Uri uri_user = Uri.parse("content://cn.scu.myprovider/user");
// 插入表中数据
ContentValues values = new ContentValues();
values.put("_id", 3);
values.put("name", "Iverson");
// 获取ContentResolver
ContentResolver resolver = getContentResolver();
// 通过ContentResolver 根据URI 向ContentProvider中插入数据
resolver.insert(uri_user,values);
// 通过ContentResolver 向ContentProvider中查询数据
Cursor cursor = resolver.query(uri_user, new String[]{"_id","name"}, null, null, null);
while (cursor.moveToNext()){
System.out.println("query book:" + cursor.getInt(0) +" "+ cursor.getString(1));
// 将表中数据全部输出
}
cursor.close();
// 关闭游标
/**
* 对job表进行操作
*/
// 和上述类似,只是URI需要更改,从而匹配不同的URI CODE,从而找到不同的数据资源
Uri uri_job = Uri.parse("content://cn.scu.myprovider/job");
// 插入表中数据
ContentValues values2 = new ContentValues();
values2.put("_id", 3);
values2.put("job", "NBA Player");
// 获取ContentResolver
ContentResolver resolver2 = getContentResolver();
// 通过ContentResolver 根据URI 向ContentProvider中插入数据
resolver2.insert(uri_job,values2);
// 通过ContentResolver 向ContentProvider中查询数据
Cursor cursor2 = resolver2.query(uri_job, new String[]{"_id","job"}, null, null, null);
while (cursor2.moveToNext()){
System.out.println("query job:" + cursor2.getInt(0) +" "+ cursor2.getString(1));
// 将表中数据全部输出
}
cursor2.close();
// 关闭游标
}
}
跨进程使用ContentProvider:
进程1:
1.创建数据库类;
2.自定义ContentProvider类;
3.注册
前两步和同进程内相同;
注册需要加上exported属性,来表示可以被其他进程使用;
android:exported="true"
进程2:
1.调用ContentResolver进行访问;
这一步和进程内访问相同;
contentprovider的权限设置:
①.无限制:
如果一个provider 用了exported=true,那么任意三方调用方都能对其进行调用;
②.只对满足特定条件三方才进行暴露:
provider方:
<provider
android:name=".MyContentProvider"
android:authorities="com.anddle.mycontentprovider"
android:enabled="true"
android:exported="true"
android:permission="com.anddle.provideraccess" />
<permission
android:name="com.anddle.provideraccess"
android:label="provider pomission"
android:protectionLevel="normal" />
1.配置清单中加android:permission="com.anddle.provideraccess"
2.再定义permission,其对应的name与声明的权限对应
调用方:
清单文件中声明: <uses-permission android:name="com.anddle.provideraccess" /> 即可调用provider
③.在②只对满足特定条件基础上并区分读写
provider方:
<provider
android:name=".MyContentProvider"
......
android:readPermission="com.anddle.provideraccess.read" />
<permission
android:name="com.anddle.provideraccess.read"
android:label="provider pomission"
android:protectionLevel="normal" />
1.配置清单中加android:readpermission(writepermission)="com.anddle.provideraccess.read"
2.再定义permission,其对应的name与声明的权限对应
调用方:
清单文件中声明: <uses-permission android:name="com.anddle.provideraccess.read" /> 即可调用provider
做Android SDK开发的时候,一般我们会将初始化的方法封装为,然后让调用SDK的开发者在Application的onCreate方法中进行初始化。但是目前一些主流的SDK框架,并没有提供相关的方法进行初始化,但是我们在使用的时候也能正常使用,通过挖掘其源码,可以看出来他们一般使用的ContentProvider来进行SDK的初始化的,目前使用ContentProvider的知名SDK有:ButterKnife、Leakcanary、BlockCanary...等等。
一、ContentProvider初始化SDK库的实现
要实现在ContentProvider初始化SDK库,首先要在库中创建一个 ContentProvider,然后在 ContentProvider 的 onCreate() 方法中借助 getContext() 返回的 Context 来完成你的库初始化,当然,这个 Context 的实际类型就是应用的 Application。
下面是通过ContentProvider实现SDK库初始化的示例代码:
class ToolContentProvider : ContentProvider() {
override fun onCreate(): Boolean {
Log.e(GlobalConfig.LOG_TAG, "ToolContentProvider onCreate")
AppContextHelper.init(context!!.applicationContext)
AppContextHelper.initRoomDB(context!!.applicationContext)
return true
}
override fun query(uri: Uri, projection: Array<out String>?, selection: String?, selectionArgs: Array<out String>?, sortOrder: String?): Cursor? {
return null
}
override fun getType(uri: Uri): String? {
return null
}
override fun insert(uri: Uri, values: ContentValues?): Uri? {
return null
}
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int {
return 0
}
override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<out String>?): Int {
return 0
}
}
<provider
android:name=".ToolContentProvider"
android:authorities="${applicationId}.library-tool"
android:exported="false" />
class MaoApplication : Application() {
private lateinit var currentActivityRef: WeakReference<Activity>;
override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base)
Log.e(GlobalConfig.LOG_TAG, "MaoApplication attachBaseContext")
}
override fun onCreate() {
super.onCreate()
Log.e(GlobalConfig.LOG_TAG, "MaoApplication onCreate")
initMMKV()
initCodeView()
}
/**
* 初始化MMKV工具
*/
private fun initMMKV() {
Log.e(GlobalConfig.LOG_TAG, "init MMKV")
MMKV.initialize(this);
}
private fun initCodeView() {
CodeProcessor.init(this)
}
}
通过ContentProvider实现SDK库初始化的功能实现了,那么 ContentProvider 的 onCreate() 方法是什么时候被调用的呢?
参考文章: Application 的 onCreate 和 attachBaseContext_emmmmsuperdan的博客-CSDN博客。可以看到,它是介于 Application 的 attachBaseContext(Context) 和 onCreate() 之间所调用的,Application 的 attachBaseContext(Context) 方法被调用这就意味着 Application 的 Context 被初始化了。这也再次说明我们确实可以通过ContentProvider来进行SDK库的初始化,并且执行时间在Application的onCreate之前。
二、ContentProvider初始化SDK库的优缺点
优点:
- 不需要使用SDK库的开发者调用初始化库的流程,降低了接入成本
- 代码侵入更低,使得SDK库的代码隔离性做的更好,而且方便升级和维护。
缺点:
- 不一定适用SDK库的使用场景,因为在 ContentProvider 的 onCreate() 执行在 Application 的 onCreate() 方法之前,倘若你的库需要有其它业务的依赖,那么就不适合这种方式了。
- 需要注意应用安全漏洞问题,避免组件暴露,需要在声明provider的时候,配置exported为false。
- 必须注意Provider的authorities千万别写死,否则两个引入同样SDK的App就无法共存了
三、ContentProvider初始化SDK库实现的源码分析
参考application初始化文章: Application 初始化流程_emmmmsuperdan的博客-CSDN博客