Android开发经验谈android知识点

Android SharedPreferences转为MMKV

2022-03-11  本文已影响0人  一个有故事的程序员

开篇废话

开局一张图,说明一切问题。


MMKV优势

可以看出MMKV相比SP的优势还是比较大的,除了需要引入库,有一些修改上的成本以外,就没有什么能够阻挡MMKV了。当然了,MMKV也有着不广为人知的缺点,放在最后。
MMKV还直接支持了将SharedPreferences的历史数据转换为MMKV进行存储,只不过需要注意一点,不可回退。

且听我慢慢道来

SP具体存在哪些问题

class ContextImpl extends Context {
    
    //根据应用包名缓存所有 SharedPreferences,根据 xmlFile 和具体的 SharedPreferencesImpl 对应上
    private static ArrayMap<String, ArrayMap<File, SharedPreferencesImpl>> sSharedPrefsCache;
 
    //根据 fileName 拿到对应的 xmlFile
    private ArrayMap<String, File> mSharedPrefsPaths;
 
}

如果我们在初始化 SharedPreferencesImpl 后紧接着就去 getValue 的话,势必也需要确保子线程已经加载完成后才去进行取值操作。SharedPreferencesImpl 就通过在每个 getValue 方法中调用 awaitLoadedLocked()方法来判断是否需要阻塞外部线程,确保取值操作一定会在子线程执行完毕后才执行。loadFromDisk()方法会在任务执行完毕后调用 mLock.notifyAll()唤醒所有被阻塞的线程。所以说,如果 SharedPreferences 存储的数据量很大的话,那么就有可能导致外部的调用者线程被阻塞,严重时甚至可能导致 ANR。当然,这种可能性也只是发生在加载磁盘文件完成之前,当加载完成后 awaitLoadedLocked()方法自然不会阻塞线程。这也是为什么第一次写入或者读取sp相比mmkv慢十多倍最主要的原因。

    @Override
    @Nullable
    public String getString(String key, @Nullable String defValue) {
        synchronized (mLock) {
            //判断是否需要让外部线程等待
            awaitLoadedLocked();
            String v = (String)mMap.get(key);
            return v != null ? v : defValue;
        }
    }
    
    @GuardedBy("mLock")
    private void awaitLoadedLocked() {
        if (!mLoaded) {
            // Raise an explicit StrictMode onReadFromDisk for this
            // thread, since the real read will be in a different
            // thread and otherwise ignored by StrictMode.
            BlockGuard.getThreadPolicy().onReadFromDisk();
        }
        while (!mLoaded) {
            try {
                //还未加载线程,让外部线程暂停等待
                mLock.wait();
            } catch (InterruptedException unused) {
            }
        }
        if (mThrowable != null) {
            throw new IllegalStateException(mThrowable);
        }
    }
    
    private void loadFromDisk() {
        ···
        synchronized (mLock) {
            mLoaded = true;
            mThrowable = thrown;
            // It's important that we always signal waiters, even if we'll make
            // them fail with an exception. The try-finally is pretty wide, but
            // better safe than sorry.
            try {
                if (thrown == null) {
                    if (map != null) {
                        mMap = map;
                        mStatTimestamp = stat.st_mtim;
                        mStatSize = stat.st_size;
                    } else {
                        mMap = new HashMap<>();
                    }
                }
                // In case of a thrown exception, we retain the old map. That allows
                // any open editors to commit and store updates.
            } catch (Throwable t) {
                mThrowable = t;
            } finally {
                //唤醒所有被阻塞的线程
                mLock.notifyAll();
            }
        }
    }
    // Returns true if any changes were made
    private MemoryCommitResult commitToMemory() {
        long memoryStateGeneration;
        boolean keysCleared = false;
        List<String> keysModified = null;
        Set<OnSharedPreferenceChangeListener> listeners = null;
        Map<String, Object> mapToWriteToDisk;
        synchronized (SharedPreferencesImpl.this.mLock) {
            // We optimistically don't make a deep copy until
            // a memory commit comes in when we're already
            // writing to disk.
            if (mDiskWritesInFlight > 0) {
                // We can't modify our mMap as a currently
                // in-flight write owns it.  Clone it before
                // modifying it.
                // noinspection unchecked
                mMap = new HashMap<String, Object>(mMap);
            }
            //拿到内存中的全量数据
            mapToWriteToDisk = mMap;
            mDiskWritesInFlight++;
            boolean hasListeners = mListeners.size() > 0;
            if (hasListeners) {
                keysModified = new ArrayList<String>();
                listeners = new HashSet<OnSharedPreferenceChangeListener>(mListeners.keySet());
            }
            synchronized (mEditorLock) {
                //用于标记最终是否改动到了 mapToWriteToDisk
                boolean changesMade = false;
                if (mClear) {
                    if (!mapToWriteToDisk.isEmpty()) {
                        changesMade = true;
                        //清空所有在内存中的数据
                        mapToWriteToDisk.clear();
                    }
                    keysCleared = true;
                    //恢复状态,避免二次修改时状态错位
                    mClear = false;
                }
                for (Map.Entry<String, Object> e : mModified.entrySet()) {
                    String k = e.getKey();
                    Object v = e.getValue();
                    // "this" is the magic value for a removal mutation. In addition,
                    // setting a value to "null" for a given key is specified to be
                    // equivalent to calling remove on that key.
                    if (v == this || v == null) { //意味着要移除该键值对
                        if (!mapToWriteToDisk.containsKey(k)) {
                            continue;
                        }
                        mapToWriteToDisk.remove(k);
                    } else { //对应修改键值对值的情况
                        if (mapToWriteToDisk.containsKey(k)) {
                            Object existingValue = mapToWriteToDisk.get(k);
                            if (existingValue != null && existingValue.equals(v)) {
                                continue;
                            }
                        }
                        //只有在的确是修改了或新插入键值对的情况才需要保存值
                        mapToWriteToDisk.put(k, v);
                    }
                    changesMade = true;
                    if (hasListeners) {
                        keysModified.add(k);
                    }
                }
                //恢复状态,避免二次修改时状态错位
                mModified.clear();
                if (changesMade) {
                    mCurrentMemoryStateGeneration++;
                }
                memoryStateGeneration = mCurrentMemoryStateGeneration;
            }
        }
        return new MemoryCommitResult(memoryStateGeneration, keysCleared, keysModified,
                listeners, mapToWriteToDisk);
    }

