当前位置: 移动技术网 > 移动技术>移动开发>Android > Android开发中多进程共享数据简析

Android开发中多进程共享数据简析

2019年07月24日  | 移动技术网移动技术  | 我要评论

 背景 最近在工作中遇到一个需求,需要在接收到推送的时候将推送获得的数据存起来,以供app启动时使用。我们会认为这不是so easy吗?只要把数据存到sharedpreferences中,然后让app打开同一个sharedpreferences读取数据就可以了。但是在实际的测试中,我们发现推送进程存入的数据,并不能在app进程中获得。所以这是为什么呢,也许聪明的读者从我们上面的陈述中已经发现了原因,因为我们有两个进程,推送进程负责将推送数据存入,而app进程负责读取,但是正是由于是两个进程,如果它们同时存在,它们各自在内存中保持了自己的sp对象和数据,在推送进程中的存入并不能在app进程体现出来,并且可能会被app进程刷掉更改的数据。那么我们怎么做才能让这两边共享数据呢?请看下面陈述。

一、多进程支持的sharedpreferences(不推荐)
我们原来的做法是使用sharedpreferences, 自然而然想到,sharedpreferences 在mode_private mode_public 之外其实还可以设置多进程的flag ———— mode_multi_process

复制代码 代码如下:
sharedpreferences myprefs = context.getsharedpreferences(my_file_name, context.mode_multi_process | context.mode_private);

一旦我们设置了这个flag,每次调用context.getsharedpreferences 的时候系统会重新从sp文件中读入数据,因此我们在使用的时候每次读取和存入都要使用context.getsharedpreferences 重新获取sp实例。即使是这样,由于sp本质上并不是多进程安全的,所以还是无法保证数据的同步,因此该方法我们并没有使用,我们也不推荐使用。

二、tray
如果sp不是多进程安全的,那么是否有多进程安全的,又有sp功能的第三方项目呢。答案是有的,tray——一个多进程安全的sharedpreferences,我们可以在github上找到它,如果是androidstudio,可以直接使用gradle引入,可谓是十分方便,如下是使用的代码,十分简单,没有apply commit,看起来比sp还要简单。

 // create a preference accessor. this is for global app preferences.
final apppreferences apppreferences = new apppreferences(getcontext()); // this preference comes for free from the library
// save a key value pair
apppreferences.put("key", "lorem ipsum");

// read the value for your key. the second parameter is a fallback (optional otherwise throws)
final string value = apppreferences.getstring("key", "default");
log.v(tag, "value: " + value); // value: lorem ipsum

// read a key that isn't saved. returns the default (or throws without default)
final string defaultvalue = apppreferences.getstring("key2", "default");
log.v(tag, "value: " + defaultvalue); // value: default

但是最终我们并没有选择使用它,主要的原因是它需要minsdk 为15,而我们是支持sdk14的,所以只能果断放弃了。

