Background

Booster v0.1.5 actually already included an optimization for SharedPreferences, though the scope was modest. Since SharedPreferences is so pervasive in Android, we were extremely cautious with it – it took several production releases to validate before we rolled out the latest optimization.

As for why SharedPreferences needs optimization at all, anyone who has done Android development knows its design has long been criticized. In truth, SharedPreferences was never designed by Google’s engineers to be used the way it is today. It simply got pushed far beyond its intended use case, leading to all sorts of jank and ANR issues.

The v0.1.5 Optimization

The SharedPreferences optimization in booster v0.1.5 replaced Editor.apply() with Editor.commit() executed on a background thread:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class ShadowEditor {

public static void apply(final SharedPreferences.Editor editor) {
if (Looper.myLooper() == Looper.getMainLooper()) {
AsyncTask.SERIAL_EXECUTOR.execute(new Runnable() {
@Override
public void run() {
editor.commit();
}
});
} else {
editor.commit();
}
}

}

For why we replace Editor.apply() with asynchronous Editor.commit(), see this article: http://www.cloudchou.com/android/post-988.html

The v0.2.0 Optimization

Booster v0.2.0 took the SharedPreferences optimization further. When Editor.commit() is called but its return value is unused, the commit is moved to a background thread:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
override fun transform(context: TransformContext, klass: ClassNode): ClassNode {
if (klass.name == SHADOW_EDITOR) {
return klass
}

klass.methods.forEach { method ->
method.instructions?.iterator()?.asIterable()?.filterIsInstance(MethodInsnNode::class.java)?.filter {
it.opcode == Opcodes.INVOKEINTERFACE && it.owner == SHARED_PREFERENCES_EDITOR
}?.forEach { invoke ->
when ("${invoke.name}${invoke.desc}") {
"commit()Z" -> if (Opcodes.POP == invoke.next?.opcode) {
// if the return value of commit() does not used
// use asynchronous commit() instead
invoke.optimize(klass, method)
method.instructions.remove(invoke.next)
}
"apply()V" -> invoke.optimize(klass, method)
}
}
}
return klass
}

The Data Consistency Problem

While the first two optimizations addressed jank and ANR to some extent, they actually introduced a bug. Consider this code:

1
2
3
4
5
SharedPreferences sp = context.getSharedPreferences("config", Context.MODE_PRIVATE);
Editor editor = sp.edit();
editor.put("key", "value");
editor.commit();
String value = sp.getString("key", null);

See the problem? A put followed immediately by a get – the value might not be there yet, because Editor.commit() could still be queued in the thread pool. While code like this is uncommon, it can still happen. So we introduced a new optimization with the following goals:

  1. Fix jank and ANR caused by SharedPreferences.
  2. Fix cross-process data sharing for SharedPreferences.
  3. Fix the data consistency issue left over from previous versions.

The Ultimate Solution

To truly solve the SharedPreferences problem, we need to avoid all the pitfalls of the native implementation:

  1. ANR caused by Editor.apply().
  2. Main thread jank caused by Editor.commit().
  3. Main thread jank or even ANR from frequent asynchronous Editor.commit() calls.
  4. Inability to synchronize data across processes in a timely manner.

Booster’s solution is BoosterSharedPreferences. Through SharedPreferencesTransformer, all calls to Context.getSharedPreferences(String, int) are replaced with ShadowSharedPreferences.getSharedPreferences(Context, String, int):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class ShadowSharedPreferences {

public static SharedPreferences getSharedPreferences(final Context context, String name, final int mode) {
if (TextUtils.isEmpty(name)) {
name = "null";
}
return BoosterSharedPreferences.getSharedPreferences(name);
}

public static SharedPreferences getPreferences(final Activity activity, final int mode) {
return getSharedPreferences(activity.getApplicationContext(), activity.getLocalClassName(), mode);
}

}

In BoosterSharedPreferences, SharedPreferences instances are cached, which significantly improves performance:

1
2
3
4
5
6
public static SharedPreferences getSharedPreferences(final String name) {
if (!sSharedPreferencesMap.containsKey(name)) {
sSharedPreferencesMap.put(name, new BoosterSharedPreferences(name));
}
return sSharedPreferencesMap.get(name);
}