从SQLite选择大量数据时,应用程序崩溃

时间:2018-12-28 11:47:24

标签: android sqlite android-sqlite

我正在开发一个应用程序,该应用程序将数据本地存储在SQLite DB中,然后单击按钮即可同步到服务器。我面临的问题是,当我尝试从具有1000行以上的表中选择数据时,我的应用程序崩溃了。这就是我选择数据的方式:

Cursor crsOutletData = mDatabase.rawQuery("SELECT * FROM table_name WHERE some_column_1='complete' AND some_column_2 IS NULL", null);

请注意some_column_1和some_column_2不是主键。

谢谢。

1 个答案:

答案 0 :(得分:1)

尽管游标有局限性,但可以处理数百万行。

限制是,如果一行包含的数据量大于CursorWindow(1M(早期版本)或2M)中可以容纳的数据量。通常,这仅在非常大的项目(例如图像或视频)中发生。

具有3,000,000行的示例

这是一个示例应用程序,该应用程序可处理300万行的插入和提取(虽然非常耗时)。

1。 DBDone.java

用于建立非UI线程何时完成的界面

  • (未运行主UI线程可能会由于应用程序未重新绑定(ANR)而导致崩溃)
    • 1000行不太可能导致ANR

:-

public interface DBDone {
    void dbDone();
}

2。 DBHelper.java

具有一些基本方法的数据库帮助程序,以允许添加和检索数据(也不允许选择WAL或日记模式,默认模式为Android,直到Android 9为止)。

public class DBHelper extends SQLiteOpenHelper {

    public static final String DBNAME = "mydb";
    public static final int DBVERSION = 1;
    public static final String TBL_TABLENAME = "table_name";
    public static final String COL_SOMECOLUMN1 = "some_column_1";
    public static final String COL_SOMECOLUMN2 = "some_column_2";

    public static final String crt_tablename_sql = "CREATE TABLE IF NOT EXISTS " + TBL_TABLENAME + "(" +
            COL_SOMECOLUMN1 + " TEXT, " +
            COL_SOMECOLUMN2 + " TEXT" +
            ")";

    private static boolean mWALMode = false;
    SQLiteDatabase mDB;

    public DBHelper(Context context, boolean wal_mode) {
        super(context, DBNAME, null, DBVERSION);
        mWALMode = wal_mode;
        mDB = this.getWritableDatabase();
    }

    @Override
    public void onConfigure(SQLiteDatabase db) {
        super.onConfigure(db);
        if (mWALMode) {
            db.enableWriteAheadLogging();
        } else {
            db.disableWriteAheadLogging();
        }
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(crt_tablename_sql);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int i, int i1) {

    }

    public long insert(String c1_value, String c2_value) {

        String nullcolumnhack = null;
        ContentValues cv = new ContentValues();
        if ((c1_value == null && c2_value == null)) {
            nullcolumnhack = COL_SOMECOLUMN1;
        }
        if (c1_value != null) {
            cv.put(COL_SOMECOLUMN1,c1_value);
        }
        if (c2_value != null) {
            cv.put(COL_SOMECOLUMN2,c2_value);
        }
        return mDB.insert(TBL_TABLENAME,nullcolumnhack,cv);
    }

    public long insertJustColumn1(String c1_value) {
        return this.insert(c1_value, null);
    }

    public long insertJustColumn2(String c2_value) {
        return this.insert(null,c2_value);
    }

    public Cursor getAllFromTableName() {
        return this.getAll(TBL_TABLENAME);
    }

    public Cursor getAll(String table) {
        return mDB.query(table,null,null,null,null,null,null);
    }
}

3。 MainActivity.java

  1. MainActivity实例化DatabaseHelper(mDBHlpr指出,当构造函数通过调用 getWritableDatabase 强制创建时,它将创建数据库和基础表)。

  2. 然后调用dbDone方法,该方法将mStage设置为0将在新线程中清空表。

  3. 当清空表时,将调用dbDone,mStage将为1,因此将添加数据(如果不存在,则不应该将其清空为表)。

    • 请注意,添加3,000,000行可能需要一段时间。
  4. 插入数据后,将调用dbDone,并且由于mStage现在为2,因此在提取所有行的Cursor后将记录一些信息。游标游标中的所有行,并且将两列都为空的行计数。

    • 提取的行数(3,000,000)将被写入日志。
    • 两列都为空的行数将被计算(只是为了使用游标做一些事情)。数字会随随机插入空值而变化。

:-

public class MainActivity extends AppCompatActivity implements DBDone {