MMKV的优点

SharedPreferences sources = context.getSharedPreferences(name, mode);
mmkv.importFromSharedPreferences(sources);

MMKV的缺点

如何替换并且兼容

如何替换才能更好的兼容之前的代码呢?直接上代码,代码很简单,一看就懂。

import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences;
import android.text.TextUtils;

import androidx.annotation.Nullable;

import com.getkeepsafe.relinker.ReLinker;
import com.tencent.mmkv.MMKV;
import com.tencent.mmkv.MMKVLogLevel;

import java.util.Set;

/**
 * Created by guoshichao on 2022/1/12
 * 替换SharedPreferences为MMKV
 */
public class BluedSharedPreferences {

    public static BluedSharedPreferences getDefaultSharedPreferences() {
        Context context = MyApplication.getAppContext();
        String defaultName = context.getPackageName() + "_preferences";
        return new BluedSharedPreferences(context, defaultName, Context.MODE_PRIVATE);
    }

    public static BluedSharedPreferences getSharedPreferences(String name) {
        return new BluedSharedPreferences(MyApplication.getAppContext(), name, Context.MODE_PRIVATE);
    }

    public static BluedSharedPreferences getSharedPreferences(String name, int mode) {
        return new BluedSharedPreferences(null, name, mode);
    }

    public static BluedSharedPreferences getSharedPreferences(Context context, String name, int mode) {
        return new BluedSharedPreferences(context, name, mode);
    }

    /**
     * WRITE_TO_MMKV 为ture表示数据写入MMKV,为false,表示数据从MMKV写入SharedPreferences
     */
    private static boolean mMMKVEnabled = true;
    public static void setMMKVEnable(boolean enable) {
        mMMKVEnabled = enable;
    }
    public static boolean isMMKVEnable() {
        return mMMKVEnabled;
    }