三、contentprovider
既然tray不支持sdk15以下的,那么我们是否可以使用tray的原理自己实现一个呢?在阅读tray的源码时我们发现其实它是在contentprovider的基础上做的,而contentprovider是android官方支持的多进程安全的。以下是使用contentprovider的一个例子。

 public class articlesprovider extends contentprovider { 
  private static final string log_tag = "shy.luo.providers.articles.articlesprovider"; 
 
  private static final string db_name = "articles.db"; 
  private static final string db_table = "articlestable"; 
  private static final int db_version = 1; 
 
  private static final string db_create = "create table " + db_table + 
              " (" + articles.id + " integer primary key autoincrement, " + 
              articles.title + " text not null, " + 
              articles.abstract + " text not null, " + 
              articles.url + " text not null);"; 
 
  private static final urimatcher urimatcher; 
  static { 
      urimatcher = new urimatcher(urimatcher.no_match); 
      urimatcher.adduri(articles.authority, "item", articles.item); 
      urimatcher.adduri(articles.authority, "item/#", articles.item_id); 
      urimatcher.adduri(articles.authority, "pos/#", articles.item_pos); 
  } 
 
  private static final hashmap<string, string> articleprojectionmap; 
  static { 
      articleprojectionmap = new hashmap<string, string>(); 
      articleprojectionmap.put(articles.id, articles.id); 
      articleprojectionmap.put(articles.title, articles.title); 
      articleprojectionmap.put(articles.abstract, articles.abstract); 
      articleprojectionmap.put(articles.url, articles.url); 
  } 
 
  private dbhelper dbhelper = null; 
  private contentresolver resolver = null; 
 
  @override 
  public boolean oncreate() { 
      context context = getcontext(); 
      resolver = context.getcontentresolver(); 
      dbhelper = new dbhelper(context, db_name, null, db_version); 
 
      log.i(log_tag, "articles provider create"); 
 
      return true; 
  } 
 
  @override 
  public string gettype(uri uri) { 
      switch (urimatcher.match(uri)) { 
      case articles.item: 
          return articles.content_type; 
      case articles.item_id: 
      case articles.item_pos: 
          return articles.content_item_type; 
      default: 
          throw new illegalargumentexception("error uri: " + uri); 
      } 
  } 
 
  @override 
  public uri insert(uri uri, contentvalues values) { 
      if(urimatcher.match(uri) != articles.item) { 
          throw new illegalargumentexception("error uri: " + uri); 
      } 
 
      sqlitedatabase db = dbhelper.getwritabledatabase(); 
 
      long id = db.insert(db_table, articles.id, values); 
      if(id < 0) { 
          throw new sqliteexception("unable to insert " + values + " for " + uri); 
      } 
 
      uri newuri = contenturis.withappendedid(uri, id); 
      resolver.notifychange(newuri, null); 
 
      return newuri; 
  } 
 
  @override 
  public int update(uri uri, contentvalues values, string selection, string[] selectionargs) { 
      sqlitedatabase db = dbhelper.getwritabledatabase(); 
      int count = 0; 
 
      switch(urimatcher.match(uri)) { 
      case articles.item: { 
          count = db.update(db_table, values, selection, selectionargs); 
          break; 
      } 
      case articles.item_id: { 
          string id = uri.getpathsegments().get(1); 
          count = db.update(db_table, values, articles.id + "=" + id 
                  + (!textutils.isempty(selection) ? " and (" + selection + ')' : ""), selectionargs); 
          break; 
      } 
      default: 
          throw new illegalargumentexception("error uri: " + uri); 
      } 
 
      resolver.notifychange(uri, null); 
 
      return count; 
  } 
 
  @override 
  public int delete(uri uri, string selection, string[] selectionargs) { 
      sqlitedatabase db = dbhelper.getwritabledatabase(); 
      int count = 0; 
 
      switch(urimatcher.match(uri)) { 
      case articles.item: { 
          count = db.delete(db_table, selection, selectionargs); 
          break; 
      } 
      case articles.item_id: { 
          string id = uri.getpathsegments().get(1); 
          count = db.delete(db_table, articles.id + "=" + id 
                  + (!textutils.isempty(selection) ? " and (" + selection + ')' : ""), selectionargs); 
          break; 
      } 
      default: 
          throw new illegalargumentexception("error uri: " + uri); 
      } 
 
      resolver.notifychange(uri, null); 
 
      return count; 
  } 
 
  @override 
  public cursor query(uri uri, string[] projection, string selection, string[] selectionargs, string sortorder) { 
      log.i(log_tag, "articlesprovider.query: " + uri); 
 
      sqlitedatabase db = dbhelper.getreadabledatabase(); 
 
      sqlitequerybuilder sqlbuilder = new sqlitequerybuilder(); 
      string limit = null; 
 
      switch (urimatcher.match(uri)) { 
      case articles.item: { 
          sqlbuilder.settables(db_table); 
          sqlbuilder.setprojectionmap(articleprojectionmap); 
          break; 
      } 
      case articles.item_id: { 
          string id = uri.getpathsegments().get(1); 
          sqlbuilder.settables(db_table); 
          sqlbuilder.setprojectionmap(articleprojectionmap); 
          sqlbuilder.appendwhere(articles.id + "=" + id); 
          break; 
      } 
      case articles.item_pos: { 
          string pos = uri.getpathsegments().get(1); 
          sqlbuilder.settables(db_table); 
          sqlbuilder.setprojectionmap(articleprojectionmap); 
          limit = pos + ", 1"; 
          break; 
      } 
      default: 
          throw new illegalargumentexception("error uri: " + uri); 
      } 
 
      cursor cursor = sqlbuilder.query(db, projection, selection, selectionargs, null, null, textutils.isempty(sortorder) ? articles.default_sort_order : sortorder, limit); 
      cursor.setnotificationuri(resolver, uri); 
 
      return cursor; 
  } 

  @override 
  public bundle call(string method, string request, bundle args) { 
      log.i(log_tag, "articlesprovider.call: " + method); 
 
      if(method.equals(articles.method_get_item_count)) { 
          return getitemcount(); 
      } 
 
      throw new illegalargumentexception("error method call: " + method); 
  } 
 
  private bundle getitemcount() { 
      log.i(log_tag, "articlesprovider.getitemcount"); 
 
      sqlitedatabase db = dbhelper.getreadabledatabase(); 
      cursor cursor = db.rawquery("select count(*) from " + db_table, null); 
 
      int count = 0; 
      if (cursor.movetofirst()) { 
          count = cursor.getint(0); 
      } 
 
      bundle bundle = new bundle(); 
      bundle.putint(articles.key_item_count, count); 
 
      cursor.close(); 
      db.close(); 
 
      return bundle; 
  } 
 
  private static class dbhelper extends sqliteopenhelper { 
      public dbhelper(context context, string name, cursorfactory factory, int version) { 
          super(context, name, factory, version); 
      } 
 
      @override 
      public void oncreate(sqlitedatabase db) { 
          db.execsql(db_create); 
      } 
 
      @override 
      public void onupgrade(sqlitedatabase db, int oldversion, int newversion) { 
          db.execsql("drop table if exists " + db_table); 
          oncreate(db); 
      } 
  } 
} 

