SharedPreferences跨进程共享数据研究

最近因工作需求,需要Android app中的SharedPreferences的数据共享到其他进程,研究很两天,终于得到了解决方案,感动不已,分享一下。


刚开始在网上找解决方案,似乎有这个需求的人太少,Google了半天,只看到一个方案。

通过MODE_MULTI_PROCESS属性使用SharedPreferences就可以解决不同进程之间不能共享数据的问题了,但SQA总是反馈一些随机但出现频率比较大的bug,比如在使用过程中没有清除程序数据的前提下,会出现欢迎界面和操作指引,这是通过保存在SharedPreferences的标志来判断用户是否是第一次启动程序的,分析发现保存在SharedPreferences中的数据丢失了,但代码中并没有去清除这些数据,所以推测可能是不同进程同一时间对SharedPreferences操作导致的,经验证确实如此,去掉多进程就不会再出现这个问题了。

最后团队分析讨论抛弃了这个方案,转而考虑使用ContentProvider去实现跨进程共享。然而问题接踵而至,即绝大部分使用ContentProvider的情况都是结合了Sqlite。ContentProvider使用要重写几个方法。

SharedPreferences跨进程共享数据研究_第1张图片

特别是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 sets = sharedPreferences.getAll().keySet();
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 map = new 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有以下特点:

  • 可以适用于Activity和Fragment。
  • 可以提供异步的方式加载数据。
  • 监听数据源,当数据改变的时候,将新的数据发布到UI上。
  • Loaders使用Cursor加载数据,在更改Cursor的时候,会自动重新连接到最后配置的Cursor中读取数据,因此不需要重新查询数据。
具体的机制读者请自行查看相关的文档。这边要实现SharedPreferences数据变更动态更新,只需要简单的一行代码。
第一行代码是保存SharedPreferences数据,第二行代码就是获取ContentResolver并传入SYNC_SIGNAL_URI通知刷新。这个URI一定要和ContentProvider里面配置的n
notificationUri是一致的。到此,基本上你就可以实现SharedPreferences跨进程共享数据了。

接下来你需要在跨进程获取数据的类里面去实现LoaderCallbacks。重写三个方法。
SharedPreferences跨进程共享数据研究_第2张图片
onCreateLoader  顾名思义,就是要创建CursorLoader,配置你的URI,要查询的条件。
onLoadFinished  方法,当第一次获取到cursor或者cursror数据源有变化的时候,就会回调该方法,在这里可以实现更新数据,动态加载。
onLoaderReset   主要是用的你的loader变化的时候。
好了,以上就是SharedPreferences跨进程共享数据的步骤。回顾起来,发现其实不难,但是这两天在研究的时候,网上没有对应的解决方案,没有入手点,只能类比数据库去研究,着实有点心塞,不过功能总算实现了,也是有点开心。

四、异常解决方案

1、SimpleCursorAdapter SqliteException no such column: _id 异常解决方案:

我们项目的数据库使用的是开源库Litepal,在共享数据库的时候遇到了上面这个异常。这个异常主要由于我们使用Litepal导致的。为什么是这样呢?因为Litepal默认主键名是id,而非_id,并且不支持修改。而SimpleCursorAdapter的父类CursorAdapter里面,有几处代码通过getColumnIndexOrThrow("_id")去获取主键的id值,所以导致二者不匹配。那么要么修改Litepal,要么修改CursorAdapter,供君自由选择。这边提供的方案是修改SimpleCursorAdapter相关的几个类,拷贝源码,就修改了两处,非常方便。


通过

这个this是指LoaderCallbacks回调接口,必须实现,并重写以下方法

SharedPreferences跨进程共享数据研究_第3张图片

你可能感兴趣的:(源码学习,Android学习)