Analysis of Multi process Sharing Data in Android Development

  • 2021-07-03 00:49:02
  • OfStack

Background Recently, I encountered a requirement in my work, which needs to store the data obtained by push when I receive it, so that I can use it when app starts up. Do we think this is not So easy? Just store the data in SharedPreferences, and then let app open the same SharedPreferences to read the data. However, in the actual test, we found that the data stored in the push process cannot be obtained in the app process. So why is this? Perhaps clever readers have discovered the reason from our above statement. Because we have two processes, The push process is responsible for storing the push data, while the app process is responsible for reading it. However, because they are two processes, if they exist at the same time, they each keep their own SP objects and data in memory. The storage in the push process cannot be reflected in the app process, and the changed data may be brushed away by the app process. So what can we do to make these two sides share data? Please look at the statement below.

1. SharedPreferences with multi-process support (not recommended)
Our original practice was to use SharedPreferences, and it naturally occurred to us that SharedPreferences can actually set up multi-process Flag --MODE_MULTI_PROCESS in addition to MODE_PRIVATE MODE_PUBLIC

SharedPreferences myPrefs = context.getSharedPreferences(MY_FILE_NAME, Context.MODE_MULTI_PROCESS | Context.MODE_PRIVATE);

1 Once we set this Flag, the system will re-read data from the SP file every time we call Context. getSharedPreferences, so we should use Context. getSharedPreferences to re-obtain the SP instance every time we read and store it. Even so, because SP is not intrinsically multi-process secure, data synchronization cannot be guaranteed, so we do not use this method and we do not recommend it.

2. Tray
If SP is not multi-process secure, is there a third-party project that is multi-process secure and has SP functionality? The answer is yes, Tray-a multi-process secure SharedPreferences, which can be found on Github. If it is AndroidStudio, it can be directly introduced by Gradle, which is 10 points convenient. The following is the code used, 10 points simple, without apply commit, which looks simpler than 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

But in the end, we didn't choose to use it. The main reason is that it needs minSdk to be 15, and we support sdk14, so we can only give up decisively.

3. ContentProvider
Since Tray does not support sdk15, can we use the principle of Tray to implement one by ourselves? When reading the source code of Tray, we found that it is actually done on the basis of ContentProvider, and ContentProvider is officially supported by Android for multi-process security. The following is an example of using 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); 
      } 
  } 
} 

We need to create a class that inherits from ContentProvider and overloads the following methods. -onCreate (), which is used to perform some initialization work. -query (Uri, String [], String, String [], String) to return data to the caller. -insert (Uri, ContentValues) for inserting new data. -update (Uri, ContentValues, String, String []) to update existing data. -delete (Uri, String, String []) for deleting data. -getType (Uri), which is used to return the MIME type of the data.
Specific use of reference Android application component Content Provider application example of this blog, here will not be repeated. In the above process of using ContentProvider, we found that the process is cumbersome, and it may be used for more complex requirements, but our requirements here are actually very simple, so we don't need to make it so complicated at all, so we didn't use this method in the end (you can understand that this blogger compares Lazy).

# Broadcast So is there an easier way? As we think of ContentProvider, we can't help but think of another android component, BroadcastReceiver. So can we use Broadcast to send the push data we receive to the app process? bingo, this seems to be the simple solution we are looking for. Let's look at the code.

First, when the push process receives the push message, we store the push data into SP. If there is no app process at this time, the stored data will be read by app process next time the app process starts. If the app process exists at this time, then the subsequent code will take effect, and it uses LocalBroadcastManager to send a broadcast. Broadcasts sent by LocalBroadcastManager will not be received outside app, and Receiver registered through it will not receive broadcasts outside app, so it has higher efficiency.


pushPref.add(push);

Intent intent = new Intent(PushHandler.KEY_GET_PUSH);
intent.putExtra(PushHandler.KEY_PUSH_CONTENT, d);
LocalBroadcastManager.getInstance(context).sendBroadcastSync(intent);

We registered an BroadReceiver in the app process to receive the above broadcast. After receiving the broadcast, the push data is stored in SP.


public class PushHandler {

public static String KEY_GET_PUSH = "PUSH_RECEIVED";
public static String KEY_PUSH_CONTENT = "PUSH_CONTENT";

// region  Push processing push
/**
 *  When there is a push, send 1 Secondary request mPushReceiver
 */
private static BroadcastReceiver mPushReceiver = new BroadcastReceiver() {
  @Override
  public void onReceive(Context context, Intent intent) {
    Timber.i(" In NoticeAction Receive a broadcast in ");
    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, " Error storing push content ");
    }
  }
};

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
} 

Compared with the above methods, this method is simple, safe and reliable, and can better achieve our needs. However, it is recommended to use ContentProvider when the demand is complicated, because after all, this method is not the right way, and there is a feeling that the sword goes sideways.
Summarize
There are many ways to realize a requirement, but what we need to find is a simple and reliable way. It is better to look for more information and listen to other people's opinions before writing code.


Related articles: