MODE_MULTI_PROCESS for SharedPreferences isn't working
Asked Answered
T

7

30

I have a SyncAdapter running on its own process separately from the main app process.

I'm using a static wrapper class around my SharedPreferences that creates a static object on process load (Application's onCreate) like so:

myPrefs = context.getSharedPreferences(MY_FILE_NAME, Context.MODE_MULTI_PROCESS | Context.MODE_PRIVATE);

The wrapper has get and set methods, like so:

public static String getSomeString() {
    return myPrefs.getString(SOME_KEY, null);
}

public static void setSomeString(String str) {
    myPrefs.edit().putString(SOME_KEY, str).commit();
}

Both SyncAdapter and app uses this wrapper class to edit and get from the prefs, this works sometimes but a lot of times I see the SyncAdapter getting old/missing prefs on accesses to the prefs, while the main app sees the recent changes properly.

According to the docs I think the MODE_MULTI_PROCESS flag should work as I expect it to, allowing both processes to see latest changes, but it doesn't work.

Update:

Per x90's suggestion, I've tried refraining from using a static SharedPreferences object and instead calling getSharedPreferences on each get/set method. This caused a new issue, where the prefs file gets deleted (!!!) on multi-process simultaneous access. i.e. I see in the logcat:

(process 1): getName => "Name"
(process 2): getName => null
(process 1): getName => null

and from that point all the prefs saved on the SharedPreferences object were deleted.

This is probably a result of another warning I see in the log:

W/FileUtils(21552): Failed to chmod(/data/data/com.my_company/shared_prefs/prefs_filename.xml): libcore.io.ErrnoException: chmod failed: ENOENT (No such file or directory)

P.S this is not a deterministic issue, I saw the above logs after a crash happened, but couldn't recreate yet on the same device, and until now it didn't seem to happen on other devices.

ANOTHER UPDATE:

I've filed a bug report on this, after writing a small testing method to confirm this is indeed an Android issue, star it at https://code.google.com/p/android/issues/detail?id=66625

Transferase answered 2/3, 2014 at 15:46 Comment(12)
Have you tried to get preference object on demand (every time you want to work with preferences) and not hold a reference on SharedPreferences object all time in Application class?Karnes
I thought about it, but this will be a very major change to the entire app, and is not currently an option due to the size of the code. I haven't seen any mention in the docs of having a static prefs object might prevent MULTI_PROCESS from working properly... any reason you might think this is the culprit?Transferase
I'm expecting (not sure) that root cause can be java threading layer. This object can be cached in threads (processes) in different state and give different results. Anyway i think you can try to change your work flow with sharedpreferences (in the way i described before).Karnes
If it is a solution - you can create api for working with shared preferences which require context. It can be not so usable (if you want to work with prefs from object that dont have a context) but i suggest you to look in this way.Karnes
And something about multi-proces android nature. As i know Dalvik have separate classloader for every process. So if you hold your preference object even as static - this object can be loaded twice and cached for every process.Karnes
Ok, I was able to have the prefs file re-created for every set/get call, I'll give it a few days and report back my findings, thanks for the pointer.Transferase
Nope... still not working perfectly, see above update.Transferase
@Transferase we have the "preferences file gets deleted" problem plaguing us in production. I've starred the Android issue, but any fix there will never reach most existing devices. I was wondering if you've found a workaround between March and now?Figureground
@Figureground I saw you comment on the Android issue, has this issues started happening after adding the new process? If the other process isn't reading or writing to prefs it sounds like a different issue. If most of the reports come from old Samsung devices it may be code.google.com/p/android/issues/detail?id=14359Transferase
It definitely first started happening in the release where we added the android:process element. I'm less certain about not using the shared prefs; the subprocess is running a third party native lib.Figureground
What version of Android are you testing this with? We had major issues with multi-process access to SharedPreferences and eventually dumped it all and replaced it with our own implementation that uses an SQLite database as backing store.Aspirin
MODE_MULTI_PROCESS This constant was deprecated in API level 23.Baluster
I
18

Had exactly the same problem and my solution was to write a ContentProvider based replacement for the SharedPreferences. It works 100% multiprocess.

I made it a library for all of us. Here is the result: https://github.com/grandcentrix/tray

Insensibility answered 4/5, 2015 at 17:1 Comment(2)
It is going to be a good option as it was deprecated in Android 6: MODE_MULTI_PROCESS This constant was deprecated in API level 23.Baluster
Working like charm.You save my day :)Guilford
K
27

I gave a very quick look at Google's code and apparently Context.MODE_MULTI_PROCESS is not an actual way to ensure process-safety of SharedPreferences.

SharedPreferences itself is not process-safe. (That's probably why SharedPreferences documentation says "currently this class does not support use across multiple processes. This will be added later.")

MODE_MULTI_PROCESS just works in conjunction with every Context.getSharedPreferences(String name, int mode) call: when you retrieve an instance of SharedPreferences specifying the MODE_MULTI_PROCESS flag android will reload the preferences file to be up to date with any (eventual) concurrent modification that occurred to it. If you then keep that instance as a class (static or not) member, the preference file won't be reloaded again.

Using Context.getSharedPreferences(...) every time you want to write or read into preferences is not process-safe either, but I guess it's probably the closest that you can get to it at the moment.

If you don't actually need to read the same preference from the different processes, then a workaround could be to use different preferences files for the different processes.

Kinesthesia answered 11/4, 2014 at 12:11 Comment(3)
yep, seems like there's currently no good multi-process way of saving and reading preferences.Transferase
MODE_MULTI_PROCESS This constant was deprecated in API level 23.Baluster
objPreferences.contains(key); this return false in service even data is stored in preference. How to overcome this issue?Guilford
I
18

Had exactly the same problem and my solution was to write a ContentProvider based replacement for the SharedPreferences. It works 100% multiprocess.

I made it a library for all of us. Here is the result: https://github.com/grandcentrix/tray

Insensibility answered 4/5, 2015 at 17:1 Comment(2)
It is going to be a good option as it was deprecated in Android 6: MODE_MULTI_PROCESS This constant was deprecated in API level 23.Baluster
Working like charm.You save my day :)Guilford
F
7

I just ran into the same problem. I switched my app to run the service in a separate process and realized sharedPreferences was all broken.

Two things:

1) Are you using Editor.apply() or .commit()? I was using .apply(). I started checking my preference file either after the activity or the service made changes to it and realized whenever one would make a change, it would create a new file with only the newly changed value. I.E., a value written from the activity would be erased when a new value was written/changed from the service and vice versa. I switched to .commit() everywhere and this is no longer the case! From the documentation: "Note that when two editors are modifying preferences at the same time, the last one to call apply wins.

2) SharedPreferencesListener doesn't appear to work across processes even after switching to .commit(). You'll have to use Messenger Handlers or Broadcast Intents to notify of a change. When you look at the documentation for the SharedPreferences class it even says "Note: currently this class does not support use across multiple processes. This will be added later." http://developer.android.com/reference/android/content/SharedPreferences.html

In that respect we're lucky we even have the MODE_MULTI_PROCESS flag working to read/write from the same SharedPreferences across different processes.

Footling answered 6/4, 2014 at 4:18 Comment(2)
No, I've used only "commit", and as I said this might work ok, but once in few days of regular usage of my 2-processes app and the prefs would get reset. So I wrote the above loop to check it, and it shows the problem appears within a minute or two of extensive writing and reading. Can you try running a loop of reads and writes of a random number from your service and your main app, and see if you can recreate my findings?Transferase
objPreferences.contains(key); this return false in service even data is stored in preference. How to overcome this issue?Guilford
D
2

MODE_MULTI_PROCESS for SharedPreferences is depreciated now (android M -API level 23-onward).It was not process safe.

Disapprove answered 11/12, 2015 at 11:34 Comment(0)
M
2

MODE_MULTI_PROCESS was deprecated in API level 23. You can solve this problem with ContentProvider. DPreference uses a ContentProvider wrappering sharepreference. It has a better performance than using sqlite implmented. https://github.com/DozenWang/DPreference

Meddlesome answered 23/12, 2015 at 2:29 Comment(0)
N
1

Because MODE_MULTI_PROCESS is not currently supported, I haven't found any way to work with Shared Preferences between processes other than working around it.

I know people are sharing the libraries they wrote to address this, but I actually used a third-party library I found on another thread that implements SQLLite in lieu of the Shared Preferences:

https://github.com/hamsterready/dbpreferences

However, what was important to me that I haven't found addressed in other solutions was maintaining the automatic UI generation already built into Preference Fragment - better to be able to specify your elements in XML and call addPreferencesFromResource(R.xml.preferences) than have to build your UI from scratch.

So, to make this work, I subclassed each of the Preference elements I needed (in my case just Preference, SwitchPreference, and EditTextPreference), and overrode a few methods from the base classes to include saving to an instance of DatabaseSharedPreferences taken from the above library.

For example, below I subclass EditTextPreference and get the preference key from the base class. I then override the persist and getPersisted methods in Preference base class. I then override onSetInitialValue, setText, and getText in the EditText base class.

