Android实现多进程安全的SharedPreferences

背景

由于app可能有多个进程,因此在某些场景下,就要进程间相互同步状态,避免多个进程各做各的,但数据不同步,导致产生异常。

方案

目前认为 Android 平台目前有这样几个方案:

  1. 使用微信MMKV,微信开源的MMKV是支持多进程同步的,开发app的话推荐使用,不过对于开发SDK避免使用第三方代码的原则,不推荐用。
  2. 使用ContentProvider 包裹 Sp ,其他进程使用的时候,通过ContentProvider来访问Sp,可以实现多进程数据同步,不好的就是需要额外注册组件。目前很多都是用这种方式。
  3. 使用广播,可以实现状态同步,不过即时性较差,不能毫秒级同步,安全方面也会一些问题存在,另外一个就是,一对多同步的时候还好,但多对多同步的时候还是不能保证,同样也需要额外注册组件。
  4. socket,类似广播,需要每个进程都维护一个套接字服务,同样有着多对多同步难和数据安全的问题。
  5. 使用文件+文件锁,文件用来存数据,文件锁用来保证每次只有一个进程在访问这个文件,通过这样保证数据的同步。

尝试实现

综合看来,广播方案是最容易的,不过它存在多对多无法同步的问题,而文件锁方案是可以满足多对多的,数据安全基于文件。socket方案pass,同样无法解决多对多的问题。

因此使用文件锁方式:

下面是实现过程:

数据结构

内存映射文件。

内存:
HashMap value;
int mid;(标识版本号)

文件:
前4字节:mid(存mid)
后面所有字节(存value)

0-4字节(mid)
- 读:共享锁
- 写:独占锁

4-end字节
- 读:共享锁
- 写:独占锁

内存map 转换 文件的方式: map - json - 文件

写数据过程

写数据过程首先保证同一个文件,只有一个进程在写,使用FileLock实现这一点:

写数据的过程保证没有进程在读,也没有进程在写.因此获得独占锁,伪代码.

获取 0 - end 位置的独占锁
内存mid=mid+1
写入mid到文件的 0 - 4 位置
内存map -> file 写入 5 - end 位置
释放 0 - end 位置的独占锁

读数据过程

读数据过程要保证现在没有进程在写,我就可以读数据了,而读数据和读数据直接是不需要互斥的,因此,读数据的时候,获取共享锁。

伪代码如下:

获取0-4位置共享锁

读取mid

if(mid!=当前内存mid){
    获取5-end共享锁
    同步 file - > map
    释放5-end共享锁
}

返回数值

释放0-4位置共享锁

待优化

map - json - file 转换的时候目前是整个替换,性能这块随着存储的数据增多转换处理的数据也将会增多,这块还需要优化.

多线程这块由于是有文件锁保护,因此是安全的,但如果在非多进程访问的时候,这块性能是很低的.

2019年9月20日19:41:28补充

由于之前map - json - file 转换的时候目前是整个替换,随着数据的增大,读写速度将显著提升,因此从这一块做了优化.方法为根据数据的大小,给文件分成多块.每个分块文件都比较小,满足了映射文件读写效率的问题,同时每个分块文件单独持有一个锁,不互斥,提升性能

以下是自定义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";
    /**
     * 默认200kb
     * 

* 分块存储文件最大值,超过这个值就加一块 */ private int mMaxBlockSize = 1024 * 10; private final Context context; private String name = "def_sysnkv"; private ArrayList mBlockList; private Queue 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.无法创建文件 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 getAll() { Map 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 getStringSet(String key, Set defValues) { try { for (Block block : mBlockList) { Object array = block.getValue().get(key); //hashmap 存完了json解析出来是jsonarray if (array instanceof Set) { return (Set) array; } else if (array instanceof JSONArray) { if (array == null) { return defValues; } JSONArray jsonArray = (JSONArray) array; Set 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 addMap = new HashMap<>(); Set 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 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()) { //在主线程操作可能会因为等待文件锁anr 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 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 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(); //当前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 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转json串会出异常?先不处理,最多就是数据存不进去 //可能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 queue = null; if (msg.obj instanceof Queue) { queue = (Queue) 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个进程并发

读写数据140次 耗时 2.40s 平均每次读写 16ms

你可能感兴趣的:(Android实现多进程安全的SharedPreferences)