最近产品中有这样一个需求,有多个进程,在系统开机时通过ContentProvider去查询另外一个进程的数据库,获取一个公共字段。
在实际开发中,有四个进程分别去执行这个操作,但是发现第一个或者前两个进程总是出现开机查询失败对情形。报错信息如下:
07-16 07:08:32.435: E/AndroidRuntime(1869): java.lang.NullPointerException
07-16 07:08:32.435: E/AndroidRuntime(1869): at android.database.sqlite.SQLiteOpenHelper.getDatabaseLocked(SQLiteOpenHelper.java:224)
07-16 07:08:32.435: E/AndroidRuntime(1869): at android.database.sqlite.SQLiteOpenHelper.getWritableDatabase(SQLiteOpenHelper.java:164)
07-16 07:08:32.435: E/AndroidRuntime(1869): at com.example.neuronsdk.DatabaseHandler.open(DatabaseHandler.java:59)
07-16 07:08:32.435: E/AndroidRuntime(1869): at com.example.neuronsdk.DatabaseHandler.addEvent(DatabaseHandler.java:72)
07-16 07:08:32.435: E/AndroidRuntime(1869): at com.example.neuronsdk.NeuronActivity.onClick(NeuronActivity.java:28)
07-16 07:08:32.435: E/AndroidRuntime(1869): at com.example.test_application.MainActivity$1.onClick(MainActivity.java:47)
07-16 07:08:32.435: E/AndroidRuntime(1869): at android.view.View.performClick(View.java:4438)
07-16 07:08:32.435: E/AndroidRuntime(1869): at android.view.View$PerformClick.run(View.java:18422)
07-16 07:08:32.435: E/AndroidRuntime(1869): at android.os.Handler.handleCallback(Handler.java:733)
07-16 07:08:32.435: E/AndroidRuntime(1869): at android.os.Handler.dispatchMessage(Handler.java:95)
07-16 07:08:32.435: E/AndroidRuntime(1869): at android.os.Looper.loop(Looper.java:136)
07-16 07:08:32.435: E/AndroidRuntime(1869): at android.app.ActivityThread.main(ActivityThread.java:5017)
07-16 07:08:32.435: E/AndroidRuntime(1869): at java.lang.reflect.Method.invokeNative(Native Method)
07-16 07:08:32.435: E/AndroidRuntime(1869): at java.lang.reflect.Method.invoke(Method.java:515)
07-16 07:08:32.435: E/AndroidRuntime(1869): at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:779)
07-16 07:08:32.435: E/AndroidRuntime(1869): at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:595)
07-16 07:08:32.435: E/AndroidRuntime(1869): at dalvik.system.NativeStart.main(Native Method)
根据这个报错信息,很容易的就能将错误信息定位在SQLiteOpenHelper类中的getDatabaseLocked方法中:
private SQLiteDatabase getDatabaseLocked(boolean writable) {
//省略代码 ```
db = mContext.openOrCreateDatabase(mName, mEnableWriteAheadLogging ?
//省略代码 ```
}
很明显初始化数据库帮助类时context为空了。由于数据库所在的进程也是一个单独的进程,当时就想,是不是查询数据库的时候数据库进程还没有完全起来,导致context为空呢?但是心里也犯嘀咕,因为明显方法已经走到ContentProvider查询方法中了,代表这数据库进程(ContentProvider进程)已经起来了。那就只剩下一个种可能了,就是数据库帮助类初始化时,传入的context为空了。
可是自始至终,我写负责的代码部分,都没有给数据库帮助类传入过context,自然的,去查看封装好的数据库帮助类的工具类,见到如下代码:
public abstract class SingleDBHelper extends BaseDBHelper {
//省略代码 ```
public SingleDBHelper() {
this((Context)null);
}
public SingleDBHelper(Context context) {
if(context == null) {
this.mDBHelper = SQLiteDBHelper.getSQLiteDBHelper(BaseApplication.getContext(), this.getDBController());
} else {
this.mDBHelper = SQLiteDBHelper.getSQLiteDBHelper(context, this.getDBController());}
//省略代码 ```
}
可以看到,两个构造方法,如果使用无参构造,时间上在拿数据库时使用的是通过BaseApplication.getContext()得到的context,而我在查询数据库时恰是使用的无参构造,恍然大悟。
ContentProvider和Application都有getContext方法,但是这个连个方法,在各自的onCreat方法被调用之前,得到的都是null。而ContentProvider是一个通过Binder提供跨进程数据共享的组件,当ContentProvider所在的进程启动时,ContentProvider会同时启动并发布到AMS中,而这个时候,ContentProvider的onCreate方法,会先于Application的onCreate方法。也就是说,在其他进程通过ContentProvider查询数据库的时候,数据库进程起来了,ContentProvider的onCreate方法被调用,但此时Application的onCreate方法可能还没有被调用,于是通过ContentProvider增删改查方法操作数据库时,就会报上面提到的错误。
总结:虽然ContentProvider的getContext方法,和Application的getContext方法,得到的context是一个context,但是由于ContentProvider和Application的onCreate方法有一个先后顺序的问题,导致ContentProvider的getContext不为null了,但是Application的getContex方法却处于可能为空的状态。因此,初始化数据库时,能保证使用ContentProvider的contex,就能避免两者onCreate方法执行时机带来的空指针异常。