Tray - マルチプロセスをサポートしたSharedPreferences


問題点

AndroidにはSharedPreferencesというデータを簡単に保存できる方法があります。
このSharedPreferencesは同一プロセス上で読み書きする分には問題はありませんが、Aプロセスで保存し、Bプロセスで読みこむといったような複数プロセスをまたいで使うような時はデータを読み書きする際に問題が生じます。MODE_MULTI_PROCESSというモードがありますが、正しく動きません。

問題点の詳細に関しては次の記事が詳しいです。
SharedPreferences と MODE_MULTI_PROCESS がイマイチよろしくない件

またMODE_MULTI_PROCESSはAPI23から非推奨になりました。
http://developer.android.com/intl/ja/reference/android/content/Context.html#MODE_MULTI_PROCESS

どういう時に困るのか

一例を挙げれば、バックグラウンドで常駐するサービスを使うとき。
サービス自体はアプリのメインプロセスでも動かすことは可能だが、別のプロセスで動かした方が使用するメモリ量が少なくなり、システムに殺されにくくなる。
サービスで取得したデータをメインプロセスで利用する時は通常のSharedPreferencesだと値を正しく読み込めず、バグや不具合の元になります。

解決策

Trayを利用する

Trayとは

ShardePreferencesと同じように簡単にデータ保存できるライブラリです。
ContentProviderをベースに作られており、マルチプロセスでのデータの読み書きをサポートしています。

書き込みと読み込み

final AppPreferences appPreferences = new AppPreferences(getContext());

// 書き込み
appPreferences.put("key", "Tray");

// 読み込み
final String value = appPreferences.getString("key", "default");
Log.v(TAG, "value: " + value); // value: Tray

// 読み込み(デフォルト)
final String defaultValue = appPreferences.getString("key2", "default");
Log.v(TAG, "value: " + defaultValue); // value: default

書き込み時にEditorでのcommit()やapply()は必要ありません。

使い方

build.gradleに次のように記述します。

build.gradle(Project)
repositories {
    jcenter()
}
build.gradle(app)
dependencies {
    compile 'net.grandcentrix.tray:tray:0.9.2'
}

android {
    ...
    defaultConfig {
        applicationId "your.app.id" // アプリのパッケージ名

        resValue "string", "tray__authority", "${applicationId}.tray"
    }
}

記述後、プロジェクトをCleanすると次のファイルが生成されます。
/build/generated/res/generated/BUILDTYPE/values/generated.xml

generated.xml
    <!-- Values from default config. -->
    <item name="tray__authority" type="string">your.app.id.tray</item>

生成されていれば準備完了です。

検証

簡単に動作を検証してみました。

ボタンを押すたびにcountを1ずつ増やし、別プロセスでその値を読み込んでログに表示します。

PrefUtils
public class PrefUtils {
    @SuppressWarnings("unused")
    private static final String TAG = PrefUtils.class.getSimpleName();

    private static final String PREF_NAME = "test";
    private static final String KEY_COUNT = "count";

    public static void put(Context context, int count) {
        SharedPreferences pref = context.getSharedPreferences(PREF_NAME, Context.MODE_MULTI_PROCESS);
        pref.edit().putInt(KEY_COUNT, count).commit();
    }

    public static int get(Context context) {
        SharedPreferences pref = context.getSharedPreferences(PREF_NAME, Context.MODE_MULTI_PROCESS);
        return pref.getInt(KEY_COUNT, 0);
    }
}
TrayUtils
public class TrayUtils {
    @SuppressWarnings("unused")
    private static final String TAG = TrayUtils.class.getSimpleName();

    private static final String KEY_COUNT = "count";

    public static void put(Context context, int count) {
        TrayAppPreferences pref = new TrayAppPreferences(context);
        pref.put(KEY_COUNT, count);
    }

    public static int get(Context context) {
        TrayAppPreferences pref = new TrayAppPreferences(context);
        return pref.getInt(KEY_COUNT, 0);
    }
}
TrayAppPreferences
public class TrayAppPreferences extends TrayModulePreferences {
    private static final int VERSION = 1;

    public TrayAppPreferences(Context context) {
        super(context, context.getPackageName(), 1);
    }

    protected void onCreate(int newVersion) {
    }

    protected void onUpgrade(int oldVersion, int newVersion) {
        throw new IllegalStateException("Can\'t upgrade database from version " + oldVersion + " to " + newVersion + ", not implemented.");
    }
}
MainActivity
public class MainActivity extends AppCompatActivity {

    private Button mButton;
    private int mCount;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mCount = 0;

        mButton = (Button) findViewById(R.id.button);
        mButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mCount++;

                PrefUtils.put(MainActivity.this, mCount);
                TrayUtils.put(MainActivity.this, mCount);

                TestService.start(MainActivity.this);
            }
        });
    }
}
TestService
public class TestService extends IntentService {
    @SuppressWarnings("unused")
    private static final String TAG = TestService.class.getSimpleName();

    public TestService() {
        super(TAG);
    }

    public TestService(String name) {
        super(name);
    }

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        int prefCount = PrefUtils.get(this);
        int trayCount = TrayUtils.get(this);

        Log.d(TAG, "pref:" + prefCount + "\ttray:" + trayCount);
    }

    public static void start(Context context) {
        Intent intent = new Intent(context, TestService.class);
        context.startService(intent);
    }
}

検証結果

結果を見ると、SharedPreferencesを使う方法では、データの読み込みが正確に行われずマルチプロセス上では正しく動いていないことがわかります。
Trayの方は1ずつ増えているのでマルチプロセス上でも問題なく動いていることがわかります。