public class EditTextDBPreference extends EditTextPreference {
private DatabaseBasedSharedPreferences mDBPrefs;
private String mKey;
private String mText;

public EditTextDBPreference(Context context) {
    super(context);
    init(context);
}

public EditTextDBPreference(Context context, AttributeSet attrs) {
    super(context, attrs);
    init(context);
}

public EditTextDBPreference(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    init(context);
}

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public EditTextDBPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    super(context, attrs, defStyleAttr, defStyleRes);
    init(context);
}

private void init(Context context)
{
    mDBPrefs = new DatabaseBasedSharedPreferences(context);
    mKey = super.getKey();
}

public DatabaseBasedSharedPreferences getSharedDBPreferences()
{
    if (mDBPrefs == null) {
        return null;
    }
    return mDBPrefs;
}

@Override
protected boolean persistBoolean(boolean value) {
    if (mKey != null)
        mDBPrefs.putBoolean(mKey,value);
    return super.persistBoolean(value);
}

@Override
protected boolean persistFloat(float value) {
    if (mKey != null)
        mDBPrefs.putFloat(mKey, value);
    return super.persistFloat(value);
}

@Override
protected boolean persistInt(int value) {
    if (mKey != null)
        mDBPrefs.putInt(mKey, value);
    return super.persistInt(value);
}

@Override
protected boolean persistLong(long value) {
    if (mKey != null)
        mDBPrefs.putLong(mKey, value);
    return super.persistLong(value);
}

@Override
protected boolean persistString(String value) {
    if (mKey != null)
        mDBPrefs.putString(mKey, value);
    return super.persistString(value);
}

@Override
protected boolean getPersistedBoolean(boolean defaultReturnValue) {
    if (mKey == null)
        return false;
    return mDBPrefs.getBoolean(mKey, defaultReturnValue);
}

@Override
protected float getPersistedFloat(float defaultReturnValue) {
    if (mKey == null)
        return -1f;
    return mDBPrefs.getFloat(mKey, defaultReturnValue);
}

@Override
protected int getPersistedInt(int defaultReturnValue) {
    if (mKey == null)
        return -1;
    return mDBPrefs.getInt(mKey, defaultReturnValue);
}

@Override
protected long getPersistedLong(long defaultReturnValue) {
    if (mKey == null)
        return (long)-1.0;
    return mDBPrefs.getLong(mKey, defaultReturnValue);
}

@Override
protected String getPersistedString(String defaultReturnValue) {
    if (mKey == null)
        return null;
    return mDBPrefs.getString(mKey, defaultReturnValue);
}

@Override
public void setKey(String key) {
    super.setKey(key);
    mKey = key;
}

@Override
protected void onSetInitialValue(boolean restoreValue, Object defaultValue) {
    setText(restoreValue ? getPersistedString(mText) : (String) defaultValue);
}

@Override
public void setText(String text) {
    final boolean wasBlocking = shouldDisableDependents();
    boolean textChanged = false;
    if (mText != null && !mText.equals(text))
        textChanged = true;
    mText = text;

    persistString(text);
    if (textChanged) {
        // NOTE: This is a an external class in my app that I use to send a broadcast to other processes that preference settings have changed
        BASettingsActivity.SendSettingsUpdate(getContext());
    }
    final boolean isBlocking = shouldDisableDependents();
    if (isBlocking != wasBlocking) {
        notifyDependencyChange(isBlocking);
    }
}

@Override
public String getText() {
    return mText;
}

Then you simply specify the new element in your preferences.xml file, and voila! You now get the process interoperability of SQLLite and the UI auto-generation of PreferenceFragment!

<com.sampleproject.EditTextDBPreference
        android:key="@string/pref_key_build_number"
        android:title="@string/build_number"
        android:enabled="false"
        android:selectable="false"
        android:persistent="false"
        android:shouldDisableView="false"/>
Nonprofit answered 1/7, 2016 at 21:5 Comment(0)
E
1

I know this is an old post, but still a problem that many folks have tried to resolve with a ContentProvider, but that is a problematic approach, as it can cause ANRs, crashes, and general UI performance problems if read/write calls are not done on a background thread.

I don't mean this to be a plug, however, I wrote a library that implements the SharedPreference interface that is process-safe, but doesn't utilize ContentProvider (it uses FileObserver to sync data between processes). https://github.com/pablobaxter/Harmony

My hope is that this solves the issue many folks are having with trying to use SharedPreferences in multiple processes, and still keep their apps performant.

Eighty answered 22/7, 2022 at 21:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.