Android實(shí)現(xiàn)多進(jìn)程安全的SharedPreferences

背景

由于app可能有多個進(jìn)程着饥,因此在某些場景下,就要進(jìn)程間相互同步狀態(tài)表蝙,避免多個進(jìn)程各做各的,但數(shù)據(jù)不同步恐仑,導(dǎo)致產(chǎn)生異常灰署。

方案

目前認(rèn)為 Android 平臺目前有這樣幾個方案:

  1. 使用微信MMKV判帮,微信開源的MMKV是支持多進(jìn)程同步的,開發(fā)app的話推薦使用溉箕,不過對于開發(fā)SDK避免使用第三方代碼的原則晦墙,不推薦用。
  2. 使用ContentProvider 包裹 Sp ,其他進(jìn)程使用的時候肴茄,通過ContentProvider來訪問Sp晌畅,可以實(shí)現(xiàn)多進(jìn)程數(shù)據(jù)同步,不好的就是需要額外注冊組件寡痰。目前很多都是用這種方式踩麦。
  3. 使用廣播,可以實(shí)現(xiàn)狀態(tài)同步氓癌,不過即時性較差谓谦,不能毫秒級同步,安全方面也會一些問題存在贪婉,另外一個就是反粥,一對多同步的時候還好,但多對多同步的時候還是不能保證疲迂,同樣也需要額外注冊組件才顿。
  4. socket,類似廣播尤蒿,需要每個進(jìn)程都維護(hù)一個套接字服務(wù)郑气,同樣有著多對多同步難和數(shù)據(jù)安全的問題。
  5. 使用文件+文件鎖腰池,文件用來存數(shù)據(jù)尾组,文件鎖用來保證每次只有一個進(jìn)程在訪問這個文件,通過這樣保證數(shù)據(jù)的同步示弓。

嘗試實(shí)現(xiàn)

綜合看來讳侨,廣播方案是最容易的,不過它存在多對多無法同步的問題奏属,而文件鎖方案是可以滿足多對多的跨跨,數(shù)據(jù)安全基于文件。socket方案pass囱皿,同樣無法解決多對多的問題勇婴。

因此使用文件鎖方式:

下面是實(shí)現(xiàn)過程:

數(shù)據(jù)結(jié)構(gòu)

內(nèi)存映射文件忱嘹。

內(nèi)存:
HashMap<String,Object> value;
int mid;(標(biāo)識版本號)

文件:
前4字節(jié):mid(存mid)
后面所有字節(jié)(存value)

0-4字節(jié)(mid)
- 讀:共享鎖
- 寫:獨(dú)占鎖

4-end字節(jié)
- 讀:共享鎖
- 寫:獨(dú)占鎖

內(nèi)存map 轉(zhuǎn)換 文件的方式: map - json - 文件

寫數(shù)據(jù)過程

寫數(shù)據(jù)過程首先保證同一個文件,只有一個進(jìn)程在寫耕渴,使用FileLock實(shí)現(xiàn)這一點(diǎn):

寫數(shù)據(jù)的過程保證沒有進(jìn)程在讀,也沒有進(jìn)程在寫.因此獲得獨(dú)占鎖,偽代碼.

獲取 0 - end 位置的獨(dú)占鎖
內(nèi)存mid=mid+1
寫入mid到文件的 0 - 4 位置
內(nèi)存map -> file 寫入 5 - end 位置
釋放 0 - end 位置的獨(dú)占鎖

讀數(shù)據(jù)過程

讀數(shù)據(jù)過程要保證現(xiàn)在沒有進(jìn)程在寫德谅,我就可以讀數(shù)據(jù)了,而讀數(shù)據(jù)和讀數(shù)據(jù)直接是不需要互斥的萨螺,因此窄做,讀數(shù)據(jù)的時候,獲取共享鎖慰技。

偽代碼如下:

獲取0-4位置共享鎖

讀取mid

if(mid!=當(dāng)前內(nèi)存mid){
    獲取5-end共享鎖
    同步 file - > map
    釋放5-end共享鎖
}

返回?cái)?shù)值

