一次失败的SP优化

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

猜你喜欢

转载自blog.csdn.net/pouloghost/article/details/76077846