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,因为毕竟这样的方法不是堂堂正道,有种剑走偏锋的感觉。
总结
实现一个需求可以有很多方法,而我们需要寻找的是又简单有可靠的方法,在写代码之前不如多找找资料,多听听别人的意见。
推荐:
不得不知Git远程操作详解
Android开发中多进程共享数据的更多相关文章
- android开发中获取<meta-data>数据
		
在 AndroidManifest.xml 中,<meta-data>元素是一个键值对,往往被包含在<application> .<activity>.<se ...
 - android开发中的5种存储数据方式
		
数据存储在开发中是使用最频繁的,根据不同的情况选择不同的存储数据方式对于提高开发效率很有帮助.下面笔者在主要介绍Android平台中实现数据存储的5种方式. 1.使用SharedPreferences ...
 - Nodejs中cluster模块的多进程共享数据问题
		
Nodejs中cluster模块的多进程共享数据问题 前述 nodejs在v0.6.x之后增加了一个模块cluster用于实现多进程,利用child_process模块来创建和管理进程,增加程序在多核 ...
 - Android开发中Bundle用法包裹数据(转)
		
Android开发中Bundle用法包裹数据 Bundle的经典用法,包裹数据放入Intent中,目的在于传输数据. SDK 里是这样描述: A mapping from String values ...
 - MVP模式在Android开发中的应用
		
一.MVP介绍 随着UI创建技术的功能日益增强,UI层也履行着越来越多的职责.为了更好地细分视图(View)与模型(Model)的功能,让View专注于处理数据的可视化以及与用户的交互.同一 ...
 - 讨论Android开发中的MVC设计思想
		
最近闲着没事,总是想想做点什么.在时间空余之时给大家说说MVC设计思想在Android开发中的运用吧! MVC设计思想在Android开发中一直都是一套比较好的设计思想.很多APP的设计都是使用这套方 ...
 - Android学习探索之Java 8 在Android 开发中的应用
		
前言: Java 8推出已经将近2年多了,引入很多革命性变化,加入了函数式编程的特征,使基于行为的编程成为可能,同时减化了各种设计模式的实现方式,是Java有史以来最重要的更新.但是Android上, ...
 - Android开发中的输入合法性检验
		
Why ? 合法性检查对于程序的健壮性具有重要作用.在Android开发中,良好的合法性检查设计机制可以使程序更加清晰,产生bug更少,交互更加友好. What ? 合法性检查的目的在于确定边界.对于 ...
 - Android开发中,那些让您觉得相见恨晚的方法、类或接口
		
Android开发中,那些让你觉得相见恨晚的方法.类或接口本篇文章内容提取自知乎Android开发中,有哪些让你觉得相见恨晚的方法.类或接口?,其实有一部是JAVA的,但是在android开发中也算常 ...
 
随机推荐
- java与C#对比文章阅读
			
文章:JAVA与C#的区别 讲了C#与java一些基本异同. 易语言官网有个表,比较了易语言.Java.C#的区别,比较全面可以借鉴.
 - 轻松精通awk数组企业问题案例
			
考试题1:处理以下文件内容,将域名取出并根据域名进行计数排序处理:(百度和sohu面试题) oldboy.log http://www.etiantian.org/index.html http:// ...
 - 【bzoj3781】小B的询问  莫队算法
			
原文地址:http://www.cnblogs.com/GXZlegend/p/6803821.html 题目描述 小B有一个序列,包含N个1~K之间的整数.他一共有M个询问,每个询问给定一个区间[L ...
 - 【bzoj2424】[HAOI2010]订货  费用流
			
原文地址:http://www.cnblogs.com/GXZlegend/p/6825296.html 题目描述 某公司估计市场在第i个月对某产品的需求量为Ui,已知在第i月该产品的订货单价为di, ...
 - HTML5 localStorage与document.domain设置问题
			
localStorage的写入和读取,不能跨子域,否则在一些移动端浏览器上,会出现读取不到的情况. 最近开发一个移动端的播放记录功能,在pc端和android版的chrome测试很顺利通过了,但后来进 ...
 - CANO入门(三)
			
最好的学习方式是什么?模仿.有人会问,那不是山寨么?但是我认为,那是模仿的初级阶段,当把别人最好的设计已经融化到自己的血液里,变成自己的东西,而灵活运用的时候,才是真正高级阶段.正所谓画虎画皮难画骨. ...
 - POJ1386 Play on Words
			
Time Limit: 1000MS Memory Limit: 10000KB 64bit IO Format: %I64d & %I64u Description Some of ...
 - ACdream 1210	Chinese Girls' Amusement(高精度)
			
Chinese Girls' Amusement Time Limit:1000MS Memory Limit:64000KB 64bit IO Format:%lld & ...
 - Windows ToolTips简要介绍(转)
			
原文转自 https://blog.csdn.net/sesiria/article/details/77450151 Windows 标准控件ToolTips简要介绍 参考文档 MSDN https ...
 - Linux的日志错误级别
			
讯息等级 系统将讯息分为七个主要的等级,依序是由不重要排列到重要讯息等级: info:仅是一些基本的讯息说明而已: notice:比 info 还需要被注意到的一些信息内容: warning 或 wa ...