    DBHelper mDBHlpr;
    int mStage = 0;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mDBHlpr = new DBHelper(this,false);
        dbDone();
    }

    // Add some data but not in the UI Thread
    private void addData() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                addSomeData(3000000);
                dbDone(); // All done so notify Main Thread
            }
        }
        ).start();
    }

    public void dbDone() {
        switch (mStage) {
            case 0:
                emptyTable();
                break;
            case 1:
                addData();
                break;
            case 2:
                logSomeInfo();
                break;
        }
        mStage++;
    }

    /**
     * Add some rows (if none exist) with random data
     * @param rows_to_add   number of rows to add
     */
    private void addSomeData(int rows_to_add) {
        Log.d("ADDSOMEDATA","The addSomeData method hass been invoked (will run in a non UI thread)");
        SQLiteDatabase db = mDBHlpr.getWritableDatabase();
        if(DatabaseUtils.queryNumEntries(db,DBHelper.TBL_TABLENAME) > 0) return;
        // Random data that can be added to the first column
        String[] potential_data1 = new String[]{null,"complete","started","stage1","stage2","stage3","stage4","stage5"};
        // Random data that can be added to the second column
        String[] potential_data2 = new String[]{null,"something else","another","different","unusual","normal"};
        Random r = new Random();
        db.beginTransaction();
        for (int i=0; i < rows_to_add; i++) {
            mDBHlpr.insert(
                    potential_data1[(r.nextInt(potential_data1.length))],
                    potential_data2[(r.nextInt(potential_data2.length))]
            );
        }
        db.setTransactionSuccessful();
        db.endTransaction();
    }


    /**
     * Log some basic info from the Cursor always traversinf the entire cursor
     */
    private void logSomeInfo() {
        Log.d("LOGSOMEINFO","The logSomeInfo method has been invoked.");
        Cursor csr = mDBHlpr.getAllFromTableName();
        StringBuilder sb = new StringBuilder("Rows in Cursor = " + String.valueOf(csr.getCount()));
        int both_null_column_count = 0;
        while (csr.moveToNext()) {
            if (csr.getString(csr.getColumnIndex(DBHelper.COL_SOMECOLUMN1)) == null && csr.getString(csr.getColumnIndex(DBHelper.COL_SOMECOLUMN2)) == null) {
                both_null_column_count++;
            }
        }
        sb.append("\n\t Number of rows where both columns are null is ").append(String.valueOf(both_null_column_count));
        Log.d("LOGSOMEINFO",sb.toString());
    }

    /**
     * Empty the table
     */
    private void emptyTable() {
        Log.d("EMPTYTABLE","The emptyTable method has been invoked (will run in a non UI thread)");
        new Thread(new Runnable() {
            @Override
            public void run() {
                mDBHlpr.getWritableDatabase().delete(DBHelper.TBL_TABLENAME,null,null);
                dbDone();
            }
        }).start();
    }
}

日志很可能包含CursorWindow完整消息,但这些消息将得到处理(因为有问题的行将包含在下一个CursorWindow中),例如:-

2018-12-30 10:19:10.862 2799-2817/so53958115.so53958115 W/CursorWindow: Window is full: requested allocation 404 bytes, free space 204 bytes, window size 2097152 bytes
2018-12-30 10:19:11.856 2799-2817/so53958115.so53958115 W/CursorWindow: Window is full: requested allocation 24 bytes, free space 11 bytes, window size 2097152 bytes
2018-12-30 10:19:12.377 2799-2817/so53958115.so53958115 W/CursorWindow: Window is full: requested allocation 7 bytes, free space 4 bytes, window size 2097152 bytes
2018-12-30 10:19:12.902 2799-2817/so53958115.so53958115 W/CursorWindow: Window is full: requested allocation 8 bytes, free space 1 bytes, window size 2097152 bytes
2018-12-30 10:19:13.433 2799-2817/so53958115.so53958115 W/CursorWindow: Window is full: requested allocation 24 bytes, free space 1 bytes, window size 2097152 bytes
2018-12-30 10:19:13.971 2799-2817/so53958115.so53958115 W/CursorWindow: Window is full: requested allocation 24 bytes, free space 21 bytes, window size 2097152 bytes
2018-12-30 10:19:14.505 2799-2817/so53958115.so53958115 W/CursorWindow: Window is full: requested allocation 24 bytes, free space 18 bytes, window size 2097152 bytes
2018-12-30 10:19:15.045 2799-2817/so53958115.so53958115 W/CursorWindow: Window is full: requested allocation 404 bytes, free space 187 bytes, window size 2097152 bytes
2018-12-30 10:19:15.598 2799-2817/so53958115.so53958115 W/CursorWindow: Window is full: requested allocation 7 bytes, free space 0 bytes, window size 2097152 bytes
...........

时间:-

日志包括:-

2018-12-30 10:17:04.610 2799-2799/? D/EMPTYTABLE: The emptyTable method has been invoked (will run in a non UI thread)
2018-12-30 10:17:04.615 2799-2817/? D/ADDSOMEDATA: The addSomeData method hass been invoked (will run in a non UI thread)
2018-12-30 10:19:10.506 2799-2817/so53958115.so53958115 D/LOGSOMEINFO: The logSomeInfo method has been invoked.
2018-12-30 10:20:17.803 2799-2817/so53958115.so53958115 D/LOGSOMEINFO: Rows in Cursor = 3000000
         Number of rows where both columns are null is 62604

所以花了:- -5毫秒以清空(已为空)表。 -2分6秒添加3,000,000行。 -1分钟7.5秒提取并遍历游标(不是您通常会提取那么多行)

但是最重要的是,游标已经处理了3,000,000行。您还可以看到CursorWindow在这种情况下为2M(2097152字节)。

结论

问题可能不是1000行对于Cursor来说太大,尽管可能是某些行(如果存储图像/视频/长文本)的最大值超过了Cursor可以处理的大小。 / p>

该问题更有可能是由于其他原因,这些原因只能通过日志中的堆栈跟踪来确定。

因此,如果没有堆栈跟踪或更全面的信息,就不可能提供特定的答案。