Android开发中多进程共享数据简析
背景最近在工作中遇到一个需求,需要在接收到推送的时候将推送获得的数据存起来,以供app启动时使用。我们会认为这不是Soeasy吗?只要把数据存到SharedPreferences中,然后让app打开同一个SharedPreferences读取数据就可以了。但是在实际的测试中,我们发现推送进程存入的数据,并不能在app进程中获得。所以这是为什么呢,也许聪明的读者从我们上面的陈述中已经发现了原因,因为我们有两个进程,推送进程负责将推送数据存入,而app进程负责读取,但是正是由于是两个进程,如果它们同时存在,它们各自在内存中保持了自己的SP对象和数据,在推送进程中的存入并不能在app进程体现出来,并且可能会被app进程刷掉更改的数据。那么我们怎么做才能让这两边共享数据呢?请看下面陈述。
一、多进程支持的SharedPreferences(不推荐)
我们原来的做法是使用SharedPreferences,自然而然想到,SharedPreferences在MODE_PRIVATEMODE_PUBLIC之外其实还可以设置多进程的Flag————MODE_MULTI_PROCESS
SharedPreferencesmyPrefs=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引入,可谓是十分方便,如下是使用的代码,十分简单,没有applycommit,看起来比SP还要简单。
//createapreferenceaccessor.Thisisforglobalapppreferences. finalAppPreferencesappPreferences=newAppPreferences(getContext());//thisPreferencecomesforfreefromthelibrary //saveakeyvaluepair appPreferences.put("key","loremipsum"); //readthevalueforyourkey.thesecondparameterisafallback(optionalotherwisethrows) finalStringvalue=appPreferences.getString("key","default"); Log.v(TAG,"value:"+value);//value:loremipsum //readakeythatisn'tsaved.returnsthedefault(orthrowswithoutdefault) finalStringdefaultValue=appPreferences.getString("key2","default"); Log.v(TAG,"value:"+defaultValue);//value:default
但是最终我们并没有选择使用它,主要的原因是它需要minSdk为15,而我们是支持sdk14的,所以只能果断放弃了。
三、ContentProvider
既然Tray不支持sdk15以下的,那么我们是否可以使用Tray的原理自己实现一个呢?在阅读Tray的源码时我们发现其实它是在ContentProvider的基础上做的,而ContentProvider是Android官方支持的多进程安全的。以下是使用ContentProvider的一个例子。
publicclassArticlesProviderextendsContentProvider{ privatestaticfinalStringLOG_TAG="shy.luo.providers.articles.ArticlesProvider"; privatestaticfinalStringDB_NAME="Articles.db"; privatestaticfinalStringDB_TABLE="ArticlesTable"; privatestaticfinalintDB_VERSION=1; privatestaticfinalStringDB_CREATE="createtable"+DB_TABLE+ "("+Articles.ID+"integerprimarykeyautoincrement,"+ Articles.TITLE+"textnotnull,"+ Articles.ABSTRACT+"textnotnull,"+ Articles.URL+"textnotnull);"; privatestaticfinalUriMatcheruriMatcher; static{ uriMatcher=newUriMatcher(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); } privatestaticfinalHashMap<String,String>articleProjectionMap; static{ articleProjectionMap=newHashMap<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); } privateDBHelperdbHelper=null; privateContentResolverresolver=null; @Override publicbooleanonCreate(){ Contextcontext=getContext(); resolver=context.getContentResolver(); dbHelper=newDBHelper(context,DB_NAME,null,DB_VERSION); Log.i(LOG_TAG,"ArticlesProviderCreate"); returntrue; } @Override publicStringgetType(Uriuri){ switch(uriMatcher.match(uri)){ caseArticles.ITEM: returnArticles.CONTENT_TYPE; caseArticles.ITEM_ID: caseArticles.ITEM_POS: returnArticles.CONTENT_ITEM_TYPE; default: thrownewIllegalArgumentException("ErrorUri:"+uri); } } @Override publicUriinsert(Uriuri,ContentValuesvalues){ if(uriMatcher.match(uri)!=Articles.ITEM){ thrownewIllegalArgumentException("ErrorUri:"+uri); } SQLiteDatabasedb=dbHelper.getWritableDatabase(); longid=db.insert(DB_TABLE,Articles.ID,values); if(id<0){ thrownewSQLiteException("Unabletoinsert"+values+"for"+uri); } UrinewUri=ContentUris.withAppendedId(uri,id); resolver.notifyChange(newUri,null); returnnewUri; } @Override publicintupdate(Uriuri,ContentValuesvalues,Stringselection,String[]selectionArgs){ SQLiteDatabasedb=dbHelper.getWritableDatabase(); intcount=0; switch(uriMatcher.match(uri)){ caseArticles.ITEM:{ count=db.update(DB_TABLE,values,selection,selectionArgs); break; } caseArticles.ITEM_ID:{ Stringid=uri.getPathSegments().get(1); count=db.update(DB_TABLE,values,Articles.ID+"="+id +(!TextUtils.isEmpty(selection)?"and("+selection+')':""),selectionArgs); break; } default: thrownewIllegalArgumentException("ErrorUri:"+uri); } resolver.notifyChange(uri,null); returncount; } @Override publicintdelete(Uriuri,Stringselection,String[]selectionArgs){ SQLiteDatabasedb=dbHelper.getWritableDatabase(); intcount=0; switch(uriMatcher.match(uri)){ caseArticles.ITEM:{ count=db.delete(DB_TABLE,selection,selectionArgs); break; } caseArticles.ITEM_ID:{ Stringid=uri.getPathSegments().get(1); count=db.delete(DB_TABLE,Articles.ID+"="+id +(!TextUtils.isEmpty(selection)?"and("+selection+')':""),selectionArgs); break; } default: thrownewIllegalArgumentException("ErrorUri:"+uri); } resolver.notifyChange(uri,null); returncount; } @Override publicCursorquery(Uriuri,String[]projection,Stringselection,String[]selectionArgs,StringsortOrder){ Log.i(LOG_TAG,"ArticlesProvider.query:"+uri); SQLiteDatabasedb=dbHelper.getReadableDatabase(); SQLiteQueryBuildersqlBuilder=newSQLiteQueryBuilder(); Stringlimit=null; switch(uriMatcher.match(uri)){ caseArticles.ITEM:{ sqlBuilder.setTables(DB_TABLE); sqlBuilder.setProjectionMap(articleProjectionMap); break; } caseArticles.ITEM_ID:{ Stringid=uri.getPathSegments().get(1); sqlBuilder.setTables(DB_TABLE); sqlBuilder.setProjectionMap(articleProjectionMap); sqlBuilder.appendWhere(Articles.ID+"="+id); break; } caseArticles.ITEM_POS:{ Stringpos=uri.getPathSegments().get(1); sqlBuilder.setTables(DB_TABLE); sqlBuilder.setProjectionMap(articleProjectionMap); limit=pos+",1"; break; } default: thrownewIllegalArgumentException("ErrorUri:"+uri); } Cursorcursor=sqlBuilder.query(db,projection,selection,selectionArgs,null,null,TextUtils.isEmpty(sortOrder)?Articles.DEFAULT_SORT_ORDER:sortOrder,limit); cursor.setNotificationUri(resolver,uri); returncursor; } @Override publicBundlecall(Stringmethod,Stringrequest,Bundleargs){ Log.i(LOG_TAG,"ArticlesProvider.call:"+method); if(method.equals(Articles.METHOD_GET_ITEM_COUNT)){ returngetItemCount(); } thrownewIllegalArgumentException("Errormethodcall:"+method); } privateBundlegetItemCount(){ Log.i(LOG_TAG,"ArticlesProvider.getItemCount"); SQLiteDatabasedb=dbHelper.getReadableDatabase(); Cursorcursor=db.rawQuery("selectcount(*)from"+DB_TABLE,null); intcount=0; if(cursor.moveToFirst()){ count=cursor.getInt(0); } Bundlebundle=newBundle(); bundle.putInt(Articles.KEY_ITEM_COUNT,count); cursor.close(); db.close(); returnbundle; } privatestaticclassDBHelperextendsSQLiteOpenHelper{ publicDBHelper(Contextcontext,Stringname,CursorFactoryfactory,intversion){ super(context,name,factory,version); } @Override publicvoidonCreate(SQLiteDatabasedb){ db.execSQL(DB_CREATE); } @Override publicvoidonUpgrade(SQLiteDatabasedb,intoldVersion,intnewVersion){ db.execSQL("DROPTABLEIFEXISTS"+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应用程序组件ContentProvider应用实例这篇博客,这里不再赘述。在以上对ContentProvider的使用过程中,我们发现过程比较繁琐,如果对于比较复杂的需求可能还比较使用,但是我们这里的需求其实很简单,完全不需要搞得那么复杂,所以最后我们也没有使用这个方法(你可以理解为本博主比较Lazy)。
#Broadcast那么是否有更简单的方法呢?由于想到了ContentProvider,我们不由地想到另一个android组件,BroadcastReceiver。那么我们是否可以使用Broadcast将我们收到的推送数据发送给app进程呢。bingo,这似乎正是我们寻找的又简单又能解决问题的方法。我们来看下代码。
首先在推送进程收到推送消息时,我们将推送数据存入SP,如果这时候没有app进程,那么下次app进程启动的时候该存入的数据就会被app进程读取到。而如果这时候app进程存在,那么之后的代码就会生效,它使用LocalBroadcastManager发送一个广播。LocalBroadcastManager发送的广播不会被app之外接收到,通过它注册的Receiver也不会接收到app之外的广播,因此拥有更高的效率。
pushPref.add(push); Intentintent=newIntent(PushHandler.KEY_GET_PUSH); intent.putExtra(PushHandler.KEY_PUSH_CONTENT,d); LocalBroadcastManager.getInstance(context).sendBroadcastSync(intent);
而我们在app进程则注册了一个BroadReceiver来接收上面发出的广播。在收到广播之后将推送数据存入SP。
publicclassPushHandler{ publicstaticStringKEY_GET_PUSH="PUSH_RECEIVED"; publicstaticStringKEY_PUSH_CONTENT="PUSH_CONTENT"; //region推送处理push /** *当有推送时,发一次请求mPushReceiver */ privatestaticBroadcastReceivermPushReceiver=newBroadcastReceiver(){ @Override publicvoidonReceive(Contextcontext,Intentintent){ Timber.i("在NoticeAction中收到广播"); PushPrefpushPref=App.DI().pushPref(); try{ StringpushContent=intent.getStringExtra(KEY_PUSH_CONTENT); PushEntitypushEntity=App.DI().gson().fromJson(pushContent,PushEntity.class); pushPref.add(pushEntity); }catch(Exceptione){ Timber.e(e,"存储推送内容出错"); } } }; publicstaticvoidstartListeningToPush(){ try{ LocalBroadcastManager.getInstance(App.getContext()).registerReceiver(mPushReceiver,newIntentFilter(KEY_GET_PUSH)); }catch(Exceptione){ Timber.e(e,"wtf"); } } publicstaticvoidstopListeningToPush(){ try{ LocalBroadcastManager.getInstance(App.getContext()).unregisterReceiver(mPushReceiver); }catch(Exceptione){ Timber.e(e,"wtf"); } } //endregion }
该方法相对于上面的方法使用简单,安全可靠,能够比较好的实现我们的需求。不过,在需求比较复杂的时候还是建议使用ContentProvider,因为毕竟这样的方法不是堂堂正道,有种剑走偏锋的感觉。
总结
实现一个需求可以有很多方法,而我们需要寻找的是又简单有可靠的方法,在写代码之前不如多找找资料,多听听别人的意见。