2013/08/29

SQLiteDatabase.openDatabase で StackOverflowError

私が GooglePlay に出しているアプリ SQLiteViewer に以下のようなクラッシュレポートが届きました。

java.lang.StackOverflowError
at java.util.HashMap.<init>(HashMap.java:138)
at java.util.HashMap.<init>(HashMap.java:174)
at java.util.LinkedHashMap.<init>(LinkedHashMap.java:119)
at android.util.LruCache.<init>(LruCache.java:81)
at android.database.sqlite.SQLiteDatabase$1.<init>(SQLiteDatabase.java:2321)
at android.database.sqlite.SQLiteDatabase.setMaxSqlCacheSize(SQLiteDatabase.java:2321)
at android.database.sqlite.SQLiteDatabase.<init>(SQLiteDatabase.java:2072)
at android.database.sqlite.SQLiteDatabase.openDatabase(SQLiteDatabase.java:1129)
at android.database.sqlite.SQLiteDatabase.openDatabase(SQLiteDatabase.java:1086)
at android.database.sqlite.SQLiteDatabase.openDatabase(SQLiteDatabase.java:1146)
at android.database.sqlite.SQLiteDatabase.openDatabase(SQLiteDatabase.java:1086)
at android.database.sqlite.SQLiteDatabase.openDatabase(SQLiteDatabase.java:1146)
at android.database.sqlite.SQLiteDatabase.openDatabase(SQLiteDatabase.java:1086)
at android.database.sqlite.SQLiteDatabase.openDatabase(SQLiteDatabase.java:1146)
at android.database.sqlite.SQLiteDatabase.openDatabase(SQLiteDatabase.java:1086)
at android.database.sqlite.SQLiteDatabase.openDatabase(SQLiteDatabase.java:1146)
at android.database.sqlite.SQLiteDatabase.openDatabase(SQLiteDatabase.java:1086)
at android.database.sqlite.SQLiteDatabase.openDatabase(SQLiteDatabase.java:1146)
at ...

どうも、SQLiteDatabase#openDatabase を再帰的に呼んでいる様子。
明らかに、SQLiteDatabase 内部で再帰呼び出しに入っているので、Android SDK 内のバグなのは間違いありません。
とはいえ、原因が分からない事には対策しようが無いので、とりあえず AOSP のコードを見てみることにしました。

が…クラッシュレポートには、発生時の端末名も Android のバージョン番号も記載がありません。
仕方がないので、Android 2.1 から順に見ていくことにしました。

参考
バージョン間の違いを見ていくのは結構面倒くさいのですが、以下のサイトを使うと効率的にコードをチェックすることができます。
GrepCode: android - Java Project - Source Code

すると、Android 4.0 の SQLiteDatabase.java に以下のようなコードが

SQLiteDatabase.java (Android 4.0)
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
/**
 * Open the database according to the flags {@link #OPEN_READWRITE}
 * {@link #OPEN_READONLY} {@link #CREATE_IF_NECESSARY} and/or {@link #NO_LOCALIZED_COLLATORS}.
 *
 * <p>Sets the locale of the database to the  the system's current locale.
 * Call {@link #setLocale} if you would like something else.</p>
 *
 * <p>Accepts input param: a concrete instance of {@link DatabaseErrorHandler} to be
 * used to handle corruption when sqlite reports database corruption.</p>
 *
 * @param path to database file to open and/or create
 * @param factory an optional factory class that is called to instantiate a
 *            cursor when query is called, or null for default
 * @param flags to control database access mode
 * @param errorHandler the {@link DatabaseErrorHandler} obj to be used to handle corruption
 * when sqlite reports database corruption
 * @return the newly opened database
 * @throws SQLiteException if the database cannot be opened
 */
public static SQLiteDatabase openDatabase(String path, CursorFactory factory, int flags,
        DatabaseErrorHandler errorHandler) {
    SQLiteDatabase sqliteDatabase = openDatabase(path, factory, flags, errorHandler,
            (short) 0 /* the main connection handle */);
 
    // set sqlite pagesize to mBlockSize
    if (sBlockSize == 0) {
        // TODO: "/data" should be a static final String constant somewhere. it is hardcoded
        // in several places right now.
        sBlockSize = new StatFs("/data").getBlockSize();
    }
    sqliteDatabase.setPageSize(sBlockSize);
    sqliteDatabase.setJournalMode(path, "TRUNCATE");
 
    // add this database to the list of databases opened in this process
    synchronized(mActiveDatabases) {
        mActiveDatabases.add(new WeakReference<sqlitedatabase>(sqliteDatabase));
    }
    return sqliteDatabase;
}
 
private static SQLiteDatabase openDatabase(String path, CursorFactory factory, int flags,
        DatabaseErrorHandler errorHandler, short connectionNum) {
    SQLiteDatabase db = new SQLiteDatabase(path, factory, flags, errorHandler, connectionNum);
    try {
        if (Log.isLoggable(TAG, Log.DEBUG)) {
            Log.i(TAG, "opening the db : " + path);
        }
        // Open the database.
        db.dbopen(path, flags);
        db.setLocale(Locale.getDefault());
        if (SQLiteDebug.DEBUG_SQL_STATEMENTS) {
            db.enableSqlTracing(path, connectionNum);
        }
        if (SQLiteDebug.DEBUG_SQL_TIME) {
            db.enableSqlProfiling(path, connectionNum);
        }
        return db;
    } catch (SQLiteDatabaseCorruptException e) {
        db.mErrorHandler.onCorruption(db);
        return SQLiteDatabase.openDatabase(path, factory, flags, errorHandler);
    } catch (SQLiteException e) {
        Log.e(TAG, "Failed to open the database. closing it.", e);
        db.close();
        throw e;
    }
}


986行目で openDatabase(String, CursorFactory, int, DatabaseErrorHandler, short) を呼んでいるのですが、そのオープン時に SQLiteDatabaseCorruptException が発生した場合、1024行目で openDatabase(String, CursorFactory, int, DatabaseErrorHandler) を呼んでしまいます。
これはそのまま、986行目に行くので、同じメソッドが呼ばれ、そのメソッド内では同じデータベースを開こうとするので、当然 SQLiteDatabaseCorruptException が呼ばれ…
という現象が起きてしまうわけです。

報告されたスタックトレースと行番号がずれているので、絶対とは言えませんが、現象からしてこの問題で間違いないと思われます。

この問題の修正方法ですが、 今回の場合、事前にデータベースが壊れているかどうかを知る術がありませんので、SQLiteDatabase#openDatabase()try & catchで囲んで、データベースが壊れている旨のエラーメッセージを出すしか回避策はなさそうです。

ちなみに、この問題は Android 4.1 では解消しているようです1

  1. Android 3.x 系はソースコードが公開されていないので、わかりません。 
?

0 件のコメント: