一次失败的SP优化

来源:互联网 发布:室内分布设计软件 编辑:程序博客网 时间:2024/05/20 23:56

去到了一个新公司,发现一个神奇的操作:所有数据都是存在同一个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本身的数据相当亮眼,并没有优化的意义。这么尴尬的事情,记录一下吧。