背景
由于app可能有多個進(jìn)程着饥,因此在某些場景下,就要進(jìn)程間相互同步狀態(tài)表蝙,避免多個進(jìn)程各做各的,但數(shù)據(jù)不同步恐仑,導(dǎo)致產(chǎn)生異常灰署。
方案
目前認(rèn)為 Android 平臺目前有這樣幾個方案:
- 使用微信MMKV判帮,微信開源的MMKV是支持多進(jìn)程同步的,開發(fā)app的話推薦使用溉箕,不過對于開發(fā)SDK避免使用第三方代碼的原則晦墙,不推薦用。
- 使用ContentProvider 包裹 Sp ,其他進(jìn)程使用的時候肴茄,通過ContentProvider來訪問Sp晌畅,可以實(shí)現(xiàn)多進(jìn)程數(shù)據(jù)同步,不好的就是需要額外注冊組件寡痰。目前很多都是用這種方式踩麦。
- 使用廣播,可以實(shí)現(xiàn)狀態(tài)同步氓癌,不過即時性較差谓谦,不能毫秒級同步,安全方面也會一些問題存在贪婉,另外一個就是反粥,一對多同步的時候還好,但多對多同步的時候還是不能保證疲迂,同樣也需要額外注冊組件才顿。
- socket,類似廣播尤蒿,需要每個進(jìn)程都維護(hù)一個套接字服務(wù)郑气,同樣有著多對多同步難和數(shù)據(jù)安全的問題。
- 使用文件+文件鎖腰池,文件用來存數(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