釋放0-4位置共享鎖

待優(yōu)化

map - json - file 轉(zhuǎn)換的時候目前是整個替換,性能這塊隨著存儲的數(shù)據(jù)增多轉(zhuǎn)換處理的數(shù)據(jù)也將會增多,這塊還需要優(yōu)化.

多線程這塊由于是有文件鎖保護(hù),因此是安全的,但如果在非多進(jìn)程訪問的時候,這塊性能是很低的.

2019年9月20日19:41:28補(bǔ)充

由于之前map - json - file 轉(zhuǎn)換的時候目前是整個替換,隨著數(shù)據(jù)的增大,讀寫速度將顯著提升,因此從這一塊做了優(yōu)化.方法為根據(jù)數(shù)據(jù)的大小,給文件分成多塊.每個分塊文件都比較小,滿足了映射文件讀寫效率的問題,同時每個分塊文件單獨(dú)持有一個鎖,不互斥,提升性能

以下是自定義SharedPreferences源代碼(2019年9月26日15:09:55)

import android.content.Context;
import android.content.SharedPreferences;
import android.os.Environment;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.Message;
import android.util.Log;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Map;
import java.util.Queue;
import java.util.Set;


public final class SysnKV implements SharedPreferences {
    private static final String TAG = "SysnKV";

    private static final String DEF_NAME = "sysn_kv";
    private static final String SUFFIX = ".skv";
    /**
     * 默認(rèn)200kb
     * <p>
     * 分塊存儲文件最大值,超過這個值就加一塊
     */
    private int mMaxBlockSize = 1024 * 10;
    private final Context context;

    private String name = "def_sysnkv";
    private ArrayList<Block> mBlockList;

    private Queue<Editor> mEditorQueue;
    private Handler mHandler;

    public SysnKV(Context context) {
        this(context, DEF_NAME);
    }


    public SysnKV(Context context, String name) {
        this.name = name;
        this.context = context;
        mBlockList = new ArrayList<>();
        try {
            for (int i = 0; ; i++) {
                String path = getBlockFile(context, name, i);
                File blockFile = new File(path);
                if (blockFile.exists() && blockFile.isFile()) {
                    Block block = null;

                    block = new Block(blockFile);
                    mBlockList.add(block);

                } else {
                    break;
                }
            }

            if (mBlockList.size() == 0) {
                String path = getBlockFile(context, name, mBlockList.size());
                Block block = new Block(new File(path));
                mBlockList.add(block);
            }

            mEditorQueue = new LinkedList<>();
            HandlerThread thread = new HandlerThread("SysnKV");
            thread.start();
            mHandler = new Handler(thread.getLooper(), new Work());
        } catch (Throwable e) {
            //1.文件禁止訪問
            //2.無法創(chuàng)建文件
            e.printStackTrace();
        }
    }

    private String getBlockFile(Context context, String name, int num) {
//        String dir = context.getFilesDir().getAbsolutePath()
//                .concat(File.separator);
        String dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getAbsolutePath()
                .concat(File.separator).concat("testSysnP/");
        return dir.concat(name).concat(String.valueOf(num)).concat(name.indexOf('.') != -1 ? "" : SUFFIX);
    }

    @Override
    public Map<String, ?> getAll() {
        Map<String, Object> mValue = new HashMap<>();
        for (Block block : mBlockList) {
            mValue.putAll(block.getValue());
        }
        return mValue;
    }


    @Override
    public String getString(String key, String defValue) {
        try {
            for (Block block : mBlockList) {
                String o = (String) block.getValue().get(key);
                if (o != null) {
                    return o;
                }
            }
        } catch (Throwable e) {
            e.printStackTrace();
        }
        return defValue;
    }