    private MMKV mmkv, defaultMMKV;
    private SharedPreferences spData;
    private SharedPreferences.Editor spEditor;

    private static boolean mmkvInited = false;
    public static void initMMKV(Application app) {
        if (mmkvInited) {
            return;
        }
        mmkvInited = true;

        if (BluedSharedPreferences.isMMKVEnable()) {
            String root = app.getFilesDir().getAbsolutePath() + "/mmkv";
            MMKVLogLevel logLevel = MyApplication.isDebuging() ? MMKVLogLevel.LevelDebug : MMKVLogLevel.LevelError;
            try {
                MMKV.initialize(root, new MMKV.LibLoader() {
                    @Override
                    public void loadLibrary(String libName) {
                        try {
                            ReLinker.loadLibrary(app, libName);
                        } catch (Throwable ex) {
                            BluedSharedPreferences.setMMKVEnable(false);
                        }
                    }
                }, logLevel);
            } catch (Throwable ex) {
                BluedSharedPreferences.setMMKVEnable(false);
            }
        }
    }

    private BluedSharedPreferences(Context context, String name, int mode) {
        if (mMMKVEnabled) {
            try {
                MMKV.initialize(MyApplication.getAppContext());
                this.mmkv = MMKV.mmkvWithID(name);
                this.defaultMMKV = MMKV.defaultMMKV();
            } catch (IllegalArgumentException iae) {
                String message = iae.getMessage();
                if (!TextUtils.isEmpty(message) && message.contains("Opening a multi-process MMKV")) {
                    try {
                        this.mmkv = MMKV.mmkvWithID(name, MMKV.MULTI_PROCESS_MODE);
                        this.defaultMMKV = MMKV.defaultMMKV(MMKV.MULTI_PROCESS_MODE, null);
                    } catch (Throwable ex) {
                        //如果出现异常抛埋点给服务端
                        BluedStatistics.getEvent().eventNormal("MMKV", 0, 102, name);
                        return;
                    }
                }
            } catch (Throwable ex) {
                //如果出现异常抛埋点给服务端
                BluedStatistics.getEvent().eventNormal("MMKV", 0, 101, name);
                return;
            }
        }

        if (null == context) {
            context = MyApplication.getAppContext();
        }

        if (null != context) {
            if (mMMKVEnabled) {
                if (null != defaultMMKV && !defaultMMKV.contains(name)) {
                    SharedPreferences sources = context.getSharedPreferences(name, mode);
                    mmkv.importFromSharedPreferences(sources);
                    defaultMMKV.encode(name, true);
                    Logger.i("BluedSharedPreferences", "transform SP-" + name + " to MMKV");
                }
            } else {
                spData = context.getSharedPreferences(name, mode);
            }
        }
    }

    public final class Editor {
        public Editor putString(String key, @Nullable String value) {
            if (mMMKVEnabled) {
                if (null != mmkv) {
                    mmkv.encode(key, value);
                }
            } else {
                if (null != spEditor) {
                    spEditor.putString(key, value);
                }
            }
            return this;
        }

        public Editor putStringSet(String key, @Nullable Set<String> values) {
            if (mMMKVEnabled) {
                if (null != mmkv) {
                    mmkv.encode(key, values);
                }
            } else {
                if (null != spEditor) {
                    spEditor.putStringSet(key, values);
                }
            }
            return this;
        }

        public Editor putInt(String key, int value) {
            if (mMMKVEnabled) {
                if (null != mmkv) {
                    mmkv.encode(key, value);
                }
            } else {
                if (null != spEditor) {
                    spEditor.putInt(key, value);
                }
            }
            return this;
        }

        public Editor putLong(String key, long value) {
            if (mMMKVEnabled) {
                if (null != mmkv) {
                    mmkv.encode(key, value);
                }
            } else {
                if (null != spEditor) {
                    spEditor.putLong(key, value);
                }
            }
            return this;
        }

