欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

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

程序员文章站 2024-02-27 16:50:09
 背景 最近在工作中遇到一个需求,需要在接收到推送的时候将推送获得的数据存起来,以供app启动时使用。我们会认为这不是so easy吗?只要把数据存到shared...

 背景 最近在工作中遇到一个需求,需要在接收到推送的时候将推送获得的数据存起来,以供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,因为毕竟这样的方法不是堂堂正道,有种剑走偏锋的感觉。
总结
实现一个需求可以有很多方法,而我们需要寻找的是又简单有可靠的方法,在写代码之前不如多找找资料,多听听别人的意见。