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)
    /**
     * 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 では解消しているようですAndroid 3.x 系はソースコードが公開されていないので、わかりません。

0 件のコメント: