2013/08/29
SQLiteDatabase.openDatabase で StackOverflowError
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
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。