我们需要创建一个类继承自contentprovider,并重载以下方法。 - oncreate(),用来执行一些初始化的工作。 - query(uri, string[], string, string[], string),用来返回数据给调用者。 - insert(uri, contentvalues),用来插入新的数据。 - update(uri, contentvalues, string, string[]),用来更新已有的数据。 - delete(uri, string, string[]),用来删除数据。 - gettype(uri),用来返回数据的mime类型。
具体使用参考 android应用程序组件content provider应用实例这篇博客,这里不再赘述。 在以上对contentprovider的使用过程中,我们发现过程比较繁琐,如果对于比较复杂的需求可能还比较使用,但是我们这里的需求其实很简单,完全不需要搞得那么复杂,所以最后我们也没有使用这个方法(你可以理解为本博主比较lazy)。

#broadcast 那么是否有更简单的方法呢?由于想到了contentprovider,我们不由地想到另一个android组件,broadcastreceiver。那么我们是否可以使用broadcast 将我们收到的推送数据发送给app进程呢。bingo,这似乎正是我们寻找的又简单又能解决问题的方法。我们来看下代码。

首先在推送进程收到推送消息时,我们将推送数据存入sp,如果这时候没有app进程,那么下次app进程启动的时候该存入的数据就会被app进程读取到。而如果这时候app进程存在,那么之后的代码就会生效,它使用localbroadcastmanager 发送一个广播。localbroadcastmanager发送的广播不会被app之外接收到,通过它注册的receiver也不会接收到app之外的广播,因此拥有更高的效率。

pushpref.add(push);

intent intent = new intent(pushhandler.key_get_push);
intent.putextra(pushhandler.key_push_content, d);
localbroadcastmanager.getinstance(context).sendbroadcastsync(intent);

而我们在app进程则注册了一个broadreceiver来接收上面发出的广播。在收到广播之后将推送数据存入sp。

public class pushhandler {

public static string key_get_push = "push_received";
public static string key_push_content = "push_content";

// region 推送处理push
/**
 * 当有推送时,发一次请求mpushreceiver
 */
private static broadcastreceiver mpushreceiver = new broadcastreceiver() {
  @override
  public void onreceive(context context, intent intent) {
    timber.i("在noticeaction中收到广播");
    pushpref pushpref = app.di().pushpref();
    try {
      string pushcontent = intent.getstringextra(key_push_content);
      pushentity pushentity = app.di().gson().fromjson(pushcontent, pushentity.class);
      pushpref.add(pushentity);
    } catch (exception e){
      timber.e(e, "存储推送内容出错");
    }
  }
};

public static void startlisteningtopush(){
  try {
    localbroadcastmanager.getinstance(app.getcontext()).registerreceiver(mpushreceiver, new intentfilter(key_get_push));
  } catch (exception e) {
    timber.e(e, "wtf");
  }
}
public static void stoplisteningtopush(){
  try {
    localbroadcastmanager.getinstance(app.getcontext()).unregisterreceiver(mpushreceiver);
  } catch (exception e) {
    timber.e(e, "wtf");
  }
}
// endregion
} 

该方法相对于上面的方法使用简单,安全可靠,能够比较好的实现我们的需求。不过,在需求比较复杂的时候还是建议使用contentprovider,因为毕竟这样的方法不是堂堂正道,有种剑走偏锋的感觉。
总结
实现一个需求可以有很多方法,而我们需要寻找的是又简单有可靠的方法,在写代码之前不如多找找资料,多听听别人的意见。

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网