ContentProvider中NullPointerException排查:数据库引用为空问题
问题
我正在自学ContentProvider基础知识,为名为BooksProvider的ContentProvider创建了契约类,底层数据存储在名为fiction的SQLite数据库表中,包含_ID、_TITLE、_AUTHOR和_YEAR列。
在Provider所在应用中实现了:
- 继承
SQLiteOpenHelper的DBHelper类 - 继承
ContentProvider的BooksProvider类 - 继承
AppCompatActivity的MainActivity(仅显示Hello World,无自定义代码,专注ContentProvider功能)
在BooksProvider中:
- 创建
DBHelper实例dbHelper,通过dbHelper.db引用应用数据库目录中的数据库 - 在
query()方法中调用数据库query方法:
cursor = dbHelper.db.query(BooksContract.TABLE, null, null, null,null,null,null);
但从测试应用访问该Provider时,上述代码抛出NullPointerException,提示db为空。已通过AVD的Device File Explorer定位并打开该数据库,无法理解db为空的原因,求解决。
相关代码:
AndroidManifest.xml
<application <provider android:authorities="com.mo.books_provider_app" android:name=".BooksProvider" android:enabled="true" android:exported="true"> </provider> <activity android:name=".MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application>
DBHelper类
public class DBHelper extends SQLiteOpenHelper { SQLiteDatabase db; Context context; public DBHelper(@Nullable Context context) { super(context, null, null, 1); this.context = context; } @Override public void onCreate(SQLiteDatabase sqLiteDatabase) { // Check if database already exists in the app's specific directory File f = context.getDatabasePath("Books.db"); if (f.exists()) { db = getWritableDatabase(); } else { AssetCopier assetCopier = new AssetCopier(context, "Books.db", "Books.db"); try { assetCopier.copyFromAssets("Books.db"); } catch (IOException e) { throw new RuntimeException(e.getCause()); } } } @Override public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) { // do nothing - there will only be one version (schema of the database) } }
BooksProvider类相关代码片段
public class BooksProvider extends ContentProvider { DBHelper dbHelper; Context context; UriMatcher uriMatcher; Cursor cursor; @Override public boolean onCreate() { this.context = getContext(); dbHelper = new DBHelper(context); uriMatcher = new UriMatcher(UriMatcher.NO_MATCH); // the uri of the whole content provider itself uriMatcher.addURI(BooksContract.AUTHORITY, BooksContract.PATH, BooksContract.TABLE_MATCH_CODE); uriMatcher.addURI(BooksContract.AUTHORITY, BooksContract.PATH + "/*", BooksContract.ROW_MATCH_CODE); return true; } @Nullable @Override public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) { // query the uri pattern switch (uriMatcher.match(uri)) { case BooksContract.TABLE_MATCH_CODE: // Return the whole table, all rows, all columns cursor = dbHelper.db.query(BooksContract.TABLE, null, null, null,null,null,null); break; case BooksContract.ROW_MATCH_CODE: // Return a particular row // Extract the row ID (row number from the uri) String bookId = uri.getLastPathSegment(); cursor = dbHelper.db.query(BooksContract.TABLE, projection, "_ID = ?", new String[] {bookId}, null, null, null); break; default: throw new IllegalArgumentException ("Unkown URI : + uri"); } return cursor; }
解决方案
问题根源
DBHelper中的db成员变量初始化逻辑错误:onCreate()方法仅在数据库首次创建时被调用,若数据库已存在(比如你通过Device File Explorer确认存在),onCreate()不会执行,导致db始终为null。- 即使
onCreate()执行,调用getWritableDatabase()会触发递归调用onCreate(),逻辑存在问题。
- 直接访问
dbHelper.db不符合SQLiteOpenHelper的正确使用方式,应该通过getWritableDatabase()或getReadableDatabase()获取数据库实例。
修改步骤
1. 重构DBHelper类
移除db成员变量,添加获取数据库实例的方法,同时修正数据库拷贝逻辑:
public class DBHelper extends SQLiteOpenHelper { private static final String DB_NAME = "Books.db"; private static final int DB_VERSION = 1; private Context context; public DBHelper(@Nullable Context context) { super(context, DB_NAME, null, DB_VERSION); this.context = context; // 检查数据库是否存在,不存在则从assets拷贝 if (!databaseExists()) { copyDatabaseFromAssets(); // 拷贝后获取数据库实例,触发SQLiteOpenHelper初始化流程 getWritableDatabase(); } } private boolean databaseExists() { File dbFile = context.getDatabasePath(DB_NAME); return dbFile.exists(); } private void copyDatabaseFromAssets() { AssetCopier assetCopier = new AssetCopier(context, DB_NAME, DB_NAME); try { assetCopier.copyFromAssets(DB_NAME); } catch (IOException e) { throw new RuntimeException("Failed to copy database from assets", e); } } @Override public void onCreate(SQLiteDatabase sqLiteDatabase) { // 因为我们是从assets拷贝已有的数据库,这里不需要创建表逻辑 // 若后续需要升级,可在此处理,但当前版本无需操作 } @Override public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) { // 按需求实现升级逻辑,当前版本无需操作 } // 提供获取可读数据库的方法 public SQLiteDatabase getReadableDB() { return getReadableDatabase(); } // 提供获取可写数据库的方法 public SQLiteDatabase getWritableDB() { return getWritableDatabase(); } }
2. 修改BooksProvider的query方法
不再直接访问dbHelper.db,而是调用DBHelper提供的方法获取数据库实例:
@Nullable @Override public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) { SQLiteDatabase db = dbHelper.getReadableDB(); Cursor cursor = null; switch (uriMatcher.match(uri)) { case BooksContract.TABLE_MATCH_CODE: cursor = db.query(BooksContract.TABLE, projection, selection, selectionArgs,null,null,sortOrder); break; case BooksContract.ROW_MATCH_CODE: String bookId = uri.getLastPathSegment(); cursor = db.query(BooksContract.TABLE, projection, "_ID = ?", new String[]{bookId}, null, null, sortOrder); break; default: throw new IllegalArgumentException("Unknown URI: " + uri); } // 注册内容观察者,当数据变化时通知外部 if (cursor != null) { cursor.setNotificationUri(getContext().getContentResolver(), uri); } return cursor; }
额外注意点
- 确保
AssetCopier类能正确从assets目录拷贝数据库文件,注意assets目录下确实存在Books.db。 - 测试应用访问ContentProvider时,确保Uri的authority与Manifest中
android:authorities一致。
内容的提问来源于stack exchange,提问作者azeemofwalsall
相关产品推荐
相关产品推荐

