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();
    }
}