    @Override
    public Set<String> getStringSet(String key, Set<String> defValues) {
        try {
            for (Block block : mBlockList) {
                Object array = block.getValue().get(key);
                //hashmap 存完了json解析出來是jsonarray
                if (array instanceof Set) {
                    return (Set<String>) array;
                } else if (array instanceof JSONArray) {
                    if (array == null) {
                        return defValues;
                    }
                    JSONArray jsonArray = (JSONArray) array;
                    Set<String> strings;
                    strings = new HashSet<>();
                    for (int i = 0; i < jsonArray.length(); i++) {
                        strings.add((String) jsonArray.opt(i));
                    }
                    return strings;
                }

            }
        } catch (Throwable e) {
            e.printStackTrace();
        }
        return defValues;
    }

    @Override
    public int getInt(String key, int defValue) {
        try {
            for (Block block : mBlockList) {
                Object val = block.getValue().get(key);
                if (val != null) {
                    return (int) val;
                }
            }
        } catch (Throwable e) {
            e.printStackTrace();
        }
        return defValue;
    }

    @Override
    public long getLong(String key, long defValue) {
        try {
            for (Block block : mBlockList) {
                Object val = block.getValue().get(key);
                if (val != null) {
                    //java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.
                    if (val instanceof Integer) {
                        return (int) val;
                    } else {
                        return (long) val;
                    }
                }
            }
        } catch (Throwable e) {
            e.printStackTrace();
        }
        return defValue;
    }

    @Override
    public float getFloat(String key, float defValue) {
        try {
            for (Block block : mBlockList) {
                Object val = block.getValue().get(key);
                if (val != null) {
                    if (val instanceof Double) {
                        double d = (double) val;
                        return (float) d;
                    } else {
                        return (float) val;
                    }
                }
            }
        } catch (Throwable e) {
            e.printStackTrace();
        }
        return defValue;
    }

    @Override
    public boolean getBoolean(String key, boolean defValue) {
        try {
            for (Block block : mBlockList) {
                Object val = block.getValue().get(key);
                if (val != null) {
                    return (boolean) val;
                }
            }
        } catch (Throwable e) {
            e.printStackTrace();
        }
        return defValue;
    }

    @Override
    public boolean contains(String key) {
        for (Block block : mBlockList) {
            Object o = block.getValue().get(key);
            if (o != null) {
                return true;
            }
        }
        return false;
    }

    @Override
    public Editor edit() {
        return new EditorImpl();
    }

    @Override
    @Deprecated
    public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {

    }

    @Override
    @Deprecated
    public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {

    }

    final class EditorImpl implements Editor {
        Map<String, Object> addMap = new HashMap<>();
        Set<String> deleteKey = new HashSet<>();
        boolean isClear;

        @Override
        public Editor putString(String key, String value) {
            addMap.put(key, value);
            return this;
        }

        @Override
        public Editor putStringSet(String key, Set<String> values) {
            addMap.put(key, values);
            return this;
        }

        @Override
        public Editor putInt(String key, int value) {
            addMap.put(key, value);
            return this;
        }

        @Override
        public Editor putLong(String key, long value) {
            addMap.put(key, value);
            return this;
        }

        @Override
        public Editor putFloat(String key, float value) {
            addMap.put(key, value);
            return this;
        }

        @Override
        public Editor putBoolean(String key, boolean value) {
            addMap.put(key, value);
            return this;
        }

        @Override
        public Editor remove(String key) {
            deleteKey.add(key);
            addMap.remove(key);
            return this;
        }

        @Override
        public Editor clear() {
            isClear = true;
            deleteKey.clear();
            addMap.clear();
            return this;
        }

        @Override
        public boolean commit() {
            if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
                //在主線程操作可能會因?yàn)榈却募ianr
                Log.w(TAG, "在主線程操作,最好使用apply防止ANR");
            }
            boolean result = false;

            try {

                for (int i = 0; i < mBlockList.size(); i++) {
                    boolean isMdf = false;

                    Block block = mBlockList.get(i);
                    if (isClear) {
                        block.getValue().clear();
                        isMdf = true;
                    } else {
                        for (String key : deleteKey) {
                            block.sync();
                            Object value = block.getValue().remove(key);
                            if (value != null) {
                                deleteKey.remove(key);
                                isMdf = true;
                            }
                        }
                        if (block.getSize() > mMaxBlockSize) {
                            continue;
                        }

                    }
                    if (!addMap.isEmpty() && block.getSize() < mMaxBlockSize) {
                        block.getValue().putAll(addMap);
                        addMap.clear();
                        isMdf = true;
                    }
                    if (isMdf) {
                        result = block.write();
                    }
                }

                if (!addMap.isEmpty()) {
                    String path = getBlockFile(context, name, mBlockList.size());
                    Block block = new Block(new File(path));
                    mBlockList.add(block);
                    block.getValue().putAll(addMap);
                    result = block.write();
                }


            } catch (Throwable e) {
                e.printStackTrace();
            }

