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

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

(0)

相关推荐

  • Android中应用多进程的整理总结

    前言 在计算机操作系统中,进程是进行资源分配和调度的基本单位.这对于基于Linux内核的Android系统也不例外.在Android的设计中,一个应用默认有一个(主)进程.但是我们通过配置可以实现一个应用对应多个进程. 本文将试图对于Android中应用多进程做一些整理总结. android:process 应用实现多进程需要依赖于android:process这个属性 适用元素:Application, Activity, BroadcastReceiver, Service, Content

  • Android编程防止进程被第三方软件杀死的方法

    本文实例讲述了Android编程防止进程被第三方软件杀死的方法.分享给大家供大家参考,具体如下: 项目测试的时候发现,按home键回到桌面,再用360清理内存,软件被结束,再次进入的时候报错,看了下log,以为是有的地方没有控制好,但是又不知道360结束的是什么(这个现在还没弄明白).使用小米系统的进程管理优化内存就不报错. 后来想到用Service防止软件被kill掉,查了下资料,发现google 管方就有,ForegroundService 前台服务,让服务一直以前台任务的方式运行,可以在s

  • Android应用开发SharedPreferences存储数据的使用方法

    SharedPreferences是Android中最容易理解的数据存储技术,实际上SharedPreferences处理的就是一个key-value(键值对).SharedPreferences常用来存储一些轻量级的数据. 复制代码 代码如下: //实例化SharedPreferences对象(第一步) SharedPreferences mySharedPreferences= getSharedPreferences("test", Activity.MODE_PRIVATE);

  • Android 进程间通信实现原理分析

    Android Service是分为两种: 本地服务(Local Service): 同一个apk内被调用 远程服务(Remote Service):被另一个apk调用远程服务需要借助AIDL来完成. AIDL 是什么 AIDL (Android Interface Definition Language) 是一种IDL 语言,用于生成可以在Android设备上两个进程之间进行进程间通信(interprocess communication, IPC)的代码.如果在一个进程中(例如Activit

  • Android 动画之ScaleAnimation应用详解

    android中提供了4中动画: AlphaAnimation 透明度动画效果 ScaleAnimation 缩放动画效果 TranslateAnimation 位移动画效果 RotateAnimation 旋转动画效果 本节讲解ScaleAnimation 动画, ScaleAnimation(float fromX, float toX, float fromY, float toY,int pivotXType, float pivotXValue, int pivotYType, flo

  • Android结束进程的方法详解

    本文实例讲述了Android结束进程的方法.分享给大家供大家参考,具体如下: 最近在做一个类似与任务管理器的东西,里面有个功能,可以通过这个管理器结束掉其他的进程. 在Android平台下,结束进程的方法还是比较多的.首先指明,此处的"结束进程",包含了结束自身进程和结束其他进程两个方面.通过查阅SDK文档和网上的一些资料,自己找到一些结束进程的方法.在这里做一些归纳和总结,文章的部分信息有可能来自网上已有的文章和帖子,由于过了比较长时间,所以若发现本文与其他文章雷同,请谅解. 一.结

  • Android 多进程资料总结

    温故而知新 网上说多进程的文章很多,不过基本都是在讨论很深的东西,这是需要去专研的,而我是来这篇是用来偷懒的(应用层次),记录的都是自己对多进程的理解,方便以后用到的时候来偷懒,如果有错,请指教,小小程序员万分感谢. 讲进程,先来回答下面几个问题: 1.什么是线程? 线程就是程序中单独执行的流控制. 2.什么是多线程? 多线程就是单个程序中执行多个流控制.作用就是最大限度的使用CPU资源. 3.什么是进程? 一般指的是一个执行单元,即一个应用程序(Android中就是一个应用). 4.线程和进程

  • 解析后台进程对Android性能影响的详解

    Android现在这么火,各种的设备也是琳琅满目,高中低等,大小屏幕都有,但是它始终未能达到iOS那样的令人称赞的卓越体验和性能,其操作的流畅度,性能和安全性方面总是略输iOS一筹.据说iPhone4虽然是单核512M内存,但是比Android的双核1G内存的操作起来更流畅,iPad2虽然是也只有512M的内存但是操作起来比Android四核1G内存还要流畅.另外在安全性方面也不如iOS. 造成Android性能,待机时间,操作流畅和安全性不好的原因是Android后台进程的管理. Androi

  • Android 动画之TranslateAnimation应用详解

    android中提供了4中动画: AlphaAnimation 透明度动画效果 ScaleAnimation 缩放动画效果 TranslateAnimation 位移动画效果 RotateAnimation 旋转动画效果 本节讲解TranslateAnimation动画,TranslateAnimation比较常用,比如QQ,网易新闻菜单条的动画,就可以用TranslateAnimation实现, 通过TranslateAnimation(float fromXDelta, float toXD

  • Android应用程序四大组件之使用AIDL如何实现跨进程调用Service

    一.问题描述 Android应用程序的四大组件中Activity.BroadcastReceiver.ContentProvider.Service都可以进行跨进程.在上一篇我们通过ContentProvider实现了不同应用之间的跨进程调用,但ContentProvider主要是提供数据的共享(如sqlite数据库),那么我们希望跨进程调用服务(Service)呢?Android系统采用了远程过程调用(RPC)方式来实现.与很多其他的基于RPC的解决方案一样,Android使用一种接口定义语言

随机推荐