如题,ContentProvider java.lang.IllegalStateException: Cannot perform this operation because the connection pool has been closed.
上个星期有个群友问我一个关于ContentProvider 和SQLiteDatabase异常的问题,正好这段时间我接触这两个东西的次数也非常频繁,正好今天也是周六,笔者打算写一篇详细的博客顺带把解决思路也一起写在博客里,满满的都是干货,张同学请接好!
他的问题如上述图片所示,然后他也把相关代码截图给了我,出于对他工作安全问题的考虑,笔者这里就不贴出来了,反正笔者是看了几个小时,然后做了一个测试就发现了问题所在
第一步:确定解决思路
首先明确一点,那就是需要确定具体是哪一方引起的异常,然后在针对性的去解决问题,根据群友小张提供的图片,笔者写了两个类似demo放到手机上运行,为了不混淆看官老爷们的思维,笔者把小张写的App叫张三App,他同事的用户信息收集App叫李四App,现在的问题就是张三App需要访问李四App里面的用户信息,但是张三App在访问李四App数据库信息的时候一直报错
首先笔者根据图片分析了张三App、李四App关于数据库这块的架构,然后写成两个App demo,张三这边的截图只有一张,那就是关于ContentResolver类的query方法,他说是在Service里调用的,笔者创建了一个新的张三App demo作为查询方,再根据李四App提供的两张代码截图创建了新的李四App demo作为被查询方
首先看张三App demo的代码(写在一个Activity里,点一次Button执行一次query):
public void query(View view) {
Toast.makeText(getApplicationContext(), "query", Toast.LENGTH_SHORT).show();
AsynIoTask.getTaskManager().execute(new Runnable() {
@Override
public void run() {
Uri uri = Uri.parse("......");
Cursor cursor = resolver.query(uri, null, null, null, null);
if (cursor.getCount() > 0) {
while (cursor.moveToNext()) {
......
}
}
cursor.close();
}
});
}
再来看李四App demo的代码(因为是跨进程,加上看了另一张图也没有代码问题,所以看自定义ContentProvider就够了):
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
// 从数据库管理中获取读的对象
SQLiteDatabase db = DatabaseManager.getDatabase().getReadable();
Cursor cursor = null;
switch (sUriMatcher.match(uri)) {
case Media:
// 查询并获取结果cursor
cursor = db.query(table, projection, selection, selectionArgs, null, null, sortOrder);
break;
default:
Log.d(TAG, "Query Uri : " + uri.toString());
break;
}
// 关闭数据库、连接池
DatabaseManager.getDatabase().close();
// 返回cursor
return cursor;
}
看完张三App、李四App数据库部分代码,笔者再来介绍一下进程中有自定义Application、自定义ContentProvider时系统的加载流程是怎么样的,这里笔者画了一个大致启动流程图,如下图所示:
当然这部分启动逻辑应该是放在李四App里面的,如果想在自定义Application中加载数据库,那么就需要放在自定义Application的attachBaseContext方法中,如果没有这个要求,可以放自定义Application的onCreate方法中或放在自定义ContentProvider的onCreate方法中。但要注意一点,如果在自定义Application的onCreate方法中加载数据库,那么在自定义ContentProvider的onCreate方法中最好不要做数据库相关操作,因为自定义Application的onCreate方法要在自定义ContentProvider的onCreate方法执行后才被执行
还需要注意一点,如果李四App要在自定义Application的attachBaseContext方法中初始化数据库,需要在super.attachBaseContext方法之后初始化,否则系统在installProvider时获取包名方法会报空指针异常,笔者来分析一下具体原因。利用一张图,然后再通过结合图与文字描述为什么会报异常
由图可知,当自定义的Application重写了attachBaseContext方法时,系统会把attachBaseContext方法交由自定义的Application处理,否则由ContextWrapper处理,如果自定义的Application没有复写super.attachBaseContext(base)时,则系统会直接把attachBaseContext方法交由自定义的Application处理,如果复写了super.attachBaseContext(base),则依旧还是由ContextWrapper处理,如果自定义的Application不复写super.attachBaseContext(base),则系统在installProvider时通过ContextWrapper获取包名方法时会报空指针异常,这一点大家也要引起注意
第二步:测试与佐证
在第一步中笔者理清了张三App与李四App关于数据库这块的业务,现在需要加以测试和佐证以达到第一步的目标,就是明确解决思路。首先笔者查看了张三App的代码,因为代码很简单就是一个简单的Uri查询而已,压根就看不出毛病,所以笔者把矛头指向了李四App,笔者的思路是既然在李四App端的自定义ContentProvider没有报crash,那么数据到达张三App按理也不会出问题才对,理清头绪以后,我在李四App的自定义ContentProvider类的query方法中加入如下逻辑,看数据查询是不是在源头就出现了问题,代码如下:
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
// 从数据库管理中获取读的对象
SQLiteDatabase db = DatabaseManager.getDatabase().getReadable();
Cursor cursor = null;
switch (sUriMatcher.match(uri)) {
case Media:
// 查询并获取结果cursor
cursor = db.query(table, projection, selection, selectionArgs, null, null, sortOrder);
break;
default:
Log.d(TAG, "Query Uri : " + uri.toString());
break;
}
// 关闭数据库、连接池
DatabaseManager.getDatabase().close();
try {
if (cursor != null) {
if (cursor.getCount() > 0) {
while (cursor.moveToNext()) {
......
}
}
}
} catch (Exception e) {
Log.d(TAG, "Query Exception : " + e.toString());
}
// 返回 cursor
return cursor;
}
加入try catch部分代码后运行程序,在张三App处执行query,李四App报如下异常:
Query Exception : java.lang.IllegalStateException: Cannot perform this operation because the connection pool has been closed.
到这步,可能很多小伙伴已经发现问题所在了,没错!我们已经找到了问题所在!
第三步:确认问题,解决问题
经过前两步的努力,笔者已经找到了问题所在,那就是张三App在执行query时,通过跨进程IPC通信通过代理调用Binder最终执行李四App的自定义ContentProvider类的query方法并得到查询结果后,最终return给代理等一系列过程最终返回到张三App,但是在李四App自定义ContentProvider类的query方法return之前,李四App执行了数据库的关闭,这导致return结果给张三App之后连接池已然已经关闭,导致张三App再次执行Cursor getCount方法时,直接报连接池已经关闭异常,所有调用过程如下图所示:
由图可知,引起异常的问题主要是由橙色背景的①和②两个步骤引起,导致菱形步骤③抛出了异常
问题已然明确,然后现在给出解决办法,当然解决办法肯定不只一种,笔者这里给一个最简单的办法,那就是在李四App中将自定义ContentProvider类的所有暴露给外部使用的接口,将全部的数据库close注释掉,然后注册一个广播供张三App在执行完query之后使用,张三App执行query后发送该广播给李四,让李四去执行数据库的close,这样问题就能迎刃而解
至此小张同学的问题就解决了!
最后再额外讲讲关于自定义ContentProvider使用的一些小经验,使用自定义ContentProvider时一定要注意安全,在暴露接口给外部使用时要注意控制好权限问题,要不然被恶意程序攻击黑你的App数据可不好!
首先自定义ContentProvider是可以使用权限申请的,介绍如下几个常用权限
readPermission:数据库读权限
writePermission:数据库写权限
permission:数据库读写权限
exported:true表示外部应用可以访问你的数据库,false只有你的应用内部可以使用
顺便提一下,readPermission、writePermission权限都高于permission权限
如果readPermission、permission同时注册,permission就不起作用,表示只对外提供readPermission权限
如果writePermission、permission同时注册,permission就不起作用,表示只对外提供writePermission权限
三个同时注册应该也不用多说了,因为permission等价于readPermission+writePermission
详细使用(这里笔者以张三App和李四App为例):
李四App需要做的处理如下:
张三App需要做的处理如下:
需要注意的是package name可以是任意内容,只要张三App的读写权限跟李四App的读写权限name一致就没问题