            return result;
        }

        @Override
        public void apply() {
            SysnKV.this.mEditorQueue.add(this);
            Message.obtain(SysnKV.this.mHandler, Work.WHAT_APPLY, SysnKV.this.mEditorQueue);
        }
    }

    final static class Block {
        private Map<String, Object> value;
        private File mFile;
        //版本id
        private Integer mId;
        private RandomAccessFile mAccessFile;
        private FileChannel mChannel;


        public Block(File file) throws IOException {
            this.mFile = file;
            if (!mFile.exists() || !mFile.isFile()) {
                File dir = mFile.getParentFile();
                if (!dir.exists()) {
                    dir.mkdirs();
                }
                mFile.createNewFile();
            }
            value = new HashMap<>();
        }

        public Map<String, Object> getValue() {
            sync();
            return value;
        }

        public long getSize() {
            return mFile.length();
        }

        public boolean write() {
            return doMap2File();
        }

        private void sync() {
            ByteBuffer buffer = null;
            FileLock lock = null;
            try {
                //讀mid
                lock = lock(0, 4, true);
                buffer = ByteBuffer.allocate(4);
                int size = mChannel.read(buffer, 0);
                unLock(lock);
                if (size == 4) {
                    buffer.flip();
                    //比較mid
                    int mid = buffer.getInt();
                    //當(dāng)前mid為空椭盏,沒同步過,同步吻商,mid不一致掏颊,同步
                    if (Block.this.mId == null || Block.this.mId != mid) {
                        doFile2Map();
                        //同步完成,更新mid
                        Block.this.mId = mid;
                    }
                }
            } catch (Throwable e) {
                //讀取mid出io異常
                unLock(lock);
                e.printStackTrace();
            }
            if (buffer != null) {
                buffer.clear();
            }
        }


        private FileLock lock(long position, long size, boolean shared) {
            try {
                if (mAccessFile == null || mChannel == null || !mChannel.isOpen()) {
                    mAccessFile = new RandomAccessFile(mFile, "rw");
                    mChannel = mAccessFile.getChannel();
                }
                if (mChannel != null && mChannel.isOpen()) {
                    size = Math.min(size, mAccessFile.length());
                    return mChannel.lock(position, size, shared);
                }
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
            return null;
        }

        private void unLock(FileLock lock) {
            if (lock != null) {
                try {
                    lock.release();
                    release();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                lock = null;
            }
        }

        private void release() {
            if (mChannel != null) {
                try {
                    mChannel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                mChannel = null;
            }
            if (mAccessFile != null) {
                try {
                    mAccessFile.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                mAccessFile = null;
            }
        }

        private void doFile2Map() {
            FileLock lock = lock(5, Long.MAX_VALUE, true);
            try {
                //前4位是mid,跳過
                mChannel.position(4);
                ByteBuffer buffer = ByteBuffer.allocate((int) (mChannel.size() - 4));

                int len = mChannel.read(buffer);
                if (len == -1) {
                    return;
                }
                buffer.flip();
                value.clear();
                JSONObject object = new JSONObject(Charset.forName("utf-8").decode(buffer).toString());
                for (Iterator<String> it = object.keys(); it.hasNext(); ) {
                    String k = it.next();
                    value.put(k, object.get(k));
                }
            } catch (IOException e) {
                // io 讀文件失敗,不用處理
                e.printStackTrace();
            } catch (JSONException e) {
                // json 解析錯誤,文件出錯,刪除這個文件
                unLock(lock);
                try {
                    mFile.delete();
                } catch (Exception e1) {
                    //刪除文件失敗,不處理
                    e1.printStackTrace();
                }
                e.printStackTrace();
                return;
            }
            unLock(lock);
        }

        private boolean doMap2File() {
            boolean result = false;
            FileLock lock = lock(0, Long.MAX_VALUE, false);
            try {
                JSONObject object = new JSONObject(value);
                byte[] bt = object.toString(0).getBytes(Charset.forName("utf-8"));
                ByteBuffer buf = ByteBuffer.allocate(bt.length + 4);
                if (mId == null) {
                    mId = Integer.MIN_VALUE;
                } else {
                    mId = (mId + 1) % (Integer.MAX_VALUE - 10);
                }
                buf.putInt(mId);
                buf.put(bt);
                buf.flip();
                //前4位是mid
                mChannel.position(0);
                while (buf.hasRemaining()) {
                    mChannel.write(buf);
                }
                //刪除后面的文件
                mChannel.truncate(4 + bt.length);
                mChannel.force(true);
                result = true;
            } catch (IOException e) {
                //todo 寫入文件失敗,用備份文件方式處理
                e.printStackTrace();
            } catch (JSONException e) {
                //map轉(zhuǎn)json串會出異常?先不處理,最多就是數(shù)據(jù)存不進(jìn)去
                //可能map存儲了含有特殊字符串的value會有這個異常.
                e.printStackTrace();
            }
            unLock(lock);
            return result;
        }

    }

    final static class Work implements Handler.Callback {

        public final static int WHAT_APPLY = 1;
        public final static int WHAT_INIT_SYSN = 2;

        @Override
        public boolean handleMessage(Message msg) {
            switch (msg.what) {
                case WHAT_APPLY:
                    Queue<Editor> queue = null;
                    if (msg.obj instanceof Queue) {
                        queue = (Queue<Editor>) msg.obj;
                    }
                    if (queue == null) {
                        break;
                    }
                    while (!queue.isEmpty()) {
                        Editor editor = queue.poll();
                        editor.commit();
                    }
                    break;
                case WHAT_INIT_SYSN:

                    break;
                default:
                    break;
            }
            return true;
        }
    }
}

測試時間記錄

14個進(jìn)程并發(fā)

讀寫數(shù)據(jù)140次 耗時 2.40s 平均每次讀寫 16ms

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末艾帐,一起剝皮案震驚了整個濱河市乌叶,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌柒爸,老刑警劉巖准浴,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異捎稚,居然都是意外死亡乐横,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進(jìn)店門今野,熙熙樓的掌柜王于貴愁眉苦臉地迎上來葡公,“玉大人,你說我怎么就攤上這事条霜〈呤玻” “怎么了?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵宰睡,是天一觀的道長蒲凶。 經(jīng)常有香客問我,道長夹厌,這世上最難降的妖魔是什么豹爹? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任裆悄,我火速辦了婚禮矛纹,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘光稼。我一直安慰自己或南,他們只是感情好孩等,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著采够,像睡著了一般肄方。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上蹬癌,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天权她,我揣著相機(jī)與錄音,去河邊找鬼逝薪。 笑死隅要,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的董济。 我是一名探鬼主播步清,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼虏肾!你這毒婦竟也來了廓啊?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤封豪,失蹤者是張志新(化名)和其女友劉穎谴轮,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體吹埠,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡书聚,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了藻雌。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片雌续。...
    茶點(diǎn)故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖胯杭,靈堂內(nèi)的尸體忽然破棺而出驯杜,到底是詐尸還是另有隱情,我是刑警寧澤做个,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布鸽心,位于F島的核電站,受9級特大地震影響居暖,放射性物質(zhì)發(fā)生泄漏顽频。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一太闺、第九天 我趴在偏房一處隱蔽的房頂上張望糯景。 院中可真熱鬧,春花似錦、人聲如沸蟀淮。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽怠惶。三九已至涨缚,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間策治,已是汗流浹背脓魏。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留通惫,地道東北人轧拄。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像讽膏,于是被迫代替她去往敵國和親檩电。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評論 2 345