        public Editor putFloat(String key, float value) {
            if (mMMKVEnabled) {
                if (null != mmkv) {
                    mmkv.encode(key, value);
                }
            } else {
                if (null != spEditor) {
                    spEditor.putFloat(key, value);
                }
            }
            return this;
        }

        public Editor putBoolean(String key, boolean value) {
            if (mMMKVEnabled) {
                if (null != mmkv) {
                    mmkv.encode(key, value);
                }
            } else {
                if (null != spEditor) {
                    spEditor.putBoolean(key, value);
                }
            }
            return this;
        }

        public Editor remove(String key) {
            if (mMMKVEnabled) {
                if (null != mmkv) {
                    mmkv.removeValueForKey(key);
                }
            } else {
                if (null != spEditor) {
                    spEditor.remove(key);
                }
            }
            return this;
        }

        public Editor clear() {
            if (mMMKVEnabled) {
                if (null != mmkv) {
                    mmkv.clearAll();
                }
            } else {
                if (null != spEditor) {
                    spEditor.clear();
                }
            }
            return this;
        }

        /**
         * 无实际意义,只是为了适配以前已经调用了commit的旧的方式
         */
        public boolean commit() {
            if (!mMMKVEnabled) {
                if (null != spEditor) {
                    return spEditor.commit();
                }
            }
            return true;
        }

        /**
         * 无实际意义,只是为了适配以前已经调用了apply的旧的方式
         */
        public void apply() {
            if (!mMMKVEnabled) {
                if (null != spEditor) {
                    spEditor.apply();
                }
            }
        }
    }

    public BluedSharedPreferences.Editor edit() {
        if (!mMMKVEnabled) {
            spEditor = spData.edit();
        }
        return new Editor();
    }

    @Nullable
    public String getString(String key, @Nullable String defValue) {
        if (mMMKVEnabled) {
            if (null != mmkv) {
                return mmkv.getString(key, defValue);
            }
        } else {
            if (null != spData) {
                return spData.getString(key, defValue);
            }
        }
        return defValue;
    }

    @Nullable
    Set<String> getStringSet(String key, @Nullable Set<String> defValues) {
        if (mMMKVEnabled) {
            if (null != mmkv) {
                return mmkv.getStringSet(key, defValues);
            }
        } else {
            if (null != spData) {
                return spData.getStringSet(key, defValues);
            }
        }
        return defValues;
    }

    public int getInt(String key, int defValue) {
        if (mMMKVEnabled) {
            if (null != mmkv) {
                return mmkv.getInt(key, defValue);
            }
        } else {
            if (null != spData) {
                return spData.getInt(key, defValue);
            }
        }
        return defValue;
    }

    public long getLong(String key, long defValue) {
        if (mMMKVEnabled) {
            if (null != mmkv) {
                return mmkv.getLong(key, defValue);
            }
        } else {
            if (null != spData) {
                return spData.getLong(key, defValue);
            }
        }
        return defValue;
    }

    public float getFloat(String key, float defValue) {
        if (mMMKVEnabled) {
            if (null != mmkv) {
                return mmkv.getFloat(key, defValue);
            }
        } else {
            if (null != spData) {
                return spData.getFloat(key, defValue);
            }
        }
        return defValue;
    }

    public boolean getBoolean(String key, boolean defValue) {
        if (mMMKVEnabled) {
            if (null != mmkv) {
                return mmkv.getBoolean(key, defValue);
            }
        } else {
            if (null != spData) {
                return spData.getBoolean(key, defValue);
            }
        }
        return defValue;
    }

    public boolean contains(String key) {
        if (mMMKVEnabled) {
            if (null != mmkv) {
                return mmkv.containsKey(key);
            }
        } else {
            if (null != spData) {
                return spData.contains(key);
            }
        }
        return false;
    }

}

写到最后

最后,最重要的就是MMKV的缺点,迁移到MMKV是不可逆操作,一定要慎重。

更多内容戳这里(整理好的各种文集)

上一篇下一篇

猜你喜欢

热点阅读