最近因工作需求,需要Android app中的SharedPreferences的数据共享到其他进程,研究很两天,终于得到了解决方案,感动不已,分享一下。
刚开始在网上找解决方案,似乎有这个需求的人太少,Google了半天,只看到一个方案。
通过MODE_MULTI_PROCESS属性使用SharedPreferences就可以解决不同进程之间不能共享数据的问题了,但SQA总是反馈一些随机但出现频率比较大的bug,比如在使用过程中没有清除程序数据的前提下,会出现欢迎界面和操作指引,这是通过保存在SharedPreferences的标志来判断用户是否是第一次启动程序的,分析发现保存在SharedPreferences中的数据丢失了,但代码中并没有去清除这些数据,所以推测可能是不同进程同一时间对SharedPreferences操作导致的,经验证确实如此,去掉多进程就不会再出现这个问题了。
最后团队分析讨论抛弃了这个方案,转而考虑使用ContentProvider去实现跨进程共享。然而问题接踵而至,即绝大部分使用ContentProvider的情况都是结合了Sqlite。ContentProvider使用要重写几个方法。
特别是query方法,因为你要查询数据,那么基本上必须要重写它。这里就是做了一件事,把你的数据从某个地方取出来,通过Cursor抛到另一个进程。所以,Sqlite就很方便啊,它提供了许多的api最后都可以去到Cursor。然而,SharedPreferences并没有这么方便。于是要共享SharedPreferences,要怎么做呢?简单的思路就是读取里面的数据写到Cursor里面。因此我们要去自定义一个Cursor。
一、如何处理SharedPreferences的数据
我在这两天的研究中,尝试了几种自定义Cursor的办法,从最顶层的实现Cursor,到参考SqliteCursor之后,决定继承AbstractWindowedCursor这个类去自定义一个Cursor。接下来我们看下代码。
以上是必须要重写的方法,来分别介绍一下。
1、构造方法
public SharedPreferencesCursor(Context context, String sharedFileName, String[] keys) {
mSharedFileName = sharedFileName;
sharedPreferences = context.getSharedPreferences(sharedFileName,
Context.MODE_PRIVATE);
Set
if (keys == null) {//如果没有指定要查询的key值列表,就查询所有
keys = new String[sets.size()];
int i = 0;
for (String key : sets) {
keys[i++] = key;
}
}
mColumns = keys;
}
此处第三个入参模拟数据库表中指定列的查询,对应你要查询的key。因为我们知道一个SharedPreferences里面有多个key,因此通过这种方式可以指定要查询的key。
2、游标移动操作
public boolean onMove(int oldPosition, int newPosition) {
if (mWindow == null
|| newPosition < mWindow.getStartPosition()
|| newPosition >= (mWindow.getStartPosition() + mWindow
.getNumRows())) {
fillWindow(newPosition);
}
return true;
}
3、填充window
private void fillWindow(int requiredPos) {
clearOrCreateWindow(mSharedFileName);
try {
if (mCount == NO_COUNT) {
mCount = fillWindow(mWindow);
} else {
fillWindow(mWindow);
}
} catch (RuntimeException e) {
closeWindow();
throw e;
}
}
private int fillWindow(CursorWindow window) {
final int numColumns = getColumnCount();
window.clear();
window.setStartPosition(0);
window.setNumColumns(numColumns);
if (!window.allocRow()) {
return -1;
}
int i = 0;
for (String key : mColumns) {
boolean success = false;
final int type = getType(key);
switch (type) {
case Cursor.FIELD_TYPE_NULL:
success = window.putNull(0, i);
break;
case Cursor.FIELD_TYPE_INTEGER:
success = window
.putLong(sharedPreferences.getInt(key, 0), 0, i);
break;
case Cursor.FIELD_TYPE_FLOAT:
success = window.putDouble(sharedPreferences.getFloat(key, 0),
0, i);
break;
case Cursor.FIELD_TYPE_BLOB:
break;
default: // assume value is convertible to String
case Cursor.FIELD_TYPE_STRING:
final String value = sharedPreferences.getString(key, "");
success = value != null ? window.putString(value, 0, i)
: window.putNull(0, i);
break;
}
if (!success) {
window.freeLastRow();
}
i++;
}
return i;
}
这两步是实现数据共享的关键。什么是CursorWindow?当你了解了SqliteCursor的源码就会发现,我们通过Uri查询数据库所得到的数据集,保存在native层的CursorWindow中。CursorWindow的实质是共享内存的抽象,以实现跨进程数据共享。共享内存所采用的实现方式是文件映射。同理,我们通过Url获取SharedPreferences里面的数据,也要保存到CursorWindow中。
4、获取key对应的index
public int getColumnIndex(String columnName) {
if (mColumnNameMap == null) {
String[] columns = mColumns;
int columnCount = columns.length;
HashMap
columnCount, 1);
for (int i = 0; i < columnCount; i++) {
map.put(columns[i], i);
}
mColumnNameMap = map;
}
Integer integer = mColumnNameMap.get(columnName);
if (integer != null) {
return integer.intValue();
}
return -1;
}
5、创建或者关闭window
private void clearOrCreateWindow(String name) {
if (mWindow == null) {
mWindow = new CursorWindow(name);
} else {
mWindow.clear();
}
}
private void closeWindow() {
if (mWindow != null) {
mWindow.close();
mWindow = null;
}
}
到此,我们自定义的Cursor就可以使用了。SharedPreferences是可以理解为键值对映射的Map,它的形式和数据库的表是不一样的,所以我把每个key看作是表的列,因此这样的“表”其实只有一行数据,永远只有一行数据。因此在取数据的时候,只需要Cursor.moveToFirst(),就可以取数据了。
二、如何通过ContentProvider共享数据
接下来我们要去自定义一个ContentProvider。
根据实际情况去重写CRUD方法。这里解释下查询方法,正如前面解释的SharedPreferencesCursor的构造函数一样,我们把用在数据库中指定列的projection入参作为指定key的入参。如果要实现数据同步更新,动态加载,需要对cursor设置notification的URI。
三、如何同步更新数据并动态加载
跨进程动态更新加载数据需要使用到很厉害的一个类,LoadManager。
简单介绍一下android的Loaders机制,Loaders,装载机,适用于Android3.0以及更高的版本,它提供了一套在UI的主线程中异步加载数据的框架。使用Loaders可以非常简单的在Activity或者Fragment中异步加载数据,一般适用于大量的数据查询,或者需要经常修改并及时展示的数据显示到UI上,这样可以避免查询数据的时候,造成UI主线程的卡顿。
Loaders有以下特点:
1、SimpleCursorAdapter SqliteException no such column: _id 异常解决方案:
我们项目的数据库使用的是开源库Litepal,在共享数据库的时候遇到了上面这个异常。这个异常主要由于我们使用Litepal导致的。为什么是这样呢?因为Litepal默认主键名是id,而非_id,并且不支持修改。而SimpleCursorAdapter的父类CursorAdapter里面,有几处代码通过getColumnIndexOrThrow("_id")去获取主键的id值,所以导致二者不匹配。那么要么修改Litepal,要么修改CursorAdapter,供君自由选择。这边提供的方案是修改SimpleCursorAdapter相关的几个类,拷贝源码,就修改了两处,非常方便。
通过
这个this是指LoaderCallbacks