Android开发中多进程共享数据简析
背景 最近在工作中遇到一个需求,需要在接收到推送的时候将推送获得的数据存起来,以供app启动时使用。我们会认为这不是so easy吗?只要把数据存到sharedpreferences中,然后让app打开同一个sharedpreferences读取数据就可以了。但是在实际的测试中,我们发现推送进程存入的数据,并不能在app进程中获得。所以这是为什么呢,也许聪明的读者从我们上面的陈述中已经发现了原因,因为我们有两个进程,推送进程负责将推送数据存入,而app进程负责读取,但是正是由于是两个进程,如果它们同时存在,它们各自在内存中保持了自己的sp对象和数据,在推送进程中的存入并不能在app进程体现出来,并且可能会被app进程刷掉更改的数据。那么我们怎么做才能让这两边共享数据呢?请看下面陈述。
一、多进程支持的sharedpreferences(不推荐)
我们原来的做法是使用sharedpreferences, 自然而然想到,sharedpreferences 在mode_private mode_public 之外其实还可以设置多进程的flag ———— mode_multi_process
一旦我们设置了这个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,因为毕竟这样的方法不是堂堂正道,有种剑走偏锋的感觉。
总结
实现一个需求可以有很多方法,而我们需要寻找的是又简单有可靠的方法,在写代码之前不如多找找资料,多听听别人的意见。
上一篇: 浅析MySQL的注入安全问题
下一篇: 多项式全家桶(三):多项式的ln,exp
推荐阅读