Realmスキーマ変更に伴うクラッシュについて
Androidアプリの開発において、Realmまわりでクラッシュが発生しております。
Realmファイルは下記の時系列で生成、変更を行いました。
- α版
- デフォルトRealmのみを使用
- β版
- RealmModuleを使用して、デフォルトRealmと別のRealmの2つに分割
- デフォルトRealm側のとあるモデルクラスにて、一部のプロパティを "Date" から "String" へ変更
β版のテスト時ではクラッシュが無いことを確認したため、そのままβ版を製品版としてリリース致しました。
(α版については内部テストのみに使用したものであり、リリースはしておりません)
発生した事象
- リリース直後から、下記のエラーログが出力される多数のクラッシュを検知(クラッシュ率は全体のダウンロード数の約16%)
- Google Playから製品版をテスト用端末へインストールしたが、クラッシュが発生しないことを確認
- α版がインストールされている端末に製品版をアップデートしたところ、下記と同様のエラーログが出力されクラッシュが発生することを確認
当該エラーログ
Fatal Exception: java.lang.RuntimeException: Unable to start activity ComponentInfo{xxx}: io.realm.exceptions.RealmMigrationNeededException: Invalid type 'String' for field 'notifyAt' in existing Realm file.
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2482)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2545)
at android.app.ActivityThread.access$900(ActivityThread.java:186)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1410)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:148)
at android.app.ActivityThread.main(ActivityThread.java:5636)
at java.lang.reflect.Method.invoke(Method.java)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:730)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:620)
Caused by io.realm.exceptions.RealmMigrationNeededException: Invalid type 'String' for field 'notifyAt' in existing Realm file.
at io.realm.ChildNotificationEntityRealmProxy.validateTable(ChildNotificationEntityRealmProxy.java:270)
at io.realm.DefaultModuleMediator.validateTable(DefaultModuleMediator.java:83)
at io.realm.Realm.initializeRealm(Realm.java:342)
at io.realm.Realm.createAndValidate(Realm.java:299)
at io.realm.Realm.createInstance(Realm.java:278)
at io.realm.RealmCache.createRealmOrGetFromCache(RealmCache.java:143)
at io.realm.Realm.getDefaultInstance(Realm.java:209)
at (appname).models.ChildModel.findById(ChildModel.java:56)
at (appname).models.UserModel.getCurrentChild(UserModel.java:30)
at (appname).activities.LaunchActivity.sendLaunchScreen(LaunchActivity.java:70)
at (appname).activities.LaunchActivity.onCreate(LaunchActivity.java:39)
at android.app.Activity.performCreate(Activity.java:6315)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1111)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2435)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2545)
at android.app.ActivityThread.access$900(ActivityThread.java:186)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1410)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:148)
at android.app.ActivityThread.main(ActivityThread.java:5636)
at java.lang.reflect.Method.invoke(Method.java)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:730)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:620)
備考
- テスト用の実機端末
- Xperia Z5, Android 6.0
- Nexus 5, Android 6.0.1
- Galaxy S3α, Android 4.3
- クラッシュしている端末、OS情報(Crashlyticsからの情報)
- Sony端末が60%、その他が40%
- OS情報
- 6.0が50%
- 4.0, 5.0が20%ずつ
- 7.0が10%
お手数ですが、ご教示お願い致します。
2017/02/23
追加情報
マイグレーション処理
import android.util.Log;
import io.realm.DynamicRealm;
import io.realm.RealmMigration;
import io.realm.RealmSchema;
public class Migration implements RealmMigration {
private static final String TAG = Migration.class.getName();
@Override
public void migrate(final DynamicRealm realm, long oldVersion, long newVersion) {
Log.d(TAG, "realm migration version:" + String.valueOf(oldVersion));
RealmSchema schema = realm.getSchema();
}
}
Realm設定部分
private static RealmConfiguration mDefaultConfig;
private static RealmConfiguration mReadOnlyConfig;
@RealmModule(classes = {
ChildArticleEntity.class,
ChildEntity.class,
ChildNotificationEntity.class,
FavoriteArticleEntity.class,
ReadArticleEntity.class
})
public static class DefaultModule {
}
@RealmModule(classes = {
ArticleEntity.class,
CategoryEntity.class,
DateEventEntity.class,
DaysOldEventEntity.class,
DaysOldNotificationEntity.class,
MonthsOldEventEntity.class,
MonthsOldNotificationEntity.class,
SuggestSearchWordEntity.class,
TimelineEntity.class
})
public static class ReadOnlyModule {
}
public static void setupDefaultRealm() {
if (mDefaultConfig == null) {
mDefaultConfig = new RealmConfiguration.Builder()
.schemaVersion(CURRENT_SCHEME_VERSION)
.migration(new Migration())
.modules(new DefaultModule())
.build();
}
Realm.setDefaultConfiguration(mDefaultConfig);
}
public static Realm getReadonlyInstance() {
if (mReadOnlyConfig == null) {
mReadOnlyConfig = new RealmConfiguration.Builder()
.name(READ_ONLY_FILE_NAME)
.schemaVersion(CURRENT_SCHEME_VERSION)
.migration(new Migration())
.modules(new ReadOnlyModule())
.build();
}
return Realm.getInstance(mReadOnlyConfig);
}
スキーマ
import java.util.Date;
import io.realm.RealmObject;
public class ChildNotificationEntity extends RealmObject {
private int childId;
private String message;
private String notifyAt;
public int getChildId() {
return childId;
}
public void setChildId(int childId) {
this.childId = childId;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
public String getNotifyAt() {
return notifyAt;
}
public void setNotifyAt(String notifyAt) {
this.notifyAt = notifyAt;
}
}
あらかじめ作成したrealmファイルをアプリケーションに読み込む処理
public static void importReadonlyRealmIfNeeded(Context context) {
try {
//端末内保持バージョン
SharedPreferences s = context.getSharedPreferences(AppConst.PACKAGE_NAME, Context.MODE_PRIVATE);
int versionCode = s.getInt(KEY_VERSION_CODE, 0);
//現在のビルドバージョン
PackageInfo pInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
int currentVersionCode = pInfo.versionCode;
//version codeがアップデートされるとインポートが走る
if (versionCode == currentVersionCode) {
Log.i("realm", "import skip");
return;
}
Log.d(TAG, "Copy readonly.realm to this device");
copyBundledRealmFile(context.getResources().openRawResource(R.raw.readonly), READ_ONLY_FILE_NAME, context);
s.edit().putInt(KEY_VERSION_CODE, currentVersionCode).apply();
Log.i("realm", "success import");
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
}
private static void copyBundledRealmFile(InputStream inputStream, String outFileName, Context context) {
try {
File file = new File(context.getFilesDir(), outFileName);
FileOutputStream outputStream = new FileOutputStream(file);
byte[] buf = new byte[1024];
int bytesRead;
while ((bytesRead = inputStream.read(buf)) > 0) {
outputStream.write(buf, 0, bytesRead);
}
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}