去到了一个新公司,发现一个神奇的操作:所有数据都是存在同一个sp中的。从源码上看,sp会在new的时候启动异步线程读取整个文件,并parse成map,这会阻塞第一个读写操作;而每次commit/apply的时候,都会写入全量文件。看起来这里有一个性能问题。本着无聊+装逼的心情,做了一个基于一致性哈希的SP。
public class ConsistentHashPreferences implements SharedPreferences {
private PreferenceConfig mPreferenceConfig;
public ConsistentHashPreferences(PreferenceConfig config) {
mPreferenceConfig = config;
}
@Override
public Map<String, ?> getAll() {
Map<String, Object> map = new HashMap<>();
for (SharedPreferences sp : mPreferenceConfig.all()) {
map.putAll(sp.getAll());
}
return map;
}
@Nullable
@Override
public String getString(String key, @Nullable String defValue) {
return mPreferenceConfig.preferenceForKey(key).getString(key, defValue);
}
@Nullable
@Override
public Set<String> getStringSet(String key, @Nullable Set<String> defValues) {
return null;
}
@Override
public int getInt(String key, int defValue) {
return mPreferenceConfig.preferenceForKey(key).getInt(key, defValue);
}
@Override
public long getLong(String key, long defValue) {
return mPreferenceConfig.preferenceForKey(key).getLong(key, defValue);
}
@Override
public float getFloat(String key, float defValue) {
return mPreferenceConfig.preferenceForKey(key).getFloat(key, defValue);
}
@Override
public boolean getBoolean(String key, boolean defValue) {
return mPreferenceConfig.preferenceForKey(key).getBoolean(key, defValue);
}
@Override
public boolean contains(String key) {
return mPreferenceConfig.preferenceForKey(key).contains(key);
}
@Override
public Editor edit() {
return new EditorCache();
}
@Override
public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {
}
@Override
public void unregisterOnSharedPreferenceChangeListener(
OnSharedPreferenceChangeListener listener) {
}
private class EditorCache implements SharedPreferences.Editor {
private Map<Integer, Editor> mEditorCache;
private List<TranscationItem> mTransactions;
private Editor obtainEditor(String key) {
if (mEditorCache == null) {
mEditorCache = new HashMap<>();
}
Editor editor = mEditorCache.get(mPreferenceConfig.hashForKey(key));
if (editor == null) {
editor = mPreferenceConfig.preferenceForKey(key).edit();
}
mEditorCache.put(mPreferenceConfig.hashForKey(key), editor);
return editor;
}
public EditorCache() {
init();
}
private void init() {
mTransactions = new ArrayList<>();
}
@Override
public SharedPreferences.Editor putString(String key, @Nullable String value) {
mTransactions.add(new StringTransactionItem(key, value));
return this;
}
@Override
public SharedPreferences.Editor putStringSet(String key, @Nullable Set<String> values) {
return null;
}
@Override
public SharedPreferences.Editor putInt(String key, int value) {
mTransactions.add(new IntegerTransactionItem(key, value));
return this;
}
@Override
public SharedPreferences.Editor putLong(String key, long value) {
mTransactions.add(new LongTransactionItem(key, value));
return this;
}
@Override
public SharedPreferences.Editor putFloat(String key, float value) {
mTransactions.add(new FloatTransactionItem(key, value));
return this;
}
@Override
public SharedPreferences.Editor putBoolean(String key, boolean value) {
mTransactions.add(new BooleanTransactionItem(key, value));
return this;
}
@Override
public SharedPreferences.Editor remove(String key) {
mTransactions.add(forRemove(key));
return this;
}
@Override
public SharedPreferences.Editor clear() {
// TODO: 2017/7/1 foreach
return this;
}
@Override
public boolean commit() {
boolean success = doCommit();
if (!success) {
doRevert();
}
init();
return success;
}
private void doRevert() {
for (TranscationItem transcationItem : mTransactions) {
transcationItem.revert(obtainEditor(transcationItem.key));
}
for (Editor editor : mEditorCache.values()) {
editor.commit();
}
}
private boolean doCommit() {
for (TranscationItem transcationItem : mTransactions) {
transcationItem.commit(obtainEditor(transcationItem.key));
}
boolean success = true;
for (Editor editor : mEditorCache.values()) {
success &= editor.commit();
}
return success;
}
@Override
public void apply() {
new Thread() {
@Override
public void run() {
super.run();
commit();
}
}.start();
}
}
private abstract class TranscationItem<T> {
public String key;
private Pair<T, T> mData;
public TranscationItem(String key, T to) {
this.key = key;
T from = oldValue();
mData = new Pair<>(from, to);
}
public void commit(Editor editor) {
if (mData.second == null) {
editor.remove(key);
} else {
toEditor(editor, mData.second);
}
}
public void revert(Editor editor) {
if (mData.first == null) {
editor.remove(key);
} else {
toEditor(editor, mData.first);
}
}
protected abstract void toEditor(Editor editor, T value);
protected abstract T oldValue();
}
private class StringTransactionItem extends TranscationItem<String> {
public StringTransactionItem(String key, String to) {
super(key, to);
}
@Override
protected void toEditor(Editor editor, String value) {
editor.putString(key, value);
}
@Override
protected String oldValue() {
return mPreferenceConfig.preferenceForKey(key).getString(key, null);
}
}
private class IntegerTransactionItem extends TranscationItem<Integer> {
public IntegerTransactionItem(String key, Integer to) {
super(key, to);
}
@Override
protected void toEditor(Editor editor, Integer value) {
editor.putInt(key, value);
}
@Override
protected Integer oldValue() {
return mPreferenceConfig.preferenceForKey(key).getInt(key, 0);
}
}
private class LongTransactionItem extends TranscationItem<Long> {
public LongTransactionItem(String key, Long to) {
super(key, to);
}
@Override
protected void toEditor(Editor editor, Long value) {
editor.putLong(key, value);
}
@Override
protected Long oldValue() {
return mPreferenceConfig.preferenceForKey(key).getLong(key, 0);
}
}
private class FloatTransactionItem extends TranscationItem<Float> {
public FloatTransactionItem(String key, Float to) {
super(key, to);
}
@Override
protected void toEditor(Editor editor, Float value) {
editor.putFloat(key, value);
}
@Override
protected Float oldValue() {
return mPreferenceConfig.preferenceForKey(key).getFloat(key, 0);
}
}
private class BooleanTransactionItem extends TranscationItem<Boolean> {
public BooleanTransactionItem(String key, Boolean to) {
super(key, to);
}
@Override
protected void toEditor(Editor editor, Boolean value) {
editor.putBoolean(key, value);
}
@Override
protected Boolean oldValue() {
return mPreferenceConfig.preferenceForKey(key).getBoolean(key, false);
}
}
private TranscationItem forRemove(String key) {
if (TextUtils.isEmpty(key) || !mPreferenceConfig.preferenceForKey(key).contains(key)) {
return null;
}
Object value = mPreferenceConfig.preferenceForKey(key).getAll().get(key);
if (value instanceof String) {
return new StringTransactionItem(key, null);
} else if (value instanceof Integer) {
return new IntegerTransactionItem(key, null);
} else if (value instanceof Long) {
return new LongTransactionItem(key, null);
} else if (value instanceof Float) {
return new FloatTransactionItem(key, null);
} else if (value instanceof Boolean) {
return new BooleanTransactionItem(key, null);
}
return null;
}
}
public class PreferenceConfig {
private static final int PREFERENCE_SIZE = 10;
private static final String KEY_VERSION = "chp_version";
private String mPrefix = "Kwai_";
private SharedPreferences mNameMappings;
private SparseArray<String> mPreferencesMappings;
private Map<String, Integer> mHashMappings;
private Map<String, SharedPreferences> mPreferences = new HashMap<>();
private Context mContext;
private int mVersion;
public PreferenceConfig(String prefix, String configName, Context context,
SparseArray<String> preferencesMappings,
Map<String, Integer> hashMappings, int version) {
mPrefix = prefix;
mNameMappings = context.getSharedPreferences(mPrefix + configName, Context.MODE_PRIVATE);
mPreferencesMappings = genPreferencesMappings(preferencesMappings);
mHashMappings = hashMappings;
mVersion = version;
mContext = context;
checkAndUpgrade();
}
@NonNull
private SparseArray<String> genPreferencesMappings(SparseArray<String> preferencesMappings) {
SparseArray<String> mappings =
preferencesMappings == null ? new SparseArray<String>() : preferencesMappings;
String last = null;
int firstIndex = 0;
for (int i = 0; i < PREFERENCE_SIZE; ++i) {
if (!TextUtils.isEmpty(last = mappings.get(i))) {
firstIndex = i;
break;
}
}
if (TextUtils.isEmpty(last)) {
last = mPrefix;
}
for (int i = PREFERENCE_SIZE + firstIndex; i > firstIndex; --i) {
if (TextUtils.isEmpty(mappings.get((i - 1) % PREFERENCE_SIZE))) {
mappings.put((i - 1) % PREFERENCE_SIZE, last);
} else {
last = mappings.get((i - 1) % PREFERENCE_SIZE);
}
}
return mappings;
}
private String nameForIndex(int i) {
String name = mPreferencesMappings.get(i);
return TextUtils.isEmpty(name) ? mPrefix : name;
}
int hashForKey(String key) {
if (mHashMappings == null) {
return key.hashCode();
}
Integer mapping = mHashMappings.get(key);
return mapping == null ? key.hashCode() : mapping;
}
SharedPreferences preferenceForKey(String key) {
int hash = hashForKey(key);
String name = nameForIndex(hash);
return getSharedPreferencesByName(name);
}
private SharedPreferences getSharedPreferencesByName(String name) {
if (mPreferences.get(name) == null) {
mPreferences.put(name, mContext
.getSharedPreferences(name, Context.MODE_MULTI_PROCESS));
}
return mPreferences.get(name);
}
private void checkAndUpgrade() {
int oldVersion = mNameMappings.getInt(KEY_VERSION, 0);
if (mVersion <= oldVersion) {
mPreferencesMappings = new SparseArray<>();
for (int i = 0; i < PREFERENCE_SIZE; ++i) {
mPreferencesMappings.put(i, mNameMappings.getString(String.valueOf(i), null));
}
return;
}
// 迁移sp
if (!transferSp()) {
return;
}
// 迁移group
if (!transferGroup()) {
return;
}
commitVersion();
}
private boolean commitVersion() {
SharedPreferences.Editor editor = mNameMappings.edit();
editor.putInt(KEY_VERSION, mVersion);
for (int i = 0; i < PREFERENCE_SIZE; ++i) {
editor.putString(String.valueOf(i), mPreferencesMappings.get(i));
}
return editor.commit();
}
private boolean transferGroup() {
// 尽量减少commit次数,重复缓存了很多东西
HashMap<String, Map<String, ?>> oldDataCache = new HashMap<>();
HashMap<String, Set<String>> toRemove = new HashMap<>();
HashMap<String, Map<String, Object>> toAdd = new HashMap<>();
for (String key : mHashMappings.keySet()) {
if (preferenceForKey(key).contains(key)) {
continue;
}
String oldName = nameForIndex(key.hashCode());
if (oldDataCache.get(oldName) == null) {
oldDataCache.put(oldName, preferenceForKey(oldName).getAll());
}
String newName = nameForIndex(hashForKey(key));
if (toAdd.get(newName) == null) {
toAdd.put(newName, new HashMap<String, Object>());
}
toAdd.get(newName).put(key, oldDataCache.get(oldName).get(key));
if (toRemove.get(oldName) == null) {
toRemove.put(oldName, new HashSet<String>());
}
toRemove.get(oldName).add(key);
}
boolean success = true;
for (String name : toAdd.keySet()) {
SharedPreferences.Editor editor = getSharedPreferencesByName(name).edit();
Map<String, Object> data = toAdd.get(name);
for (String key : data.keySet()) {
putByType(editor, key, data.get(key));
}
success &= editor.commit();
}
if (!success) {
return false;
}
for (String name : toRemove.keySet()) {
SharedPreferences.Editor editor = getSharedPreferencesByName(name).edit();
Set<String> keys = toRemove.get(name);
for (String key : keys) {
editor.remove(key);
}
success &= editor.commit();
}
return success;
}
private boolean transferSp() {
// 目标sp只会对应到一个源sp
HashMap<String, String> transferMapping = new HashMap<>();
for (int i = 0; i < PREFERENCE_SIZE; ++i) {
String oldName = mNameMappings.getString(String.valueOf(i), null);
String name = mPreferencesMappings.get(i);
if (!TextUtils.equals(oldName, name)) {
// 不同sp
if (oldName == null) {
oldName = mPrefix;
}
transferMapping.put(name, oldName);
}
}
boolean success = true;
for (String newName : transferMapping.keySet()) {
Set<String> toRemove = new HashSet<>();
SharedPreferences oldPreferences = getSharedPreferencesByName(transferMapping.get(newName));
Map<String, ?> oldData = oldPreferences.getAll();
SharedPreferences.Editor oldEditor = oldPreferences.edit();
SharedPreferences.Editor newEditor = getSharedPreferencesByName(newName).edit();
for (String key : oldData.keySet()) {
if (TextUtils.equals(nameForIndex(hashForKey(key)), newName)) {
toRemove.add(key);
putByType(newEditor, key, oldData.get(key));
}
}
if (newEditor.commit()) {
for (String key : toRemove) {
oldEditor.remove(key);
}
success &= oldEditor.commit();
} else {
success = false;
}
}
return success;
}
private void putByType(SharedPreferences.Editor editor, String key, Object value) {
if (TextUtils.isEmpty(key) || value == null) {
return;
}
if (value instanceof String) {
editor.putString(key, (String) value);
} else if (value instanceof Integer) {
editor.putInt(key, (Integer) value);
} else if (value instanceof Long) {
editor.putLong(key, (Long) value);
} else if (value instanceof Float) {
editor.putFloat(key, (Float) value);
} else if (value instanceof Boolean) {
editor.putBoolean(key, (Boolean) value);
}
}
public Collection<SharedPreferences> all() {
return mPreferences.values();
}
}
ConsistentHash算法就不用多说了,用在sp上的坑也是有的:
- 升级和分块的控制,即具体sp实体放到哪个hashcode。此处仿照DB的version思路。
- commit的原子性。一次edit到commit之间的所有操作应该是一个transaction。使用了command模式。这里是性能瓶颈,出现了太多的额外对象创建和Map扩展。最终导致了悲剧。
最后做了性能测试,发现一致性哈希完败。千行数据,只有在单行超30字符的时候,读取能够持平;写入有百倍的劣化。而且,最尴尬的是,sp本身的数据相当亮眼,并没有优化的意义。这么尴尬的事情,记录一下吧。